From a3dd93dd9ada931fd96acd960748123de67c2a75 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Tue, 11 Oct 2022 21:49:41 +0800 Subject: [PATCH 0001/1392] strategy:irr: add backtest/realtime ability --- config/irr.yaml | 10 +- pkg/strategy/irr/strategy.go | 187 ++++++++++++++++++++++++----------- 2 files changed, 138 insertions(+), 59 deletions(-) diff --git a/config/irr.yaml b/config/irr.yaml index 5aa782bc50..95954f4d58 100644 --- a/config/irr.yaml +++ b/config/irr.yaml @@ -15,9 +15,10 @@ exchangeStrategies: - on: binance irr: symbol: BTCBUSD - interval: 1m + interval: 1s window: 120 - amount: 5_000.0 + amount: 500.0 + humpThreshold: 0.000025 # Draw pnl drawGraph: true graphPNLPath: "./pnl.png" @@ -26,8 +27,9 @@ exchangeStrategies: backtest: sessions: - binance - startTime: "2022-09-01" - endTime: "2022-10-04" + startTime: "2022-10-09" + endTime: "2022-10-11" +# syncSecKLines: true symbols: - BTCBUSD accounts: diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index 75e5910d61..27fc9c5645 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -9,17 +9,19 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" + "github.com/sirupsen/logrus" + "math" "os" "sync" - - "github.com/sirupsen/logrus" + "time" ) const ID = "irr" var one = fixedpoint.One var zero = fixedpoint.Zero -var Fee = 0.0008 // taker fee % * 2, for upper bound +var Fee = 0.000 // taker fee % * 2, for upper bound var log = logrus.WithField("strategy", ID) @@ -47,7 +49,16 @@ type Strategy struct { orderExecutor *bbgo.GeneralOrderExecutor bbgo.QuantityOrAmount - nrr *NRR + + HumpThreshold float64 `json:"humpThreshold"` + + lastTwoPrices *types.Queue + // for back-test + Nrr *NRR + // for realtime book ticker + lastPrice fixedpoint.Value + rtNrr *types.Queue + stopC chan struct{} // StrategyController bbgo.StrategyController @@ -194,10 +205,10 @@ func (r *AccumulatedProfitReport) Output(symbol string) { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) - if !bbgo.IsBackTesting { - session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) + } else { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } s.ExitMethods.SetAndSubscribe(session, s) @@ -273,9 +284,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.AccumulatedProfitReport.RecordProfit(profit.Profit) }) - // s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - // s.AccumulatedProfitReport.RecordTrade(trade.Fee) - // }) session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1d, func(kline types.KLine) { s.AccumulatedProfitReport.DailyUpdate(s.TradeStats) })) @@ -319,63 +327,59 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) + s.InitDrawCommands(&profitSlice, &cumProfitSlice) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) s.orderExecutor.Bind() s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - for _, method := range s.ExitMethods { - method.Bind(session, s.orderExecutor) - } - + //back-test only, because 1s delayed a lot kLineStore, _ := s.session.MarketDataStore(s.Symbol) - s.nrr = &NRR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}, RankingWindow: s.Window} - s.nrr.BindK(s.session.MarketDataStream, s.Symbol, s.Interval) - if klines, ok := kLineStore.KLinesOfInterval(s.nrr.Interval); ok { - s.nrr.LoadK((*klines)[0:]) + s.Nrr = &NRR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}, RankingWindow: s.Window} + s.Nrr.BindK(s.session.MarketDataStream, s.Symbol, s.Interval) + if klines, ok := kLineStore.KLinesOfInterval(s.Nrr.Interval); ok { + s.Nrr.LoadK((*klines)[0:]) } - // startTime := s.Environment.StartTime() - // s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1h, startTime)) - - s.session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - - // ts_rank(): transformed to [0~1] which divided equally - // queued first signal as its initial process - // important: delayed signal in order to submit order at current kline close (a.k.a. next open while in production) - // instead of right in current kline open - - // alpha-weighted assets (inventory and capital) - targetBase := s.QuantityOrAmount.CalculateQuantity(kline.Close).Mul(fixedpoint.NewFromFloat(s.nrr.RankedValues.Index(1))) - diffQty := targetBase.Sub(s.Position.Base) - - log.Infof("decision alpah: %f, ranked negative return: %f, current position: %f, target position diff: %f", s.nrr.RankedValues.Index(1), s.nrr.RankedValues.Last(), s.Position.Base.Float64(), diffQty.Float64()) - - // use kline direction to prevent reversing position too soon - if diffQty.Sign() > 0 { // && kline.Direction() >= 0 - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Quantity: diffQty.Abs(), - Type: types.OrderTypeMarket, - Tag: "irr buy more", - }) - } else if diffQty.Sign() < 0 { // && kline.Direction() <= 0 - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Quantity: diffQty.Abs(), - Type: types.OrderTypeMarket, - Tag: "irr sell more", - }) - } + s.lastTwoPrices = types.NewQueue(2) // current price & previous price + s.rtNrr = types.NewQueue(s.Window) + if !bbgo.IsBackTesting { - })) + s.stopC = make(chan struct{}) + + go func() { + secondTicker := time.NewTicker(util.MillisecondsJitter(s.Interval.Duration(), 200)) + defer secondTicker.Stop() + + for { + select { + case <-secondTicker.C: + s.rebalancePosition(true) + case <-s.stopC: + log.Warnf("%s goroutine stopped, due to the stop signal", s.Symbol) + return + + case <-ctx.Done(): + log.Warnf("%s goroutine stopped, due to the cancelled context", s.Symbol) + return + } + } + }() + + s.session.MarketDataStream.OnBookTickerUpdate(func(bt types.BookTicker) { + s.lastPrice = bt.Buy.Add(bt.Sell).Div(fixedpoint.Two) + }) + } else { + s.session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + s.lastPrice = kline.Close + s.rebalancePosition(false) + }) + } bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - // Output accumulated profit report if bbgo.IsBackTesting { defer s.AccumulatedProfitReport.Output(s.Symbol) @@ -385,8 +389,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.WithError(err).Errorf("cannot draw graph") } } + } else { + close(s.stopC) } - _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) _ = s.orderExecutor.GracefulCancel(ctx) }) @@ -398,3 +403,75 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { balances := s.session.GetAccount().Balances() return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) } + +func (s *Strategy) rebalancePosition(rt bool) { + + s.lastTwoPrices.Update(s.lastPrice.Float64()) + if s.lastTwoPrices.Length() >= 2 { + log.Infof("Interval Closed Price: %f", s.lastTwoPrices.Last()) + // main idea: negative return + nr := -1 * (s.lastTwoPrices.Last()/s.lastTwoPrices.Index(1) - 1) + if rt { + // hump operation to reduce noise + // update nirr indicator when above threshold + if math.Abs(s.rtNrr.Last()-nr) < s.HumpThreshold { + s.rtNrr.Update(s.rtNrr.Last()) + } else { + s.rtNrr.Update(nr) + return + } + } else { + if math.Abs(s.Nrr.Last()-s.Nrr.Index(1)) < s.HumpThreshold { + return + } + } + + // when have enough Nrr to do ts_rank() + if (s.rtNrr.Length() >= s.Window && rt) || (s.Nrr.Length() >= s.Window && !rt) { + + // alpha-weighted assets (inventory and capital) + position := s.orderExecutor.Position() + // weight: 0~1, since it's a long only strategy + weight := fixedpoint.NewFromFloat(s.rtNrr.Rank(s.Window).Last() / float64(s.Window)) + if !rt { + weight = fixedpoint.NewFromFloat(s.Nrr.Rank(s.Window).Last() / float64(s.Window)) + } + targetBase := s.QuantityOrAmount.CalculateQuantity(fixedpoint.NewFromFloat(s.lastTwoPrices.Mean(2))).Mul(weight) + + // to buy/sell quantity + diffQty := targetBase.Sub(position.Base) + log.Infof("Alpha: %f/1.0, Target Position Diff: %f", weight.Float64(), diffQty.Float64()) + + // ignore small changes + if diffQty.Abs().Float64() < 0.001 { + return + } + // re-balance position + if diffQty.Sign() > 0 { + _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Quantity: diffQty.Abs(), + Type: types.OrderTypeMarket, + //Price: bt.Sell, + Tag: "irr re-balance: buy", + }) + if err != nil { + log.WithError(err) + } + } else if diffQty.Sign() < 0 { + _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Quantity: diffQty.Abs(), + Type: types.OrderTypeMarket, + //Price: bt.Buy, + Tag: "irr re-balance: sell", + }) + if err != nil { + log.WithError(err) + } + } + } + } +} From 150c37995e43cc2080fb0742abd5b070df723f99 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Mon, 17 Oct 2022 21:34:55 +0800 Subject: [PATCH 0002/1392] strategy:irr redesign trigger --- config/irr.yaml | 20 +-- pkg/strategy/irr/draw.go | 35 ++-- pkg/strategy/irr/neg_return_rate.go | 24 +-- pkg/strategy/irr/strategy.go | 238 +++++++++++++++------------- 4 files changed, 164 insertions(+), 153 deletions(-) diff --git a/config/irr.yaml b/config/irr.yaml index 95954f4d58..9e8fd56acb 100644 --- a/config/irr.yaml +++ b/config/irr.yaml @@ -15,25 +15,11 @@ exchangeStrategies: - on: binance irr: symbol: BTCBUSD - interval: 1s - window: 120 + # in milliseconds(ms) + hftInterval: 1000 + # maxima position in USD amount: 500.0 - humpThreshold: 0.000025 # Draw pnl drawGraph: true graphPNLPath: "./pnl.png" graphCumPNLPath: "./cumpnl.png" - -backtest: - sessions: - - binance - startTime: "2022-10-09" - endTime: "2022-10-11" -# syncSecKLines: true - symbols: - - BTCBUSD - accounts: - binance: - takerFeeRate: 0.0 - balances: - BUSD: 5_000.0 diff --git a/pkg/strategy/irr/draw.go b/pkg/strategy/irr/draw.go index cf9f98bfa9..98f99d818e 100644 --- a/pkg/strategy/irr/draw.go +++ b/pkg/strategy/irr/draw.go @@ -11,26 +11,41 @@ import ( "github.com/wcharczuk/go-chart/v2" ) -func (s *Strategy) InitDrawCommands(profit, cumProfit types.Series) { - bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { +func (s *Strategy) InitDrawCommands(profit, cumProfit, cumProfitDollar types.Series) { + bbgo.RegisterCommand("/rt", "Draw Return Rate(%) Per Trade", func(reply interact.Reply) { + canvas := DrawPNL(s.InstanceID(), profit) var buffer bytes.Buffer if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render pnl in drift") - reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err)) + log.WithError(err).Errorf("cannot render return in irr") + reply.Message(fmt.Sprintf("[error] cannot render return in irr: %v", err)) return } - bbgo.SendPhoto(&buffer) + go bbgo.SendPhoto(&buffer) }) - bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { + bbgo.RegisterCommand("/nav", "Draw Net Assets Value", func(reply interact.Reply) { + canvas := DrawCumPNL(s.InstanceID(), cumProfit) var buffer bytes.Buffer if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render cumpnl in drift") - reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) + log.WithError(err).Errorf("cannot render nav in irr") + reply.Message(fmt.Sprintf("[error] canot render nav in irr: %v", err)) + return + } + go bbgo.SendPhoto(&buffer) + + }) + bbgo.RegisterCommand("/pnl", "Draw Cumulative Profit & Loss", func(reply interact.Reply) { + + canvas := DrawCumPNL(s.InstanceID(), cumProfitDollar) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in irr") + reply.Message(fmt.Sprintf("[error] canot render pnl in irr: %v", err)) return } - bbgo.SendPhoto(&buffer) + go bbgo.SendPhoto(&buffer) + }) } @@ -77,7 +92,7 @@ func DrawPNL(instanceID string, profit types.Series) *types.Canvas { func DrawCumPNL(instanceID string, cumProfit types.Series) *types.Canvas { canvas := types.NewCanvas(instanceID) - canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length()) + canvas.PlotRaw("cumulative pnl", cumProfit, cumProfit.Length()) canvas.YAxis = chart.YAxis{ ValueFormatter: func(v interface{}) string { if vf, isFloat := v.(float64); isFloat { diff --git a/pkg/strategy/irr/neg_return_rate.go b/pkg/strategy/irr/neg_return_rate.go index 09cee398b7..b3ec5edcc7 100644 --- a/pkg/strategy/irr/neg_return_rate.go +++ b/pkg/strategy/irr/neg_return_rate.go @@ -30,18 +30,21 @@ type NRR struct { var _ types.SeriesExtend = &NRR{} -func (inc *NRR) Update(price float64) { +func (inc *NRR) Update(openPrice, closePrice float64) { if inc.SeriesBase.Series == nil { inc.SeriesBase.Series = inc inc.Prices = types.NewQueue(inc.Window) } - inc.Prices.Update(price) + inc.Prices.Update(closePrice) if inc.Prices.Length() < inc.Window { return } - irr := (inc.Prices.Last() / inc.Prices.Index(inc.Window-1)) - 1 + // D0 + irr := openPrice - closePrice + // D1 + // -1*((inc.Prices.Last() / inc.Prices.Index(inc.Window-1)) - 1) - inc.Values.Push(-irr) // neg ret here + inc.Values.Push(irr) // neg ret here inc.RankedValues.Push(inc.Rank(inc.RankingWindow).Last() / float64(inc.RankingWindow)) // ranked neg ret here } @@ -75,7 +78,7 @@ func (inc *NRR) PushK(k types.KLine) { return } - inc.Update(indicator.KLineClosePriceMapper(k)) + inc.Update(indicator.KLineOpenPriceMapper(k), indicator.KLineClosePriceMapper(k)) inc.EndTime = k.EndTime.Time() inc.EmitUpdate(inc.Last()) } @@ -86,14 +89,3 @@ func (inc *NRR) LoadK(allKLines []types.KLine) { } inc.EmitUpdate(inc.Last()) } - -//func calculateReturn(klines []types.KLine, window int, val KLineValueMapper) (float64, error) { -// length := len(klines) -// if length == 0 || length < window { -// return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) -// } -// -// rate := val(klines[length-1])/val(klines[length-2]) - 1 -// -// return rate, nil -//} diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index 27fc9c5645..f7b14c1741 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -9,12 +9,10 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/util" "github.com/sirupsen/logrus" - "math" + "go.uber.org/atomic" "os" "sync" - "time" ) const ID = "irr" @@ -50,15 +48,26 @@ type Strategy struct { bbgo.QuantityOrAmount - HumpThreshold float64 `json:"humpThreshold"` + Interval int `json:"hftInterval"` - lastTwoPrices *types.Queue // for back-test Nrr *NRR - // for realtime book ticker - lastPrice fixedpoint.Value - rtNrr *types.Queue - stopC chan struct{} + Ma *indicator.SMA + // realtime book ticker to submit order + obBuyPrice *atomic.Float64 + obSellPrice *atomic.Float64 + // for negative return rate + openPrice float64 + closePrice float64 + rtNr *types.Queue + // for moving average reversion + rtMaFast *types.Queue + rtMaSlow *types.Queue + rtMr *types.Queue + + // for final alpha (Nr+Mr)/2 + rtWeight *types.Queue + stopC chan struct{} // StrategyController bbgo.StrategyController @@ -206,12 +215,10 @@ func (r *AccumulatedProfitReport) Output(symbol string) { func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { if !bbgo.IsBackTesting { + session.Subscribe(types.AggTradeChannel, s.Symbol, types.SubscribeOptions{}) session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) - } else { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } - - s.ExitMethods.SetAndSubscribe(session, s) + //session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } func (s *Strategy) ID() string { @@ -249,7 +256,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Cancel active orders _ = s.orderExecutor.GracefulCancel(ctx) // Close 100% position - // _ = s.ClosePosition(ctx, fixedpoint.One) + _ = s.orderExecutor.ClosePosition(ctx, fixedpoint.One) }) // initial required information @@ -294,6 +301,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se price, _ := session.LastPrice(s.Symbol) initAsset := s.CalcAssetValue(price).Float64() cumProfitSlice := floats.Slice{initAsset, initAsset} + profitDollarSlice := floats.Slice{0, 0} + cumProfitDollarSlice := floats.Slice{0, 0} s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { if bbgo.IsBackTesting { @@ -309,6 +318,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se profitSlice.Update(s.sellPrice / price) cumProfitSlice.Update(s.CalcAssetValue(trade.Price).Float64()) } + profitDollarSlice.Update(profit.Float64()) + cumProfitDollarSlice.Update(profitDollarSlice.Sum()) if s.Position.IsDust(trade.Price) { s.buyPrice = 0 s.sellPrice = 0 @@ -327,7 +338,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - s.InitDrawCommands(&profitSlice, &cumProfitSlice) + s.InitDrawCommands(&profitSlice, &cumProfitSlice, &cumProfitDollarSlice) s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) @@ -336,45 +347,81 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) //back-test only, because 1s delayed a lot - kLineStore, _ := s.session.MarketDataStore(s.Symbol) - s.Nrr = &NRR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}, RankingWindow: s.Window} - s.Nrr.BindK(s.session.MarketDataStream, s.Symbol, s.Interval) - if klines, ok := kLineStore.KLinesOfInterval(s.Nrr.Interval); ok { - s.Nrr.LoadK((*klines)[0:]) - } + //kLineStore, _ := s.session.MarketDataStore(s.Symbol) + //s.Nrr = &NRR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}, RankingWindow: s.Window} + //s.Nrr.BindK(s.session.MarketDataStream, s.Symbol, s.Interval) + //if klines, ok := kLineStore.KLinesOfInterval(s.Nrr.Interval); ok { + // s.Nrr.LoadK((*klines)[0:]) + //} + //s.Ma = &indicator.SMA{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}} + //s.Ma.BindK(s.session.MarketDataStream, s.Symbol, s.Interval) + //if klines, ok := kLineStore.KLinesOfInterval(s.Ma.Interval); ok { + // s.Ma.LoadK((*klines)[0:]) + //} - s.lastTwoPrices = types.NewQueue(2) // current price & previous price - s.rtNrr = types.NewQueue(s.Window) - if !bbgo.IsBackTesting { + s.rtNr = types.NewQueue(100) - s.stopC = make(chan struct{}) + s.rtMaFast = types.NewQueue(1) + s.rtMaSlow = types.NewQueue(5) + s.rtMr = types.NewQueue(100) - go func() { - secondTicker := time.NewTicker(util.MillisecondsJitter(s.Interval.Duration(), 200)) - defer secondTicker.Stop() + s.rtWeight = types.NewQueue(100) - for { - select { - case <-secondTicker.C: - s.rebalancePosition(true) - case <-s.stopC: - log.Warnf("%s goroutine stopped, due to the stop signal", s.Symbol) - return + currentRoundTime := int64(0) + previousRoundTime := int64(0) - case <-ctx.Done(): - log.Warnf("%s goroutine stopped, due to the cancelled context", s.Symbol) - return - } - } - }() + currentTradePrice := 0. + previousTradePrice := 0. + + if !bbgo.IsBackTesting { s.session.MarketDataStream.OnBookTickerUpdate(func(bt types.BookTicker) { - s.lastPrice = bt.Buy.Add(bt.Sell).Div(fixedpoint.Two) + // quote order book price + s.obBuyPrice = atomic.NewFloat64(bt.Buy.Float64()) + s.obSellPrice = atomic.NewFloat64(bt.Sell.Float64()) }) - } else { - s.session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - s.lastPrice = kline.Close - s.rebalancePosition(false) + + s.session.MarketDataStream.OnAggTrade(func(trade types.Trade) { + // rounding to 1000 milliseconds if hftInterval is set to 1000 + currentRoundTime = trade.Time.UnixMilli() % int64(s.Interval) + currentTradePrice = trade.Price.Float64() + if currentRoundTime < previousRoundTime { + + s.openPrice = s.closePrice + // D0 strategy can use now data + // D1 strategy only use previous data (we're here) + s.closePrice = previousTradePrice + //log.Infof("Previous Close Price: %f", s.closePrice) + //log.Infof("Previous Open Price: %f", s.openPrice) + log.Infof("Now Open Price: %f", currentTradePrice) + s.orderExecutor.CancelNoWait(ctx) + // calculate real-time Negative Return + s.rtNr.Update(s.openPrice - s.closePrice) + // calculate real-time Negative Return Rank + rtNrRank := 0. + if s.rtNr.Length() >= 100 { + rtNrRank = s.rtNr.Rank(s.rtNr.Length()).Last() / float64(s.rtNr.Length()) + } + // calculate real-time Mean Reversion + s.rtMaFast.Update(s.closePrice) + s.rtMaSlow.Update(s.closePrice) + s.rtMr.Update((s.rtMaSlow.Mean() - s.rtMaFast.Mean()) / s.rtMaSlow.Mean()) + // calculate real-time Mean Reversion Rank + rtMrRank := 0. + if s.rtMr.Length() >= 100 { + rtMrRank = s.rtMr.Rank(s.rtMr.Length()).Last() / float64(s.rtMr.Length()) + } + s.rtWeight.Update((rtNrRank + rtMrRank) / 2) + rtWeightRank := 0. + if s.rtWeight.Length() >= 100 { + rtWeightRank = s.rtWeight.Rank(s.rtWeight.Length()).Last() / float64(s.rtWeight.Length()) + } + log.Infof("Alpha: %f/1.0", rtWeightRank) + s.rebalancePosition(s.obBuyPrice.Load(), s.obSellPrice.Load(), rtWeightRank) + } + + previousRoundTime = currentRoundTime + previousTradePrice = currentTradePrice }) } @@ -404,74 +451,45 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) } -func (s *Strategy) rebalancePosition(rt bool) { - - s.lastTwoPrices.Update(s.lastPrice.Float64()) - if s.lastTwoPrices.Length() >= 2 { - log.Infof("Interval Closed Price: %f", s.lastTwoPrices.Last()) - // main idea: negative return - nr := -1 * (s.lastTwoPrices.Last()/s.lastTwoPrices.Index(1) - 1) - if rt { - // hump operation to reduce noise - // update nirr indicator when above threshold - if math.Abs(s.rtNrr.Last()-nr) < s.HumpThreshold { - s.rtNrr.Update(s.rtNrr.Last()) - } else { - s.rtNrr.Update(nr) - return - } - } else { - if math.Abs(s.Nrr.Last()-s.Nrr.Index(1)) < s.HumpThreshold { - return - } - } +func (s *Strategy) rebalancePosition(bestBid, bestAsk float64, w float64) { + // alpha-weighted assets (inventory and capital) + position := s.orderExecutor.Position() + p := fixedpoint.NewFromFloat((bestBid + bestAsk) / 2) - // when have enough Nrr to do ts_rank() - if (s.rtNrr.Length() >= s.Window && rt) || (s.Nrr.Length() >= s.Window && !rt) { + targetBase := s.QuantityOrAmount.CalculateQuantity(p).Mul(fixedpoint.NewFromFloat(w)) - // alpha-weighted assets (inventory and capital) - position := s.orderExecutor.Position() - // weight: 0~1, since it's a long only strategy - weight := fixedpoint.NewFromFloat(s.rtNrr.Rank(s.Window).Last() / float64(s.Window)) - if !rt { - weight = fixedpoint.NewFromFloat(s.Nrr.Rank(s.Window).Last() / float64(s.Window)) - } - targetBase := s.QuantityOrAmount.CalculateQuantity(fixedpoint.NewFromFloat(s.lastTwoPrices.Mean(2))).Mul(weight) + // to buy/sell quantity + diffQty := targetBase.Sub(position.Base) + log.Infof("Target Position Diff: %f", diffQty.Float64()) - // to buy/sell quantity - diffQty := targetBase.Sub(position.Base) - log.Infof("Alpha: %f/1.0, Target Position Diff: %f", weight.Float64(), diffQty.Float64()) + // ignore small changes + if diffQty.Abs().Float64() < 0.0005 { + return + } - // ignore small changes - if diffQty.Abs().Float64() < 0.001 { - return - } - // re-balance position - if diffQty.Sign() > 0 { - _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Quantity: diffQty.Abs(), - Type: types.OrderTypeMarket, - //Price: bt.Sell, - Tag: "irr re-balance: buy", - }) - if err != nil { - log.WithError(err) - } - } else if diffQty.Sign() < 0 { - _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Quantity: diffQty.Abs(), - Type: types.OrderTypeMarket, - //Price: bt.Buy, - Tag: "irr re-balance: sell", - }) - if err != nil { - log.WithError(err) - } - } + if diffQty.Sign() > 0 { + _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Quantity: diffQty.Abs(), + Type: types.OrderTypeLimit, + Price: fixedpoint.NewFromFloat(bestBid), + Tag: "irr re-balance: buy", + }) + if err != nil { + log.WithError(err) + } + } else if diffQty.Sign() < 0 { + _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Quantity: diffQty.Abs(), + Type: types.OrderTypeLimit, + Price: fixedpoint.NewFromFloat(bestAsk), + Tag: "irr re-balance: sell", + }) + if err != nil { + log.WithError(err) } } } From 2b397940b80901529c1562ea5095e597b885dd4e Mon Sep 17 00:00:00 2001 From: austin362667 Date: Mon, 17 Oct 2022 22:13:22 +0800 Subject: [PATCH 0003/1392] strategy:irr fix draw goroutine --- pkg/strategy/irr/draw.go | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/pkg/strategy/irr/draw.go b/pkg/strategy/irr/draw.go index 98f99d818e..b4346b8150 100644 --- a/pkg/strategy/irr/draw.go +++ b/pkg/strategy/irr/draw.go @@ -16,36 +16,44 @@ func (s *Strategy) InitDrawCommands(profit, cumProfit, cumProfitDollar types.Ser canvas := DrawPNL(s.InstanceID(), profit) var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render return in irr") - reply.Message(fmt.Sprintf("[error] cannot render return in irr: %v", err)) + go func() { + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render return in irr") + reply.Message(fmt.Sprintf("[error] cannot render return in irr: %v", err)) + return + } + bbgo.SendPhoto(&buffer) return - } - go bbgo.SendPhoto(&buffer) + }() }) bbgo.RegisterCommand("/nav", "Draw Net Assets Value", func(reply interact.Reply) { canvas := DrawCumPNL(s.InstanceID(), cumProfit) var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render nav in irr") - reply.Message(fmt.Sprintf("[error] canot render nav in irr: %v", err)) + go func() { + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render nav in irr") + reply.Message(fmt.Sprintf("[error] canot render nav in irr: %v", err)) + return + } + bbgo.SendPhoto(&buffer) return - } - go bbgo.SendPhoto(&buffer) + }() }) bbgo.RegisterCommand("/pnl", "Draw Cumulative Profit & Loss", func(reply interact.Reply) { canvas := DrawCumPNL(s.InstanceID(), cumProfitDollar) var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render pnl in irr") - reply.Message(fmt.Sprintf("[error] canot render pnl in irr: %v", err)) + go func() { + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in irr") + reply.Message(fmt.Sprintf("[error] canot render pnl in irr: %v", err)) + return + } + bbgo.SendPhoto(&buffer) return - } - go bbgo.SendPhoto(&buffer) - + }() }) } From 58bdb9b194c967b1ccee83a44dcf521b999a9b87 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Mon, 17 Oct 2022 22:14:06 +0800 Subject: [PATCH 0004/1392] strategy:irr remove alpha ranking --- pkg/strategy/irr/strategy.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index f7b14c1741..164ac8256d 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -412,12 +412,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se rtMrRank = s.rtMr.Rank(s.rtMr.Length()).Last() / float64(s.rtMr.Length()) } s.rtWeight.Update((rtNrRank + rtMrRank) / 2) - rtWeightRank := 0. - if s.rtWeight.Length() >= 100 { - rtWeightRank = s.rtWeight.Rank(s.rtWeight.Length()).Last() / float64(s.rtWeight.Length()) - } - log.Infof("Alpha: %f/1.0", rtWeightRank) - s.rebalancePosition(s.obBuyPrice.Load(), s.obSellPrice.Load(), rtWeightRank) + //rtWeightRank := 0. + //if s.rtWeight.Length() >= 100 { + // rtWeightRank = s.rtWeight.Rank(s.rtWeight.Length()).Last() / float64(s.rtWeight.Length()) + //} + log.Infof("Alpha: %f/1.0", s.rtWeight.Last()) + s.rebalancePosition(s.obBuyPrice.Load(), s.obSellPrice.Load(), s.rtWeight.Last()) } previousRoundTime = currentRoundTime From 7974ee8fd31260fe8804844324c9c96c0a9057b5 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Mon, 17 Oct 2022 22:26:33 +0800 Subject: [PATCH 0005/1392] strategy:irr: seperate alphas --- config/irr.yaml | 4 ++++ pkg/strategy/irr/strategy.go | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/config/irr.yaml b/config/irr.yaml index 9e8fd56acb..4247efd75c 100644 --- a/config/irr.yaml +++ b/config/irr.yaml @@ -19,6 +19,10 @@ exchangeStrategies: hftInterval: 1000 # maxima position in USD amount: 500.0 + # alpha1: negative return reversion + NR: false + # alpha2: moving average reversion + MR: true # Draw pnl drawGraph: true graphPNLPath: "./pnl.png" diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index 164ac8256d..4b7648d4a5 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -48,7 +48,9 @@ type Strategy struct { bbgo.QuantityOrAmount - Interval int `json:"hftInterval"` + Interval int `json:"hftInterval"` + NR bool `json:"NR"` + MR bool `json:"MR"` // for back-test Nrr *NRR @@ -411,7 +413,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if s.rtMr.Length() >= 100 { rtMrRank = s.rtMr.Rank(s.rtMr.Length()).Last() / float64(s.rtMr.Length()) } - s.rtWeight.Update((rtNrRank + rtMrRank) / 2) + alpha := 0. + if s.NR && s.MR { + alpha = (rtNrRank + rtMrRank) / 2 + } else if s.NR && !s.MR { + alpha = rtNrRank + } else if !s.NR && s.MR { + alpha = rtMrRank + } + s.rtWeight.Update(alpha) //rtWeightRank := 0. //if s.rtWeight.Length() >= 100 { // rtWeightRank = s.rtWeight.Rank(s.rtWeight.Length()).Last() / float64(s.rtWeight.Length()) From 42d87adeecba0f3303e2a25310aaecc483e349a8 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Tue, 18 Oct 2022 02:29:31 +0800 Subject: [PATCH 0006/1392] strategy:irr: rollback to interval time ticker --- config/irr.yaml | 4 +- pkg/strategy/irr/strategy.go | 131 +++++++++++++++++++++-------------- 2 files changed, 80 insertions(+), 55 deletions(-) diff --git a/config/irr.yaml b/config/irr.yaml index 4247efd75c..a9f09128b4 100644 --- a/config/irr.yaml +++ b/config/irr.yaml @@ -16,11 +16,11 @@ exchangeStrategies: irr: symbol: BTCBUSD # in milliseconds(ms) - hftInterval: 1000 + hftInterval: 5 # maxima position in USD amount: 500.0 # alpha1: negative return reversion - NR: false + NR: true # alpha2: moving average reversion MR: true # Draw pnl diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index 4b7648d4a5..3d8f6c51f2 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -3,6 +3,10 @@ package irr import ( "context" "fmt" + "os" + "sync" + "time" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/data/tsv" "github.com/c9s/bbgo/pkg/datatype/floats" @@ -11,8 +15,6 @@ import ( "github.com/c9s/bbgo/pkg/types" "github.com/sirupsen/logrus" "go.uber.org/atomic" - "os" - "sync" ) const ID = "irr" @@ -58,6 +60,8 @@ type Strategy struct { // realtime book ticker to submit order obBuyPrice *atomic.Float64 obSellPrice *atomic.Float64 + // for getting close price + currentTradePrice *atomic.Float64 // for negative return rate openPrice float64 closePrice float64 @@ -369,11 +373,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.rtWeight = types.NewQueue(100) - currentRoundTime := int64(0) - previousRoundTime := int64(0) - - currentTradePrice := 0. - previousTradePrice := 0. + s.currentTradePrice = atomic.NewFloat64(0) if !bbgo.IsBackTesting { @@ -384,55 +384,80 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.session.MarketDataStream.OnAggTrade(func(trade types.Trade) { - // rounding to 1000 milliseconds if hftInterval is set to 1000 - currentRoundTime = trade.Time.UnixMilli() % int64(s.Interval) - currentTradePrice = trade.Price.Float64() - if currentRoundTime < previousRoundTime { - - s.openPrice = s.closePrice - // D0 strategy can use now data - // D1 strategy only use previous data (we're here) - s.closePrice = previousTradePrice - //log.Infof("Previous Close Price: %f", s.closePrice) - //log.Infof("Previous Open Price: %f", s.openPrice) - log.Infof("Now Open Price: %f", currentTradePrice) - s.orderExecutor.CancelNoWait(ctx) - // calculate real-time Negative Return - s.rtNr.Update(s.openPrice - s.closePrice) - // calculate real-time Negative Return Rank - rtNrRank := 0. - if s.rtNr.Length() >= 100 { - rtNrRank = s.rtNr.Rank(s.rtNr.Length()).Last() / float64(s.rtNr.Length()) - } - // calculate real-time Mean Reversion - s.rtMaFast.Update(s.closePrice) - s.rtMaSlow.Update(s.closePrice) - s.rtMr.Update((s.rtMaSlow.Mean() - s.rtMaFast.Mean()) / s.rtMaSlow.Mean()) - // calculate real-time Mean Reversion Rank - rtMrRank := 0. - if s.rtMr.Length() >= 100 { - rtMrRank = s.rtMr.Rank(s.rtMr.Length()).Last() / float64(s.rtMr.Length()) + s.currentTradePrice = atomic.NewFloat64(trade.Price.Float64()) + }) + + go func() { + intervalCloseTicker := time.NewTicker(time.Duration(s.Interval) * time.Millisecond) + defer intervalCloseTicker.Stop() + + for { + select { + case <-intervalCloseTicker.C: + if s.currentTradePrice.Load() > 0 { + s.closePrice = s.currentTradePrice.Load() + //log.Infof("Close Price: %f", s.closePrice) + // calculate real-time Negative Return + s.rtNr.Update((s.openPrice - s.closePrice) / s.openPrice) + // calculate real-time Negative Return Rank + rtNrRank := 0. + if s.rtNr.Length() > 2 { + rtNrRank = s.rtNr.Rank(s.rtNr.Length()).Last() / float64(s.rtNr.Length()) + } + // calculate real-time Mean Reversion + s.rtMaFast.Update(s.closePrice) + s.rtMaSlow.Update(s.closePrice) + s.rtMr.Update((s.rtMaSlow.Mean() - s.rtMaFast.Mean()) / s.rtMaSlow.Mean()) + // calculate real-time Mean Reversion Rank + rtMrRank := 0. + if s.rtMr.Length() > 2 { + rtMrRank = s.rtMr.Rank(s.rtMr.Length()).Last() / float64(s.rtMr.Length()) + } + alpha := 0. + if s.NR && s.MR { + alpha = (rtNrRank + rtMrRank) / 2 + } else if s.NR && !s.MR { + alpha = rtNrRank + } else if !s.NR && s.MR { + alpha = rtMrRank + } + s.rtWeight.Update(alpha) + log.Infof("Alpha: %f/1.0", s.rtWeight.Last()) + s.rebalancePosition(s.obBuyPrice.Load(), s.obSellPrice.Load(), s.rtWeight.Last()) + s.orderExecutor.CancelNoWait(context.Background()) + } + case <-s.stopC: + log.Warnf("%s goroutine stopped, due to the stop signal", s.Symbol) + return + + case <-ctx.Done(): + log.Warnf("%s goroutine stopped, due to the cancelled context", s.Symbol) + return } - alpha := 0. - if s.NR && s.MR { - alpha = (rtNrRank + rtMrRank) / 2 - } else if s.NR && !s.MR { - alpha = rtNrRank - } else if !s.NR && s.MR { - alpha = rtMrRank + } + }() + + go func() { + intervalOpenTicker := time.NewTicker(time.Duration(s.Interval) * time.Millisecond) + defer intervalOpenTicker.Stop() + for { + select { + case <-intervalOpenTicker.C: + time.Sleep(200 * time.Microsecond) + if s.currentTradePrice.Load() > 0 { + s.openPrice = s.currentTradePrice.Load() + //log.Infof("Open Price: %f", s.openPrice) + } + case <-s.stopC: + log.Warnf("%s goroutine stopped, due to the stop signal", s.Symbol) + return + + case <-ctx.Done(): + log.Warnf("%s goroutine stopped, due to the cancelled context", s.Symbol) + return } - s.rtWeight.Update(alpha) - //rtWeightRank := 0. - //if s.rtWeight.Length() >= 100 { - // rtWeightRank = s.rtWeight.Rank(s.rtWeight.Length()).Last() / float64(s.rtWeight.Length()) - //} - log.Infof("Alpha: %f/1.0", s.rtWeight.Last()) - s.rebalancePosition(s.obBuyPrice.Load(), s.obSellPrice.Load(), s.rtWeight.Last()) } - - previousRoundTime = currentRoundTime - previousTradePrice = currentTradePrice - }) + }() } bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { From 303e2c8413f9cd8bb37b89df60711cdf291ac2a7 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Tue, 18 Oct 2022 05:59:47 +0800 Subject: [PATCH 0007/1392] strategy:irr: redesign to maker strategy --- config/irr.yaml | 8 +++-- pkg/strategy/irr/draw.go | 42 +++++++++++--------------- pkg/strategy/irr/strategy.go | 58 ++++++++++++++---------------------- 3 files changed, 46 insertions(+), 62 deletions(-) diff --git a/config/irr.yaml b/config/irr.yaml index a9f09128b4..8fa9ac329e 100644 --- a/config/irr.yaml +++ b/config/irr.yaml @@ -17,8 +17,12 @@ exchangeStrategies: symbol: BTCBUSD # in milliseconds(ms) hftInterval: 5 - # maxima position in USD - amount: 500.0 + # indicator window + window: 100 + # limit maker order quantity + quantity: 0.01 + # bonus spread in USD + spread: 0.25 # alpha1: negative return reversion NR: true # alpha2: moving average reversion diff --git a/pkg/strategy/irr/draw.go b/pkg/strategy/irr/draw.go index b4346b8150..b038ec7026 100644 --- a/pkg/strategy/irr/draw.go +++ b/pkg/strategy/irr/draw.go @@ -16,44 +16,38 @@ func (s *Strategy) InitDrawCommands(profit, cumProfit, cumProfitDollar types.Ser canvas := DrawPNL(s.InstanceID(), profit) var buffer bytes.Buffer - go func() { - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render return in irr") - reply.Message(fmt.Sprintf("[error] cannot render return in irr: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render return in irr") + reply.Message(fmt.Sprintf("[error] cannot render return in irr: %v", err)) return - }() + } + bbgo.SendPhoto(&buffer) + return }) bbgo.RegisterCommand("/nav", "Draw Net Assets Value", func(reply interact.Reply) { canvas := DrawCumPNL(s.InstanceID(), cumProfit) var buffer bytes.Buffer - go func() { - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render nav in irr") - reply.Message(fmt.Sprintf("[error] canot render nav in irr: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render nav in irr") + reply.Message(fmt.Sprintf("[error] canot render nav in irr: %v", err)) return - }() + } + bbgo.SendPhoto(&buffer) + return }) bbgo.RegisterCommand("/pnl", "Draw Cumulative Profit & Loss", func(reply interact.Reply) { canvas := DrawCumPNL(s.InstanceID(), cumProfitDollar) var buffer bytes.Buffer - go func() { - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render pnl in irr") - reply.Message(fmt.Sprintf("[error] canot render pnl in irr: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in irr") + reply.Message(fmt.Sprintf("[error] canot render pnl in irr: %v", err)) return - }() + } + bbgo.SendPhoto(&buffer) + return }) } diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index 3d8f6c51f2..68ac52961d 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -49,6 +49,7 @@ type Strategy struct { orderExecutor *bbgo.GeneralOrderExecutor bbgo.QuantityOrAmount + Spread float64 `json:"spread"` Interval int `json:"hftInterval"` NR bool `json:"NR"` @@ -365,13 +366,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // s.Ma.LoadK((*klines)[0:]) //} - s.rtNr = types.NewQueue(100) + s.rtNr = types.NewQueue(s.Window) s.rtMaFast = types.NewQueue(1) s.rtMaSlow = types.NewQueue(5) - s.rtMr = types.NewQueue(100) + s.rtMr = types.NewQueue(s.Window) - s.rtWeight = types.NewQueue(100) + s.rtWeight = types.NewQueue(s.Window) s.currentTradePrice = atomic.NewFloat64(0) @@ -395,6 +396,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se select { case <-intervalCloseTicker.C: if s.currentTradePrice.Load() > 0 { + s.orderExecutor.CancelNoWait(context.Background()) s.closePrice = s.currentTradePrice.Load() //log.Infof("Close Price: %f", s.closePrice) // calculate real-time Negative Return @@ -424,7 +426,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.rtWeight.Update(alpha) log.Infof("Alpha: %f/1.0", s.rtWeight.Last()) s.rebalancePosition(s.obBuyPrice.Load(), s.obSellPrice.Load(), s.rtWeight.Last()) - s.orderExecutor.CancelNoWait(context.Background()) } case <-s.stopC: log.Warnf("%s goroutine stopped, due to the stop signal", s.Symbol) @@ -443,7 +444,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se for { select { case <-intervalOpenTicker.C: - time.Sleep(200 * time.Microsecond) + time.Sleep(50 * time.Microsecond) if s.currentTradePrice.Load() > 0 { s.openPrice = s.currentTradePrice.Load() //log.Infof("Open Price: %f", s.openPrice) @@ -487,44 +488,29 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { } func (s *Strategy) rebalancePosition(bestBid, bestAsk float64, w float64) { - // alpha-weighted assets (inventory and capital) - position := s.orderExecutor.Position() - p := fixedpoint.NewFromFloat((bestBid + bestAsk) / 2) - - targetBase := s.QuantityOrAmount.CalculateQuantity(p).Mul(fixedpoint.NewFromFloat(w)) - - // to buy/sell quantity - diffQty := targetBase.Sub(position.Base) - log.Infof("Target Position Diff: %f", diffQty.Float64()) - - // ignore small changes - if diffQty.Abs().Float64() < 0.0005 { - return - } - - if diffQty.Sign() > 0 { - _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ + if w < 0.5 { + _, errB := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, - Quantity: diffQty.Abs(), - Type: types.OrderTypeLimit, - Price: fixedpoint.NewFromFloat(bestBid), - Tag: "irr re-balance: buy", + Quantity: s.Quantity, + Type: types.OrderTypeLimitMaker, + Price: fixedpoint.NewFromFloat(bestBid - s.Spread), + Tag: "irr short: buy", }) - if err != nil { - log.WithError(err) + if errB != nil { + log.WithError(errB) } - } else if diffQty.Sign() < 0 { - _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ + } else if w > 0.5 { + _, errA := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, - Quantity: diffQty.Abs(), - Type: types.OrderTypeLimit, - Price: fixedpoint.NewFromFloat(bestAsk), - Tag: "irr re-balance: sell", + Quantity: s.Quantity, + Type: types.OrderTypeLimitMaker, + Price: fixedpoint.NewFromFloat(bestAsk + s.Spread), + Tag: "irr long: buy", }) - if err != nil { - log.WithError(err) + if errA != nil { + log.WithError(errA) } } } From 612261c48c0c8df7306d19ab56b4a3a28aac68c0 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Wed, 19 Oct 2022 16:02:00 +0800 Subject: [PATCH 0008/1392] strategy:irr add klines box mean reversion --- config/irr.yaml | 19 +++-- pkg/strategy/irr/draw.go | 4 - pkg/strategy/irr/strategy.go | 145 ++++++++++++++++++++--------------- 3 files changed, 96 insertions(+), 72 deletions(-) diff --git a/config/irr.yaml b/config/irr.yaml index 8fa9ac329e..eb899c093d 100644 --- a/config/irr.yaml +++ b/config/irr.yaml @@ -10,19 +10,26 @@ sessions: binance: exchange: binance envVarPrefix: binance + max: + exchange: max + envVarPrefix: max + ftx: + exchange: ftx + envVarPrefix: ftx exchangeStrategies: - on: binance irr: symbol: BTCBUSD # in milliseconds(ms) - hftInterval: 5 + hftInterval: 1000 # indicator window - window: 100 - # limit maker order quantity - quantity: 0.01 - # bonus spread in USD - spread: 0.25 + window: 0 + # maxima position in USD + amount: 100 + quantity: 0.001 + # minProfit pips in USD + pips: 0.0 # alpha1: negative return reversion NR: true # alpha2: moving average reversion diff --git a/pkg/strategy/irr/draw.go b/pkg/strategy/irr/draw.go index b038ec7026..fdf4ea9e29 100644 --- a/pkg/strategy/irr/draw.go +++ b/pkg/strategy/irr/draw.go @@ -22,7 +22,6 @@ func (s *Strategy) InitDrawCommands(profit, cumProfit, cumProfitDollar types.Ser return } bbgo.SendPhoto(&buffer) - return }) bbgo.RegisterCommand("/nav", "Draw Net Assets Value", func(reply interact.Reply) { @@ -34,8 +33,6 @@ func (s *Strategy) InitDrawCommands(profit, cumProfit, cumProfitDollar types.Ser return } bbgo.SendPhoto(&buffer) - return - }) bbgo.RegisterCommand("/pnl", "Draw Cumulative Profit & Loss", func(reply interact.Reply) { @@ -47,7 +44,6 @@ func (s *Strategy) InitDrawCommands(profit, cumProfit, cumProfitDollar types.Ser return } bbgo.SendPhoto(&buffer) - return }) } diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index 68ac52961d..c75c9e29a9 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -21,7 +21,6 @@ const ID = "irr" var one = fixedpoint.One var zero = fixedpoint.Zero -var Fee = 0.000 // taker fee % * 2, for upper bound var log = logrus.WithField("strategy", ID) @@ -49,7 +48,7 @@ type Strategy struct { orderExecutor *bbgo.GeneralOrderExecutor bbgo.QuantityOrAmount - Spread float64 `json:"spread"` + MinProfit float64 `json:"pips"` Interval int `json:"hftInterval"` NR bool `json:"NR"` @@ -61,8 +60,12 @@ type Strategy struct { // realtime book ticker to submit order obBuyPrice *atomic.Float64 obSellPrice *atomic.Float64 + // for posting LO + canBuy bool + canSell bool // for getting close price currentTradePrice *atomic.Float64 + tradePriceSeries *types.Queue // for negative return rate openPrice float64 closePrice float64 @@ -343,6 +346,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.highestPrice = 0 s.lowestPrice = s.sellPrice } + + if trade.Side == types.SideTypeBuy { + s.canSell = true + s.canBuy = false + } else if trade.Side == types.SideTypeSell { + s.canBuy = true + s.canSell = false + } }) s.InitDrawCommands(&profitSlice, &cumProfitSlice, &cumProfitDollarSlice) @@ -374,7 +385,20 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.rtWeight = types.NewQueue(s.Window) - s.currentTradePrice = atomic.NewFloat64(0) + s.currentTradePrice = atomic.NewFloat64(0.) + s.tradePriceSeries = types.NewQueue(2) + + // adverse selection based on order flow dynamics + s.canBuy = true + s.canSell = true + + s.closePrice = 0. // atomic.NewFloat64(0) + s.openPrice = 0. //atomic.NewFloat64(0) + klinDirections := types.NewQueue(100) + started := false + boxOpenPrice := 0. + boxClosePrice := 0. + boxCounter := 0 if !bbgo.IsBackTesting { @@ -389,43 +413,67 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) go func() { + time.Sleep(time.Duration(s.Interval-int(time.Now().UnixMilli())%s.Interval) * time.Millisecond) + intervalCloseTicker := time.NewTicker(time.Duration(s.Interval) * time.Millisecond) defer intervalCloseTicker.Stop() for { select { case <-intervalCloseTicker.C: + + s.orderExecutor.CancelNoWait(context.Background()) + if s.currentTradePrice.Load() > 0 { - s.orderExecutor.CancelNoWait(context.Background()) s.closePrice = s.currentTradePrice.Load() - //log.Infof("Close Price: %f", s.closePrice) - // calculate real-time Negative Return - s.rtNr.Update((s.openPrice - s.closePrice) / s.openPrice) - // calculate real-time Negative Return Rank - rtNrRank := 0. - if s.rtNr.Length() > 2 { - rtNrRank = s.rtNr.Rank(s.rtNr.Length()).Last() / float64(s.rtNr.Length()) - } - // calculate real-time Mean Reversion - s.rtMaFast.Update(s.closePrice) - s.rtMaSlow.Update(s.closePrice) - s.rtMr.Update((s.rtMaSlow.Mean() - s.rtMaFast.Mean()) / s.rtMaSlow.Mean()) - // calculate real-time Mean Reversion Rank - rtMrRank := 0. - if s.rtMr.Length() > 2 { - rtMrRank = s.rtMr.Rank(s.rtMr.Length()).Last() / float64(s.rtMr.Length()) - } - alpha := 0. - if s.NR && s.MR { - alpha = (rtNrRank + rtMrRank) / 2 - } else if s.NR && !s.MR { - alpha = rtNrRank - } else if !s.NR && s.MR { - alpha = rtMrRank + log.Infof("Close Price: %f", s.closePrice) + if s.closePrice > 0 && s.openPrice > 0 { + direction := s.closePrice - s.openPrice + klinDirections.Update(direction) + regimeShift := klinDirections.Index(0)*klinDirections.Index(1) < 0 + if regimeShift && !started { + boxOpenPrice = s.openPrice + started = true + boxCounter = 0 + log.Infof("box started at price: %f", boxOpenPrice) + } else if regimeShift && started { + boxClosePrice = s.openPrice + started = false + log.Infof("box ended at price: %f with time length: %d", boxClosePrice, boxCounter) + // box ending, should re-balance position + nirr := fixedpoint.NewFromFloat(((boxOpenPrice - boxClosePrice) / boxOpenPrice) / (float64(boxCounter) + 1)) + qty := s.QuantityOrAmount.CalculateQuantity(fixedpoint.Value(boxClosePrice)) + qty = qty.Mul(nirr.Abs().Div(fixedpoint.NewFromInt(1000))) + log.Infof("Alpha: %f with Diff Qty: %f", nirr.Float64(), qty.Float64()) + if nirr.Float64() < 0 { + _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Quantity: s.Quantity, + Type: types.OrderTypeLimitMaker, + Price: fixedpoint.NewFromFloat(s.obSellPrice.Load()), + Tag: "irr re-balance: sell", + }) + if err != nil { + log.WithError(err) + } + } else if nirr.Float64() > 0 { + _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Quantity: s.Quantity, + Type: types.OrderTypeLimitMaker, + Price: fixedpoint.NewFromFloat(s.obBuyPrice.Load()), + Tag: "irr re-balance: buy", + }) + if err != nil { + log.WithError(err) + } + } + } else { + boxCounter++ + } } - s.rtWeight.Update(alpha) - log.Infof("Alpha: %f/1.0", s.rtWeight.Last()) - s.rebalancePosition(s.obBuyPrice.Load(), s.obSellPrice.Load(), s.rtWeight.Last()) } case <-s.stopC: log.Warnf("%s goroutine stopped, due to the stop signal", s.Symbol) @@ -439,15 +487,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }() go func() { + time.Sleep(time.Duration(s.Interval-int(time.Now().UnixMilli())%s.Interval) * time.Millisecond) intervalOpenTicker := time.NewTicker(time.Duration(s.Interval) * time.Millisecond) defer intervalOpenTicker.Stop() for { select { case <-intervalOpenTicker.C: - time.Sleep(50 * time.Microsecond) - if s.currentTradePrice.Load() > 0 { + time.Sleep(time.Duration(s.Interval/10) * time.Millisecond) + if s.currentTradePrice.Load() > 0 && s.closePrice > 0 { s.openPrice = s.currentTradePrice.Load() - //log.Infof("Open Price: %f", s.openPrice) + log.Infof("Open Price: %f", s.openPrice) } case <-s.stopC: log.Warnf("%s goroutine stopped, due to the stop signal", s.Symbol) @@ -486,31 +535,3 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { balances := s.session.GetAccount().Balances() return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) } - -func (s *Strategy) rebalancePosition(bestBid, bestAsk float64, w float64) { - if w < 0.5 { - _, errB := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Quantity: s.Quantity, - Type: types.OrderTypeLimitMaker, - Price: fixedpoint.NewFromFloat(bestBid - s.Spread), - Tag: "irr short: buy", - }) - if errB != nil { - log.WithError(errB) - } - } else if w > 0.5 { - _, errA := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Quantity: s.Quantity, - Type: types.OrderTypeLimitMaker, - Price: fixedpoint.NewFromFloat(bestAsk + s.Spread), - Tag: "irr long: buy", - }) - if errA != nil { - log.WithError(errA) - } - } -} From 614209e9fd4f3837b578bc2c6bc5fe5a8a6a98fa Mon Sep 17 00:00:00 2001 From: austin362667 Date: Wed, 19 Oct 2022 17:10:33 +0800 Subject: [PATCH 0009/1392] strategy:irr fix kline time syncing --- pkg/strategy/irr/strategy.go | 104 +++++++++++------------------------ 1 file changed, 32 insertions(+), 72 deletions(-) diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index c75c9e29a9..8eb0104408 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "sync" + "sync/atomic" "time" "github.com/c9s/bbgo/pkg/bbgo" @@ -14,7 +15,6 @@ import ( "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" "github.com/sirupsen/logrus" - "go.uber.org/atomic" ) const ID = "irr" @@ -58,26 +58,15 @@ type Strategy struct { Nrr *NRR Ma *indicator.SMA // realtime book ticker to submit order - obBuyPrice *atomic.Float64 - obSellPrice *atomic.Float64 - // for posting LO - canBuy bool - canSell bool + obBuyPrice uint64 + obSellPrice uint64 // for getting close price - currentTradePrice *atomic.Float64 - tradePriceSeries *types.Queue + currentTradePrice uint64 // for negative return rate openPrice float64 closePrice float64 - rtNr *types.Queue - // for moving average reversion - rtMaFast *types.Queue - rtMaSlow *types.Queue - rtMr *types.Queue - // for final alpha (Nr+Mr)/2 - rtWeight *types.Queue - stopC chan struct{} + stopC chan struct{} // StrategyController bbgo.StrategyController @@ -346,14 +335,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.highestPrice = 0 s.lowestPrice = s.sellPrice } - - if trade.Side == types.SideTypeBuy { - s.canSell = true - s.canBuy = false - } else if trade.Side == types.SideTypeSell { - s.canBuy = true - s.canSell = false - } }) s.InitDrawCommands(&profitSlice, &cumProfitSlice, &cumProfitDollarSlice) @@ -364,36 +345,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.Bind() s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - //back-test only, because 1s delayed a lot - //kLineStore, _ := s.session.MarketDataStore(s.Symbol) - //s.Nrr = &NRR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}, RankingWindow: s.Window} - //s.Nrr.BindK(s.session.MarketDataStream, s.Symbol, s.Interval) - //if klines, ok := kLineStore.KLinesOfInterval(s.Nrr.Interval); ok { - // s.Nrr.LoadK((*klines)[0:]) - //} - //s.Ma = &indicator.SMA{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}} - //s.Ma.BindK(s.session.MarketDataStream, s.Symbol, s.Interval) - //if klines, ok := kLineStore.KLinesOfInterval(s.Ma.Interval); ok { - // s.Ma.LoadK((*klines)[0:]) - //} - - s.rtNr = types.NewQueue(s.Window) - - s.rtMaFast = types.NewQueue(1) - s.rtMaSlow = types.NewQueue(5) - s.rtMr = types.NewQueue(s.Window) - - s.rtWeight = types.NewQueue(s.Window) - - s.currentTradePrice = atomic.NewFloat64(0.) - s.tradePriceSeries = types.NewQueue(2) - - // adverse selection based on order flow dynamics - s.canBuy = true - s.canSell = true - - s.closePrice = 0. // atomic.NewFloat64(0) - s.openPrice = 0. //atomic.NewFloat64(0) + atomic.SwapUint64(&s.currentTradePrice, 0.) + s.closePrice = 0. + s.openPrice = 0. klinDirections := types.NewQueue(100) started := false boxOpenPrice := 0. @@ -404,40 +358,43 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.session.MarketDataStream.OnBookTickerUpdate(func(bt types.BookTicker) { // quote order book price - s.obBuyPrice = atomic.NewFloat64(bt.Buy.Float64()) - s.obSellPrice = atomic.NewFloat64(bt.Sell.Float64()) + newBid := uint64(bt.Buy.Float64()) + newAsk := uint64(bt.Sell.Float64()) + atomic.SwapUint64(&s.obBuyPrice, newBid) + atomic.SwapUint64(&s.obSellPrice, newAsk) }) s.session.MarketDataStream.OnAggTrade(func(trade types.Trade) { - s.currentTradePrice = atomic.NewFloat64(trade.Price.Float64()) + tradePrice := uint64(trade.Price.Float64()) + atomic.SwapUint64(&s.currentTradePrice, tradePrice) }) + closeTime := <-time.After(time.Duration(s.Interval-int(time.Now().UnixMilli())%s.Interval) * time.Millisecond) + log.Infof("kline close timing synced @ %s", closeTime.Format("2006-01-02 15:04:05.000000")) go func() { - time.Sleep(time.Duration(s.Interval-int(time.Now().UnixMilli())%s.Interval) * time.Millisecond) - intervalCloseTicker := time.NewTicker(time.Duration(s.Interval) * time.Millisecond) defer intervalCloseTicker.Stop() - for { select { case <-intervalCloseTicker.C: + log.Infof("kline close time @ %s", time.Now().Format("2006-01-02 15:04:05.000000")) s.orderExecutor.CancelNoWait(context.Background()) - if s.currentTradePrice.Load() > 0 { - s.closePrice = s.currentTradePrice.Load() - log.Infof("Close Price: %f", s.closePrice) + if s.currentTradePrice > 0 { + s.closePrice = float64(s.currentTradePrice) + log.Infof("Close Price: %f", float64(s.closePrice)) if s.closePrice > 0 && s.openPrice > 0 { direction := s.closePrice - s.openPrice - klinDirections.Update(direction) + klinDirections.Update(float64(direction)) regimeShift := klinDirections.Index(0)*klinDirections.Index(1) < 0 if regimeShift && !started { - boxOpenPrice = s.openPrice + boxOpenPrice = float64(s.openPrice) started = true boxCounter = 0 log.Infof("box started at price: %f", boxOpenPrice) } else if regimeShift && started { - boxClosePrice = s.openPrice + boxClosePrice = float64(s.openPrice) started = false log.Infof("box ended at price: %f with time length: %d", boxClosePrice, boxCounter) // box ending, should re-balance position @@ -451,7 +408,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Side: types.SideTypeSell, Quantity: s.Quantity, Type: types.OrderTypeLimitMaker, - Price: fixedpoint.NewFromFloat(s.obSellPrice.Load()), + Price: fixedpoint.NewFromFloat(float64(s.obSellPrice)), Tag: "irr re-balance: sell", }) if err != nil { @@ -463,7 +420,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Side: types.SideTypeBuy, Quantity: s.Quantity, Type: types.OrderTypeLimitMaker, - Price: fixedpoint.NewFromFloat(s.obBuyPrice.Load()), + Price: fixedpoint.NewFromFloat(float64(s.obBuyPrice)), Tag: "irr re-balance: buy", }) if err != nil { @@ -484,18 +441,22 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } } + }() + openTime := <-time.After(time.Duration(s.Interval-int(time.Now().UnixMilli())%s.Interval) * time.Millisecond) + log.Infof("kline open timing synced @ %s", openTime.Format("2006-01-02 15:04:05.000000")) go func() { - time.Sleep(time.Duration(s.Interval-int(time.Now().UnixMilli())%s.Interval) * time.Millisecond) intervalOpenTicker := time.NewTicker(time.Duration(s.Interval) * time.Millisecond) defer intervalOpenTicker.Stop() for { select { case <-intervalOpenTicker.C: time.Sleep(time.Duration(s.Interval/10) * time.Millisecond) - if s.currentTradePrice.Load() > 0 && s.closePrice > 0 { - s.openPrice = s.currentTradePrice.Load() + log.Infof("kline open time @ %s", time.Now().Format("2006-01-02 15:04:05.000000")) + + if s.currentTradePrice > 0 && s.closePrice > 0 { + s.openPrice = float64(s.currentTradePrice) log.Infof("Open Price: %f", s.openPrice) } case <-s.stopC: @@ -527,7 +488,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) _ = s.orderExecutor.GracefulCancel(ctx) }) - return nil } From 778a3d8be100143b5e838e391c5b30dcc6b20621 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Wed, 19 Oct 2022 17:24:27 +0800 Subject: [PATCH 0010/1392] strategy:irr: clean up strategy:irr: clean up strategy:irr: clean up strategy:irr: clean up --- config/irr.yaml | 12 ++---------- pkg/strategy/irr/strategy.go | 18 +++++------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/config/irr.yaml b/config/irr.yaml index eb899c093d..302980d289 100644 --- a/config/irr.yaml +++ b/config/irr.yaml @@ -22,18 +22,10 @@ exchangeStrategies: irr: symbol: BTCBUSD # in milliseconds(ms) + # must > 10 ms hftInterval: 1000 - # indicator window - window: 0 - # maxima position in USD - amount: 100 + # qty per trade quantity: 0.001 - # minProfit pips in USD - pips: 0.0 - # alpha1: negative return reversion - NR: true - # alpha2: moving average reversion - MR: true # Draw pnl drawGraph: true graphPNLPath: "./pnl.png" diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index 8eb0104408..dbdf30c821 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -48,15 +48,9 @@ type Strategy struct { orderExecutor *bbgo.GeneralOrderExecutor bbgo.QuantityOrAmount - MinProfit float64 `json:"pips"` - Interval int `json:"hftInterval"` - NR bool `json:"NR"` - MR bool `json:"MR"` + Interval int `json:"hftInterval"` - // for back-test - Nrr *NRR - Ma *indicator.SMA // realtime book ticker to submit order obBuyPrice uint64 obSellPrice uint64 @@ -399,9 +393,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Infof("box ended at price: %f with time length: %d", boxClosePrice, boxCounter) // box ending, should re-balance position nirr := fixedpoint.NewFromFloat(((boxOpenPrice - boxClosePrice) / boxOpenPrice) / (float64(boxCounter) + 1)) - qty := s.QuantityOrAmount.CalculateQuantity(fixedpoint.Value(boxClosePrice)) - qty = qty.Mul(nirr.Abs().Div(fixedpoint.NewFromInt(1000))) - log.Infof("Alpha: %f with Diff Qty: %f", nirr.Float64(), qty.Float64()) + log.Infof("Alpha: %f", nirr.Float64()) if nirr.Float64() < 0 { _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ Symbol: s.Symbol, @@ -409,7 +401,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Quantity: s.Quantity, Type: types.OrderTypeLimitMaker, Price: fixedpoint.NewFromFloat(float64(s.obSellPrice)), - Tag: "irr re-balance: sell", + Tag: "irrSell", }) if err != nil { log.WithError(err) @@ -421,7 +413,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Quantity: s.Quantity, Type: types.OrderTypeLimitMaker, Price: fixedpoint.NewFromFloat(float64(s.obBuyPrice)), - Tag: "irr re-balance: buy", + Tag: "irrBuy", }) if err != nil { log.WithError(err) @@ -452,7 +444,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se for { select { case <-intervalOpenTicker.C: - time.Sleep(time.Duration(s.Interval/10) * time.Millisecond) + time.Sleep(10 * time.Millisecond) log.Infof("kline open time @ %s", time.Now().Format("2006-01-02 15:04:05.000000")) if s.currentTradePrice > 0 && s.closePrice > 0 { From 6e29359c858147ed317eb60627e1031a9b64a80c Mon Sep 17 00:00:00 2001 From: austin362667 Date: Wed, 19 Oct 2022 22:08:44 +0800 Subject: [PATCH 0011/1392] strategy:irr: fix logical error --- pkg/strategy/irr/strategy.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index dbdf30c821..a155eb3f8a 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -377,18 +377,18 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if s.currentTradePrice > 0 { s.closePrice = float64(s.currentTradePrice) - log.Infof("Close Price: %f", float64(s.closePrice)) + log.Infof("Close Price: %f", s.closePrice) if s.closePrice > 0 && s.openPrice > 0 { direction := s.closePrice - s.openPrice - klinDirections.Update(float64(direction)) + klinDirections.Update(direction) regimeShift := klinDirections.Index(0)*klinDirections.Index(1) < 0 if regimeShift && !started { - boxOpenPrice = float64(s.openPrice) + boxOpenPrice = s.openPrice started = true boxCounter = 0 log.Infof("box started at price: %f", boxOpenPrice) } else if regimeShift && started { - boxClosePrice = float64(s.openPrice) + boxClosePrice = s.closePrice started = false log.Infof("box ended at price: %f with time length: %d", boxClosePrice, boxCounter) // box ending, should re-balance position From 7de997533653ce766fe2db768a8b7220a4931c05 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 21 Oct 2022 16:14:47 +0800 Subject: [PATCH 0012/1392] indicator/linreg: LinReg indicator --- pkg/indicator/linreg.go | 98 +++++++++++++++++++++++++++++++ pkg/indicator/linreg_callbacks.go | 15 +++++ 2 files changed, 113 insertions(+) create mode 100644 pkg/indicator/linreg.go create mode 100644 pkg/indicator/linreg_callbacks.go diff --git a/pkg/indicator/linreg.go b/pkg/indicator/linreg.go new file mode 100644 index 0000000000..e48c82a2cb --- /dev/null +++ b/pkg/indicator/linreg.go @@ -0,0 +1,98 @@ +package indicator + +import ( + "github.com/sirupsen/logrus" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +var log = logrus.WithField("indicator", "supertrend") + +// LinReg is Linear Regression baseline +//go:generate callbackgen -type LinReg +type LinReg struct { + types.SeriesBase + types.IntervalWindow + + // Values are the slopes of linear regression baseline + Values floats.Slice + klines types.KLineWindow + + EndTime time.Time + UpdateCallbacks []func(value float64) +} + +// Last slope of linear regression baseline +func (lr *LinReg) Last() float64 { + if lr.Values.Length() == 0 { + return 0.0 + } + return lr.Values.Last() +} + +// Index returns the slope of specified index +func (lr *LinReg) Index(i int) float64 { + if i >= lr.Values.Length() { + return 0.0 + } + + return lr.Values.Index(i) +} + +// Length of the slope values +func (lr *LinReg) Length() int { + return lr.Values.Length() +} + +var _ types.SeriesExtend = &LinReg{} + +// Update Linear Regression baseline slope +func (lr *LinReg) Update(kline types.KLine) { + lr.klines.Add(kline) + lr.klines.Truncate(lr.Window) + if len(lr.klines) < lr.Window { + lr.Values.Push(0) + return + } + + var sumX, sumY, sumXSqr, sumXY float64 = 0, 0, 0, 0 + end := len(lr.klines) - 1 // The last kline + for i := end; i >= end-lr.Window+1; i-- { + val := lr.klines[i].GetClose().Float64() + per := float64(end - i + 1) + sumX += per + sumY += val + sumXSqr += per * per + sumXY += val * per + } + length := float64(lr.Window) + slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX) + average := sumY / length + endPrice := average - slope*sumX/length + slope + startPrice := endPrice + slope*(length-1) + lr.Values.Push((endPrice - startPrice) / (length - 1)) + + log.Debugf("linear regression baseline slope: %f", lr.Last()) +} + +func (lr *LinReg) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, lr.PushK)) +} + +func (lr *LinReg) PushK(k types.KLine) { + var zeroTime = time.Time{} + if lr.EndTime != zeroTime && k.EndTime.Before(lr.EndTime) { + return + } + + lr.Update(k) + lr.EndTime = k.EndTime.Time() +} + +func (lr *LinReg) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + lr.PushK(k) + } +} diff --git a/pkg/indicator/linreg_callbacks.go b/pkg/indicator/linreg_callbacks.go new file mode 100644 index 0000000000..c719c8f514 --- /dev/null +++ b/pkg/indicator/linreg_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type LinReg"; DO NOT EDIT. + +package indicator + +import () + +func (lr *LinReg) OnUpdate(cb func(value float64)) { + lr.UpdateCallbacks = append(lr.UpdateCallbacks, cb) +} + +func (lr *LinReg) EmitUpdate(value float64) { + for _, cb := range lr.UpdateCallbacks { + cb(value) + } +} From df05cf65d231c8a7d2b105965455311b80419e02 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 21 Oct 2022 16:15:55 +0800 Subject: [PATCH 0013/1392] feature/dynamicSpread: dynamicSpread as a common package --- pkg/dynamicmetric/dynamic_spread.go | 275 ++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 pkg/dynamicmetric/dynamic_spread.go diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/dynamicmetric/dynamic_spread.go new file mode 100644 index 0000000000..2f4d663602 --- /dev/null +++ b/pkg/dynamicmetric/dynamic_spread.go @@ -0,0 +1,275 @@ +package dynamicmetric + +import ( + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "math" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type DynamicSpread struct { + // AmpSpread calculates spreads based on kline amplitude + AmpSpread *DynamicSpreadAmp `json:"amplitude"` + + // WeightedBollWidthRatioSpread calculates spreads based on two Bollinger Bands + WeightedBollWidthRatioSpread *DynamicSpreadBollWidthRatio `json:"weightedBollWidth"` + + // deprecated + Enabled *bool `json:"enabled"` + + // deprecated + types.IntervalWindow + + // deprecated. AskSpreadScale is used to define the ask spread range with the given percentage. + AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` + + // deprecated. BidSpreadScale is used to define the bid spread range with the given percentage. + BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` +} + +// Initialize dynamic spread +func (ds *DynamicSpread) Initialize(symbol string, session *bbgo.ExchangeSession) { + switch { + case ds.AmpSpread != nil: + ds.AmpSpread.initialize(symbol, session) + case ds.WeightedBollWidthRatioSpread != nil: + ds.WeightedBollWidthRatioSpread.initialize(symbol, session) + case ds.Enabled != nil && *ds.Enabled: + // backward compatibility + ds.AmpSpread = &DynamicSpreadAmp{ + IntervalWindow: ds.IntervalWindow, + AskSpreadScale: ds.AskSpreadScale, + BidSpreadScale: ds.BidSpreadScale, + } + ds.AmpSpread.initialize(symbol, session) + } +} + +func (ds *DynamicSpread) IsEnabled() bool { + return ds.AmpSpread != nil || ds.WeightedBollWidthRatioSpread != nil +} + +// GetAskSpread returns current ask spread +func (ds *DynamicSpread) GetAskSpread() (askSpread float64, err error) { + switch { + case ds.AmpSpread != nil: + return ds.AmpSpread.getAskSpread() + case ds.WeightedBollWidthRatioSpread != nil: + return ds.WeightedBollWidthRatioSpread.getAskSpread() + default: + return 0, errors.New("dynamic spread is not enabled") + } +} + +// GetBidSpread returns current dynamic bid spread +func (ds *DynamicSpread) GetBidSpread() (bidSpread float64, err error) { + switch { + case ds.AmpSpread != nil: + return ds.AmpSpread.getBidSpread() + case ds.WeightedBollWidthRatioSpread != nil: + return ds.WeightedBollWidthRatioSpread.getBidSpread() + default: + return 0, errors.New("dynamic spread is not enabled") + } +} + +// DynamicSpreadAmp uses kline amplitude to calculate spreads +type DynamicSpreadAmp struct { + types.IntervalWindow + + // AskSpreadScale is used to define the ask spread range with the given percentage. + AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` + + // BidSpreadScale is used to define the bid spread range with the given percentage. + BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` + + dynamicAskSpread *indicator.SMA + dynamicBidSpread *indicator.SMA +} + +// initialize amplitude dynamic spread and preload SMAs +func (ds *DynamicSpreadAmp) initialize(symbol string, session *bbgo.ExchangeSession) { + ds.dynamicBidSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}} + ds.dynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}} + + // Subscribe kline + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ + Interval: ds.Interval, + }) + + // Update on kline closed + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, ds.Interval, func(kline types.KLine) { + ds.update(kline) + })) + + // Preload + kLineStore, _ := session.MarketDataStore(symbol) + if klines, ok := kLineStore.KLinesOfInterval(ds.Interval); ok { + for i := 0; i < len(*klines); i++ { + ds.update((*klines)[i]) + } + } +} + +// update amplitude dynamic spread with kline +func (ds *DynamicSpreadAmp) update(kline types.KLine) { + // ampl is the amplitude of kline + ampl := (kline.GetHigh().Float64() - kline.GetLow().Float64()) / kline.GetOpen().Float64() + + switch kline.Direction() { + case types.DirectionUp: + ds.dynamicAskSpread.Update(ampl) + ds.dynamicBidSpread.Update(0) + case types.DirectionDown: + ds.dynamicBidSpread.Update(ampl) + ds.dynamicAskSpread.Update(0) + default: + ds.dynamicAskSpread.Update(0) + ds.dynamicBidSpread.Update(0) + } +} + +func (ds *DynamicSpreadAmp) getAskSpread() (askSpread float64, err error) { + if ds.AskSpreadScale != nil && ds.dynamicAskSpread.Length() >= ds.Window { + askSpread, err = ds.AskSpreadScale.Scale(ds.dynamicAskSpread.Last()) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicAskSpread") + return 0, err + } + + return askSpread, nil + } + + return 0, errors.New("incomplete dynamic spread settings or not enough data yet") +} + +func (ds *DynamicSpreadAmp) getBidSpread() (bidSpread float64, err error) { + if ds.BidSpreadScale != nil && ds.dynamicBidSpread.Length() >= ds.Window { + bidSpread, err = ds.BidSpreadScale.Scale(ds.dynamicBidSpread.Last()) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicBidSpread") + return 0, err + } + + return bidSpread, nil + } + + return 0, errors.New("incomplete dynamic spread settings or not enough data yet") +} + +// BollingerSetting is for Bollinger Band settings +type BollingerSetting struct { + types.IntervalWindow + BandWidth float64 `json:"bandWidth"` +} + +type DynamicSpreadBollWidthRatio struct { + // AskSpreadScale is used to define the ask spread range with the given percentage. + AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` + + // BidSpreadScale is used to define the bid spread range with the given percentage. + BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` + + // Sensitivity factor of the weighting function: 1 / (1 + exp(-(x - mid) * sensitivity / width)) + // A positive number. The greater factor, the sharper weighting function. Default set to 1.0 . + Sensitivity float64 `json:"sensitivity"` + + DefaultBollinger *BollingerSetting `json:"defaultBollinger"` + NeutralBollinger *BollingerSetting `json:"neutralBollinger"` + + StandardIndicatorSet *bbgo.StandardIndicatorSet + + neutralBoll *indicator.BOLL + defaultBoll *indicator.BOLL +} + +func (ds *DynamicSpreadBollWidthRatio) initialize(symbol string, session *bbgo.ExchangeSession) { + ds.neutralBoll = ds.StandardIndicatorSet.BOLL(ds.NeutralBollinger.IntervalWindow, ds.NeutralBollinger.BandWidth) + ds.defaultBoll = ds.StandardIndicatorSet.BOLL(ds.DefaultBollinger.IntervalWindow, ds.DefaultBollinger.BandWidth) + + // Subscribe kline + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ + Interval: ds.NeutralBollinger.Interval, + }) + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ + Interval: ds.DefaultBollinger.Interval, + }) + + if ds.Sensitivity <= 0. { + ds.Sensitivity = 1. + } +} + +func (ds *DynamicSpreadBollWidthRatio) getAskSpread() (askSpread float64, err error) { + askSpread, err = ds.AskSpreadScale.Scale(ds.getWeightedBBWidthRatio(true)) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicAskSpread") + return 0, err + } + + return askSpread, nil +} + +func (ds *DynamicSpreadBollWidthRatio) getBidSpread() (bidSpread float64, err error) { + bidSpread, err = ds.BidSpreadScale.Scale(ds.getWeightedBBWidthRatio(false)) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicAskSpread") + return 0, err + } + + return bidSpread, nil +} + +func (ds *DynamicSpreadBollWidthRatio) getWeightedBBWidthRatio(positiveSigmoid bool) float64 { + // Weight the width of Boll bands with sigmoid function and calculate the ratio after integral. + // + // Given the default band: moving average default_BB_mid, band from default_BB_lower to default_BB_upper. + // And the neutral band: from neutral_BB_lower to neutral_BB_upper. + // And a sensitivity factor alpha, which is a positive constant. + // + // width of default BB w = default_BB_upper - default_BB_lower + // + // 1 x - default_BB_mid + // sigmoid weighting function f(y) = ------------- where y = -------------------- + // 1 + exp(-y) w / alpha + // Set the sigmoid weighting function: + // - To ask spread, the weighting density function d_weight(x) is sigmoid((x - default_BB_mid) / (w / alpha)) + // - To bid spread, the weighting density function d_weight(x) is sigmoid((default_BB_mid - x) / (w / alpha)) + // - The higher sensitivity factor alpha, the sharper weighting function. + // + // Then calculate the weighted band width ratio by taking integral of d_weight(x) from neutral_BB_lower to neutral_BB_upper: + // infinite integral of ask spread sigmoid weighting density function F(x) = (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha))) + // infinite integral of bid spread sigmoid weighting density function F(x) = x - (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha))) + // Note that we've rescaled the sigmoid function to fit default BB, + // the weighted default BB width is always calculated by integral(f of x from default_BB_lower to default_BB_upper) + // F(neutral_BB_upper) - F(neutral_BB_lower) + // weighted ratio = ------------------------------------------- + // F(default_BB_upper) - F(default_BB_lower) + // - The wider neutral band get greater ratio + // - To ask spread, the higher neutral band get greater ratio + // - To bid spread, the lower neutral band get greater ratio + + defaultMid := ds.defaultBoll.SMA.Last() + defaultUpper := ds.defaultBoll.UpBand.Last() + defaultLower := ds.defaultBoll.DownBand.Last() + defaultWidth := defaultUpper - defaultLower + neutralUpper := ds.neutralBoll.UpBand.Last() + neutralLower := ds.neutralBoll.DownBand.Last() + factor := defaultWidth / ds.Sensitivity + var weightedUpper, weightedLower, weightedDivUpper, weightedDivLower float64 + if positiveSigmoid { + weightedUpper = factor * math.Log(math.Exp(neutralUpper/factor)+math.Exp(defaultMid/factor)) + weightedLower = factor * math.Log(math.Exp(neutralLower/factor)+math.Exp(defaultMid/factor)) + weightedDivUpper = factor * math.Log(math.Exp(defaultUpper/factor)+math.Exp(defaultMid/factor)) + weightedDivLower = factor * math.Log(math.Exp(defaultLower/factor)+math.Exp(defaultMid/factor)) + } else { + weightedUpper = neutralUpper - factor*math.Log(math.Exp(neutralUpper/factor)+math.Exp(defaultMid/factor)) + weightedLower = neutralLower - factor*math.Log(math.Exp(neutralLower/factor)+math.Exp(defaultMid/factor)) + weightedDivUpper = defaultUpper - factor*math.Log(math.Exp(defaultUpper/factor)+math.Exp(defaultMid/factor)) + weightedDivLower = defaultLower - factor*math.Log(math.Exp(defaultLower/factor)+math.Exp(defaultMid/factor)) + } + return (weightedUpper - weightedLower) / (weightedDivUpper - weightedDivLower) +} From faee87d2adbd70394cab7f186304cf5b588bf9c6 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 21 Oct 2022 17:20:31 +0800 Subject: [PATCH 0014/1392] feature/dynamicExposure: dynamicExposure as a common package --- pkg/dynamicmetric/bollsetting.go | 9 +++ pkg/dynamicmetric/dynamic_exposure.go | 83 +++++++++++++++++++++++++++ pkg/dynamicmetric/dynamic_spread.go | 6 -- 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 pkg/dynamicmetric/bollsetting.go create mode 100644 pkg/dynamicmetric/dynamic_exposure.go diff --git a/pkg/dynamicmetric/bollsetting.go b/pkg/dynamicmetric/bollsetting.go new file mode 100644 index 0000000000..79bff4ef1e --- /dev/null +++ b/pkg/dynamicmetric/bollsetting.go @@ -0,0 +1,9 @@ +package dynamicmetric + +import "github.com/c9s/bbgo/pkg/types" + +// BollingerSetting is for Bollinger Band settings +type BollingerSetting struct { + types.IntervalWindow + BandWidth float64 `json:"bandWidth"` +} diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go new file mode 100644 index 0000000000..aadd1ece3f --- /dev/null +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -0,0 +1,83 @@ +package dynamicmetric + +import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "math" +) + +type DynamicExposure struct { + // BollBandExposure calculates the max exposure with the Bollinger Band + BollBandExposure *DynamicExposureBollBand `json:"bollBandExposure"` +} + +// Initialize dynamic exposure +func (d *DynamicExposure) Initialize(symbol string, session *bbgo.ExchangeSession) { + switch { + case d.BollBandExposure != nil: + d.BollBandExposure.initialize(symbol, session) + } +} + +func (d *DynamicExposure) IsEnabled() bool { + return d.BollBandExposure != nil +} + +// GetMaxExposure returns the max exposure +func (d *DynamicExposure) GetMaxExposure(price float64) (maxExposure fixedpoint.Value, err error) { + switch { + case d.BollBandExposure != nil: + return d.BollBandExposure.getMaxExposure(price) + default: + return fixedpoint.Zero, errors.New("dynamic exposure is not enabled") + } +} + +// DynamicExposureBollBand calculates the max exposure with the Bollinger Band +type DynamicExposureBollBand struct { + // DynamicExposureBollBandScale is used to define the exposure range with the given percentage. + DynamicExposureBollBandScale *bbgo.PercentageScale `json:"dynamicExposurePositionScale"` + + *BollingerSetting + + StandardIndicatorSet *bbgo.StandardIndicatorSet + + dynamicExposureBollBand *indicator.BOLL +} + +// Initialize DynamicExposureBollBand +func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession) { + d.dynamicExposureBollBand = d.StandardIndicatorSet.BOLL(d.IntervalWindow, d.BandWidth) + + // Subscribe kline + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ + Interval: d.dynamicExposureBollBand.Interval, + }) +} + +// getMaxExposure returns the max exposure +func (d *DynamicExposureBollBand) getMaxExposure(price float64) (fixedpoint.Value, error) { + downBand := d.dynamicExposureBollBand.DownBand.Last() + upBand := d.dynamicExposureBollBand.UpBand.Last() + sma := d.dynamicExposureBollBand.SMA.Last() + log.Infof("dynamicExposureBollBand bollinger band: up %f sma %f down %f", upBand, sma, downBand) + + bandPercentage := 0.0 + if price < sma { + // should be negative percentage + bandPercentage = (price - sma) / math.Abs(sma-downBand) + } else if price > sma { + // should be positive percentage + bandPercentage = (price - sma) / math.Abs(upBand-sma) + } + + v, err := d.DynamicExposureBollBandScale.Scale(bandPercentage) + if err != nil { + return fixedpoint.Zero, err + } + return fixedpoint.NewFromFloat(v), nil +} diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/dynamicmetric/dynamic_spread.go index 2f4d663602..a1e5e631c8 100644 --- a/pkg/dynamicmetric/dynamic_spread.go +++ b/pkg/dynamicmetric/dynamic_spread.go @@ -160,12 +160,6 @@ func (ds *DynamicSpreadAmp) getBidSpread() (bidSpread float64, err error) { return 0, errors.New("incomplete dynamic spread settings or not enough data yet") } -// BollingerSetting is for Bollinger Band settings -type BollingerSetting struct { - types.IntervalWindow - BandWidth float64 `json:"bandWidth"` -} - type DynamicSpreadBollWidthRatio struct { // AskSpreadScale is used to define the ask spread range with the given percentage. AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` From 675f84dccfa2aa7617d36f5d88e7ff55ac111a97 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 19 Oct 2022 13:17:44 +0900 Subject: [PATCH 0015/1392] fix: SerialMarketDataStore together with backtests --- config/driftBTC.yaml | 8 +- config/elliottwave.yaml | 2 + pkg/bbgo/marketdatastore.go | 2 +- pkg/bbgo/serialmarketdatastore.go | 114 ++++++++++++++++++++++++--- pkg/bbgo/session.go | 32 ++++++-- pkg/bbgo/trader.go | 30 +++++-- pkg/strategy/drift/strategy.go | 92 +++++++++++++-------- pkg/strategy/elliottwave/strategy.go | 53 +++++++------ pkg/types/interval.go | 36 ++++++++- 9 files changed, 282 insertions(+), 87 deletions(-) diff --git a/config/driftBTC.yaml b/config/driftBTC.yaml index a05c110a29..ffe9733675 100644 --- a/config/driftBTC.yaml +++ b/config/driftBTC.yaml @@ -26,12 +26,13 @@ exchangeStrategies: - on: binance drift: + minInterval: 1s limitOrder: true #quantity: 0.0012 canvasPath: "./output.png" symbol: BTCUSDT # kline interval for indicators - interval: 1m + interval: 1s window: 6 useAtr: true useStopLoss: true @@ -125,11 +126,12 @@ sync: - BTCUSDT backtest: - startTime: "2022-09-25" - endTime: "2022-10-30" + startTime: "2022-10-18" + endTime: "2022-10-19" symbols: - BTCUSDT sessions: [binance] + syncSecKLines: true accounts: binance: makerFeeRate: 0.000 diff --git a/config/elliottwave.yaml b/config/elliottwave.yaml index d3d138f4cb..1d57a20dd1 100644 --- a/config/elliottwave.yaml +++ b/config/elliottwave.yaml @@ -23,6 +23,7 @@ exchangeStrategies: - on: binance elliottwave: + minInterval: 1s symbol: BNBBUSD limitOrder: true quantity: 0.16 @@ -115,6 +116,7 @@ backtest: symbols: - BNBBUSD sessions: [binance] + syncSecKLines: true accounts: binance: makerFeeRate: 0.000 diff --git a/pkg/bbgo/marketdatastore.go b/pkg/bbgo/marketdatastore.go index 11f06902ee..dd9eaf0992 100644 --- a/pkg/bbgo/marketdatastore.go +++ b/pkg/bbgo/marketdatastore.go @@ -22,7 +22,7 @@ func NewMarketDataStore(symbol string) *MarketDataStore { Symbol: symbol, // KLineWindows stores all loaded klines per interval - KLineWindows: make(map[types.Interval]*types.KLineWindow, len(types.SupportedIntervals)), // 12 interval, 1m,5m,15m,30m,1h,2h,4h,6h,12h,1d,3d,1w + KLineWindows: make(map[types.Interval]*types.KLineWindow, len(types.SupportedIntervals)), // 13 interval, 1s,1m,5m,15m,30m,1h,2h,4h,6h,12h,1d,3d,1w } } diff --git a/pkg/bbgo/serialmarketdatastore.go b/pkg/bbgo/serialmarketdatastore.go index 3bcf6b645f..f4b6bf01e0 100644 --- a/pkg/bbgo/serialmarketdatastore.go +++ b/pkg/bbgo/serialmarketdatastore.go @@ -1,22 +1,34 @@ package bbgo import ( + "context" + "sync" "time" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) type SerialMarketDataStore struct { *MarketDataStore - KLines map[types.Interval]*types.KLine - Subscription []types.Interval + UseAggTrade bool + KLines map[types.Interval]*types.KLine + MinInterval types.Interval + Subscription []types.Interval + o, h, l, c, v, qv, price fixedpoint.Value + lock sync.Mutex } -func NewSerialMarketDataStore(symbol string) *SerialMarketDataStore { +// @param symbol: symbol to trace on +// @param minInterval: unit interval, related to your signal timeframe +// @param useAggTrade: if not assigned, default to false. if assigned to true, will use AggTrade signal to generate klines +func NewSerialMarketDataStore(symbol string, minInterval types.Interval, useAggTrade ...bool) *SerialMarketDataStore { return &SerialMarketDataStore{ MarketDataStore: NewMarketDataStore(symbol), KLines: make(map[types.Interval]*types.KLine), + UseAggTrade: len(useAggTrade) > 0 && useAggTrade[0], Subscription: []types.Interval{}, + MinInterval: minInterval, } } @@ -30,24 +42,98 @@ func (store *SerialMarketDataStore) Subscribe(interval types.Interval) { store.Subscription = append(store.Subscription, interval) } -func (store *SerialMarketDataStore) BindStream(stream types.Stream) { - stream.OnKLineClosed(store.handleKLineClosed) +func (store *SerialMarketDataStore) BindStream(ctx context.Context, stream types.Stream) { + if store.UseAggTrade { + go store.tickerProcessor(ctx) + stream.OnAggTrade(store.handleMarketTrade) + } else { + stream.OnKLineClosed(store.handleKLineClosed) + } } func (store *SerialMarketDataStore) handleKLineClosed(kline types.KLine) { store.AddKLine(kline) } -func (store *SerialMarketDataStore) AddKLine(kline types.KLine) { +func (store *SerialMarketDataStore) handleMarketTrade(trade types.Trade) { + store.lock.Lock() + store.price = trade.Price + store.c = store.price + if store.price.Compare(store.h) > 0 { + store.h = store.price + } + if !store.l.IsZero() { + if store.price.Compare(store.l) < 0 { + store.l = store.price + } + } else { + store.l = store.price + } + if store.o.IsZero() { + store.o = store.price + } + store.v.Add(trade.Quantity) + store.qv.Add(trade.QuoteQuantity) + store.lock.Unlock() +} + +func (store *SerialMarketDataStore) tickerProcessor(ctx context.Context) { + duration := store.MinInterval.Duration() + intervalCloseTicker := time.NewTicker(duration) + defer intervalCloseTicker.Stop() + + for { + select { + case time := <-intervalCloseTicker.C: + kline := types.KLine{ + Symbol: store.Symbol, + StartTime: types.Time(time.Add(-1 * duration)), + EndTime: types.Time(time), + Interval: store.MinInterval, + Closed: true, + } + store.lock.Lock() + if store.c.IsZero() { + kline.Open = store.price + kline.Close = store.price + kline.High = store.price + kline.Low = store.price + kline.Volume = fixedpoint.Zero + kline.QuoteVolume = fixedpoint.Zero + } else { + kline.Open = store.o + kline.Close = store.c + kline.High = store.h + kline.Low = store.l + kline.Volume = store.v + kline.QuoteVolume = store.qv + store.o = fixedpoint.Zero + store.c = fixedpoint.Zero + store.h = fixedpoint.Zero + store.l = fixedpoint.Zero + store.v = fixedpoint.Zero + store.qv = fixedpoint.Zero + } + store.lock.Unlock() + store.AddKLine(kline, true) + case <-ctx.Done(): + return + } + } + +} + +func (store *SerialMarketDataStore) AddKLine(kline types.KLine, async ...bool) { if kline.Symbol != store.Symbol { return } - // only consumes kline1m - if kline.Interval != types.Interval1m { + // only consumes MinInterval + if kline.Interval != store.MinInterval { return } - // endtime in minutes - timestamp := kline.StartTime.Time().Add(time.Minute) + // endtime + duration := store.MinInterval.Duration() + timestamp := kline.StartTime.Time().Round(duration).Add(duration) for _, val := range store.Subscription { k, ok := store.KLines[val] if !ok { @@ -60,9 +146,13 @@ func (store *SerialMarketDataStore) AddKLine(kline types.KLine) { k.Merge(&kline) k.Closed = false } - if timestamp.Truncate(val.Duration()) == timestamp { + if timestamp.Round(val.Duration()) == timestamp { k.Closed = true - store.MarketDataStore.AddKLine(*k) + if len(async) > 0 && async[0] { + go store.MarketDataStore.AddKLine(*k) + } else { + store.MarketDataStore.AddKLine(*k) + } delete(store.KLines, val) } } diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index e62ac6a608..7457f9a0c6 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -395,8 +395,7 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ // used kline intervals by the given symbol var klineSubscriptions = map[types.Interval]struct{}{} - // always subscribe the 1m kline so we can make sure the connection persists. - klineSubscriptions[types.Interval1m] = struct{}{} + minInterval := types.Interval1m // Aggregate the intervals that we are using in the subscriptions. for _, sub := range session.Subscriptions { @@ -411,12 +410,20 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ continue } + if minInterval.Seconds() > sub.Options.Interval.Seconds() { + minInterval = sub.Options.Interval + } + if sub.Symbol == symbol { klineSubscriptions[types.Interval(sub.Options.Interval)] = struct{}{} } } } + // always subscribe the 1m kline so we can make sure the connection persists. + klineSubscriptions[minInterval] = struct{}{} + log.Warnf("sub: %v", klineSubscriptions) + for interval := range klineSubscriptions { // avoid querying the last unclosed kline endTime := environ.startTime @@ -440,10 +447,12 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ // update last prices by the given kline lastKLine := kLines[len(kLines)-1] - if interval == types.Interval1m { + if interval == minInterval { session.lastPrices[symbol] = lastKLine.Close } + log.Warnf("load %s", interval) + for _, k := range kLines { // let market data store trigger the update, so that the indicator could be updated too. marketDataStore.AddKLine(k) @@ -497,6 +506,7 @@ func (session *ExchangeSession) Positions() map[string]*types.Position { // MarketDataStore returns the market data store of a symbol func (session *ExchangeSession) MarketDataStore(symbol string) (s *MarketDataStore, ok bool) { s, ok = session.marketDataStores[symbol] + // FIXME: the returned MarketDataStore when !ok will be empty if !ok { s = NewMarketDataStore(symbol) s.BindStream(session.MarketDataStream) @@ -507,15 +517,21 @@ func (session *ExchangeSession) MarketDataStore(symbol string) (s *MarketDataSto } // KLine updates will be received in the order listend in intervals array -func (session *ExchangeSession) SerialMarketDataStore(symbol string, intervals []types.Interval) (store *SerialMarketDataStore, ok bool) { +func (session *ExchangeSession) SerialMarketDataStore(ctx context.Context, symbol string, intervals []types.Interval, useAggTrade ...bool) (store *SerialMarketDataStore, ok bool) { st, ok := session.MarketDataStore(symbol) if !ok { return nil, false } - store = NewSerialMarketDataStore(symbol) - klines, ok := st.KLinesOfInterval(types.Interval1m) + minInterval := types.Interval1m + for _, i := range intervals { + if minInterval.Seconds() > i.Seconds() { + minInterval = i + } + } + store = NewSerialMarketDataStore(symbol, minInterval, useAggTrade...) + klines, ok := st.KLinesOfInterval(minInterval) if !ok { - log.Errorf("SerialMarketDataStore: cannot get 1m history") + log.Errorf("SerialMarketDataStore: cannot get %s history", minInterval) return nil, false } for _, interval := range intervals { @@ -524,7 +540,7 @@ func (session *ExchangeSession) SerialMarketDataStore(symbol string, intervals [ for _, kline := range *klines { store.AddKLine(kline) } - store.BindStream(session.MarketDataStream) + store.BindStream(ctx, session.MarketDataStream) return store, true } diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index b6813909b0..b80e15dc94 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -262,7 +262,7 @@ func (trader *Trader) RunAllSingleExchangeStrategy(ctx context.Context) error { return nil } -func (trader *Trader) injectFields() error { +func (trader *Trader) injectFields(ctx context.Context) error { // load and run Session strategies for sessionName, strategies := range trader.exchangeStrategies { var session = trader.environment.sessions[sessionName] @@ -285,8 +285,30 @@ func (trader *Trader) injectFields() error { return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy) } + if defaulter, ok := strategy.(StrategyDefaulter); ok { + if err := defaulter.Defaults(); err != nil { + panic(err) + } + } + + if initializer, ok := strategy.(StrategyInitializer); ok { + if err := initializer.Initialize(); err != nil { + panic(err) + } + } + + if subscriber, ok := strategy.(ExchangeSessionSubscriber); ok { + subscriber.Subscribe(session) + } else { + log.Errorf("strategy %s does not implement ExchangeSessionSubscriber", strategy.ID()) + } + if symbol, ok := dynamic.LookupSymbolField(rs); ok { - log.Infof("found symbol based strategy from %s", rs.Type()) + log.Infof("found symbol %s based strategy from %s", symbol, rs.Type()) + + if err := session.initSymbol(ctx, trader.environment, symbol); err != nil { + return errors.Wrapf(err, "failed to inject object into %T when initSymbol", strategy) + } market, ok := session.Market(symbol) if !ok { @@ -339,12 +361,10 @@ func (trader *Trader) Run(ctx context.Context) error { // trader.environment.Connect will call interact.Start interact.AddCustomInteraction(NewCoreInteraction(trader.environment, trader)) - if err := trader.injectFields(); err != nil { + if err := trader.injectFields(ctx); err != nil { return err } - trader.Subscribe() - if err := trader.environment.Start(ctx); err != nil { return err } diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 5c8f010473..1113e94332 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -65,7 +65,8 @@ type Strategy struct { *types.ProfitStats `persistence:"profit_stats"` *types.TradeStats `persistence:"trade_stats"` - p *types.Position + p *types.Position + MinInterval types.Interval `json:"MinInterval"` priceLines *types.Queue trendLine types.UpdatableSeriesExtend @@ -78,10 +79,10 @@ type Strategy struct { lock sync.RWMutex `ignore:"true"` positionLock sync.RWMutex `ignore:"true"` startTime time.Time - minutesCounter int + counter int orderPendingCounter map[uint64]int frameKLine *types.KLine - kline1m *types.KLine + klineMin *types.KLine beta float64 @@ -135,15 +136,33 @@ func (s *Strategy) InstanceID() string { func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // by default, bbgo only pre-subscribe 1000 klines. // this is not enough if we're subscribing 30m intervals using SerialMarketDataStore - maxWindow := (s.Window + s.SmootherWindow + s.FisherTransformWindow) * s.Interval.Minutes() - bbgo.KLinePreloadLimit = int64((maxWindow/1000 + 1) * 1000) - log.Errorf("set kLinePreloadLimit to %d, %d %d", bbgo.KLinePreloadLimit, s.Interval.Minutes(), maxWindow) - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: types.Interval1m, - }) if !bbgo.IsBackTesting { session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) + session.Subscribe(types.AggTradeChannel, s.Symbol, types.SubscribeOptions{}) + // able to preload + if s.MinInterval.Milliseconds() >= types.Interval1s.Milliseconds() && s.MinInterval.Milliseconds()%types.Interval1s.Milliseconds() == 0 { + maxWindow := (s.Window + s.SmootherWindow + s.FisherTransformWindow) * (s.Interval.Milliseconds() / s.MinInterval.Milliseconds()) + bbgo.KLinePreloadLimit = int64((maxWindow/1000 + 1) * 1000) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.MinInterval, + }) + } else { + bbgo.KLinePreloadLimit = 0 + } + } else { + maxWindow := (s.Window + s.SmootherWindow + s.FisherTransformWindow) * (s.Interval.Milliseconds() / s.MinInterval.Milliseconds()) + bbgo.KLinePreloadLimit = int64((maxWindow/1000 + 1) * 1000) + // gave up preload + if s.Interval.Milliseconds() < s.MinInterval.Milliseconds() { + bbgo.KLinePreloadLimit = 0 + } + log.Errorf("set kLinePreloadLimit to %d, %d %d", bbgo.KLinePreloadLimit, s.Interval.Milliseconds()/s.MinInterval.Milliseconds(), maxWindow) + if bbgo.KLinePreloadLimit > 0 { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.MinInterval, + }) + } } s.ExitMethods.SetAndSubscribe(session, s) } @@ -208,6 +227,9 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.ATRWindow}} s.trendLine = &indicator.EWMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.TrendWindow}} + if bbgo.KLinePreloadLimit == 0 { + return nil + } klines, ok := store.KLinesOfInterval(s.Interval) klinesLength := len(*klines) if !ok || klinesLength == 0 { @@ -229,16 +251,15 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { if s.frameKLine != nil && klines != nil { s.frameKLine.Set(&(*klines)[len(*klines)-1]) } - klines, ok = store.KLinesOfInterval(types.Interval1m) + klines, ok = store.KLinesOfInterval(s.MinInterval) klinesLength = len(*klines) if !ok || klinesLength == 0 { return errors.New("klines not exists") } - log.Infof("loaded %d klines1m", klinesLength) - if s.kline1m != nil && klines != nil { - s.kline1m.Set(&(*klines)[len(*klines)-1]) + log.Infof("loaded %d klines%s", klinesLength, s.MinInterval) + if s.klineMin != nil && klines != nil { + s.klineMin.Set(&(*klines)[len(*klines)-1]) } - s.startTime = s.kline1m.StartTime.Time().Add(s.kline1m.Interval.Duration()) return nil } @@ -254,8 +275,8 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e if order.Status != types.OrderStatusNew && order.Status != types.OrderStatusPartiallyFilled { continue } - log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.minutesCounter) - if s.minutesCounter-s.orderPendingCounter[order.OrderID] > s.PendingMinutes { + log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.counter) + if s.counter-s.orderPendingCounter[order.OrderID] > s.PendingMinutes { toCancel = true } else if order.Side == types.SideTypeBuy { // 75% of the probability @@ -272,7 +293,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e } } if toCancel { - err := s.GeneralOrderExecutor.GracefulCancel(ctx) + err := s.GeneralOrderExecutor.CancelNoWait(ctx) // TODO: clean orderPendingCounter on cancel/trade for _, order := range nonTraded { delete(s.orderPendingCounter, order.OrderID) @@ -545,8 +566,8 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) } -func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) { - s.kline1m.Set(&kline) +func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) { + s.klineMin.Set(&kline) if s.Status != types.StrategyStatusRunning { return } @@ -667,7 +688,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { if exitCondition { s.positionLock.Unlock() - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + if err := s.GeneralOrderExecutor.CancelNoWait(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") return } @@ -680,7 +701,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { } if longCondition { - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + if err := s.GeneralOrderExecutor.CancelNoWait(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") s.positionLock.Unlock() return @@ -712,12 +733,12 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { } log.Infof("orders %v", createdOrders) if createdOrders != nil { - s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + s.orderPendingCounter[createdOrders[0].OrderID] = s.counter } return } if shortCondition { - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + if err := s.GeneralOrderExecutor.CancelNoWait(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") s.positionLock.Unlock() return @@ -750,7 +771,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { } log.Infof("orders %v", createdOrders) if createdOrders != nil { - s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + s.orderPendingCounter[createdOrders[0].OrderID] = s.counter } return } @@ -800,7 +821,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.GeneralOrderExecutor.Bind() s.orderPendingCounter = make(map[uint64]int) - s.minutesCounter = 0 + s.counter = 0 // Exit methods from config for _, method := range s.ExitMethods { @@ -862,13 +883,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.frameKLine = &types.KLine{} - s.kline1m = &types.KLine{} + s.klineMin = &types.KLine{} s.priceLines = types.NewQueue(300) s.initTickerFunctions(ctx) - startTime := s.Environment.StartTime() - s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime)) - s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime)) + s.startTime = s.Environment.StartTime() + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, s.startTime)) + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, s.startTime)) // default value: use 1m kline if !s.NoTrailingStopLoss && s.IsBackTesting() || s.TrailingStopLossType == "" { @@ -933,21 +954,22 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.RegisterModifier(s) - // event trigger order: s.Interval => Interval1m - store, ok := session.SerialMarketDataStore(s.Symbol, []types.Interval{s.Interval, types.Interval1m}) + // event trigger order: s.Interval => s.MinInterval + store, ok := session.SerialMarketDataStore(ctx, s.Symbol, []types.Interval{s.Interval, s.MinInterval}, !bbgo.IsBackTesting) if !ok { - panic("cannot get 1m history") + panic("cannot get " + s.MinInterval + " history") } if err := s.initIndicators(store); err != nil { log.WithError(err).Errorf("initIndicator failed") return nil } + store.OnKLineClosed(func(kline types.KLine) { - s.minutesCounter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Minutes()) + s.counter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) if kline.Interval == s.Interval { s.klineHandler(ctx, kline) - } else if kline.Interval == types.Interval1m { - s.klineHandler1m(ctx, kline) + } else if kline.Interval == s.MinInterval { + s.klineHandlerMin(ctx, kline) } }) diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go index 4f7998799b..7128affcc1 100644 --- a/pkg/strategy/elliottwave/strategy.go +++ b/pkg/strategy/elliottwave/strategy.go @@ -44,6 +44,7 @@ type Strategy struct { Session *bbgo.ExchangeSession Interval types.Interval `json:"interval"` + MinInterval types.Interval `json:"minInterval"` Stoploss fixedpoint.Value `json:"stoploss" modifiable:"true"` WindowATR int `json:"windowATR"` WindowQuick int `json:"windowQuick"` @@ -74,7 +75,7 @@ type Strategy struct { // for smart cancel orderPendingCounter map[uint64]int startTime time.Time - minutesCounter int + counter int // for position buyPrice float64 `persistence:"buy_price"` @@ -101,12 +102,22 @@ func (s *Strategy) InstanceID() string { func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // by default, bbgo only pre-subscribe 1000 klines. // this is not enough if we're subscribing 30m intervals using SerialMarketDataStore - bbgo.KLinePreloadLimit = int64((s.Interval.Minutes()*s.WindowSlow/1000 + 1) + 1000) - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: types.Interval1m, - }) if !bbgo.IsBackTesting { session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) + session.Subscribe(types.AggTradeChannel, s.Symbol, types.SubscribeOptions{}) + if s.MinInterval.Milliseconds() >= types.Interval1s.Milliseconds() && s.MinInterval.Milliseconds()%types.Interval1s.Milliseconds() == 0 { + bbgo.KLinePreloadLimit = int64(((s.Interval.Milliseconds()/s.MinInterval.Milliseconds())*s.WindowSlow/1000 + 1) + 1000) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.MinInterval, + }) + } else { + bbgo.KLinePreloadLimit = 0 + } + } else { + bbgo.KLinePreloadLimit = int64((s.Interval.Milliseconds()/s.MinInterval.Milliseconds()*s.WindowSlow/1000 + 1) + 1000) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.MinInterval, + }) } s.ExitMethods.SetAndSubscribe(session, s) } @@ -162,8 +173,6 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { if !ok || klineLength == 0 { return errors.New("klines not exists") } - tmpK := (*klines)[klineLength-1] - s.startTime = tmpK.StartTime.Time().Add(tmpK.Interval.Duration()) s.heikinAshi = NewHeikinAshi(500) for _, kline := range *klines { @@ -185,9 +194,9 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int { if order.Status != types.OrderStatusNew && order.Status != types.OrderStatusPartiallyFilled { continue } - log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.minutesCounter) + log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.counter) toCancel := false - if s.minutesCounter-s.orderPendingCounter[order.OrderID] >= s.PendingMinutes { + if s.counter-s.orderPendingCounter[order.OrderID] >= s.PendingMinutes { toCancel = true } else if order.Side == types.SideTypeBuy { if order.Price.Float64()+s.atr.Last()*2 <= pricef { @@ -315,7 +324,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.GeneralOrderExecutor.Bind() s.orderPendingCounter = make(map[uint64]int) - s.minutesCounter = 0 + s.counter = 0 for _, method := range s.ExitMethods { method.Bind(session, s.GeneralOrderExecutor) @@ -355,16 +364,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.initTickerFunctions() - startTime := s.Environment.StartTime() - s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime)) - s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime)) + s.startTime = s.Environment.StartTime() + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, s.startTime)) + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, s.startTime)) s.initOutputCommands() - // event trigger order: s.Interval => Interval1m - store, ok := session.SerialMarketDataStore(s.Symbol, []types.Interval{s.Interval, types.Interval1m}) + // event trigger order: s.Interval => minInterval + store, ok := session.SerialMarketDataStore(ctx, s.Symbol, []types.Interval{s.Interval, s.MinInterval}, !bbgo.IsBackTesting) if !ok { - panic("cannot get 1m history") + panic("cannot get " + s.MinInterval + " history") } if err := s.initIndicators(store); err != nil { log.WithError(err).Errorf("initIndicator failed") @@ -372,11 +381,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } s.InitDrawCommands(store, &profit, &cumProfit) store.OnKLineClosed(func(kline types.KLine) { - s.minutesCounter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Minutes()) + s.counter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) if kline.Interval == s.Interval { s.klineHandler(ctx, kline) - } else if kline.Interval == types.Interval1m { - s.klineHandler1m(ctx, kline) + } else if kline.Interval == s.MinInterval { + s.klineHandlerMin(ctx, kline) } }) @@ -401,7 +410,7 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) } -func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) { +func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) { if s.Status != types.StrategyStatusRunning { return } @@ -513,7 +522,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { return } if createdOrders != nil { - s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + s.orderPendingCounter[createdOrders[0].OrderID] = s.counter } return } @@ -539,7 +548,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { return } if createdOrders != nil { - s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + s.orderPendingCounter[createdOrders[0].OrderID] = s.counter } return } diff --git a/pkg/types/interval.go b/pkg/types/interval.go index 405f64939d..300ca08655 100644 --- a/pkg/types/interval.go +++ b/pkg/types/interval.go @@ -25,8 +25,41 @@ func (i Interval) Seconds() int { return m } +// specially handled, for better precision +func (i Interval) Milliseconds() int { + t := 0 + index := 0 + for i, rn := range string(i) { + if rn >= '0' && rn <= '9' { + t = t*10 + int(rn-'0') + } else { + index = i + break + } + } + switch strings.ToLower(string(i[index:])) { + case "ms": + return t + case "s": + return t * 1000 + case "m": + t *= 60 + case "h": + t *= 60 * 60 + case "d": + t *= 60 * 60 * 24 + case "w": + t *= 60 * 60 * 24 * 7 + case "mo": + t *= 60 * 60 * 24 * 30 + default: + panic("unknown interval input: " + i) + } + return t * 1000 +} + func (i Interval) Duration() time.Duration { - return time.Duration(i.Seconds()) * time.Second + return time.Duration(i.Milliseconds()) * time.Millisecond } func (i *Interval) UnmarshalJSON(b []byte) (err error) { @@ -53,6 +86,7 @@ func (s IntervalSlice) StringSlice() (slice []string) { return slice } +var Interval1ms = Interval("1ms") var Interval1s = Interval("1s") var Interval1m = Interval("1m") var Interval3m = Interval("3m") From e021cdd06092b79cb11b27fac68b54eac6c2b02c Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 19 Oct 2022 14:42:34 +0900 Subject: [PATCH 0016/1392] rename: lock to mu --- pkg/bbgo/serialmarketdatastore.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/bbgo/serialmarketdatastore.go b/pkg/bbgo/serialmarketdatastore.go index f4b6bf01e0..51258ec724 100644 --- a/pkg/bbgo/serialmarketdatastore.go +++ b/pkg/bbgo/serialmarketdatastore.go @@ -16,7 +16,7 @@ type SerialMarketDataStore struct { MinInterval types.Interval Subscription []types.Interval o, h, l, c, v, qv, price fixedpoint.Value - lock sync.Mutex + mu sync.Mutex } // @param symbol: symbol to trace on @@ -56,7 +56,7 @@ func (store *SerialMarketDataStore) handleKLineClosed(kline types.KLine) { } func (store *SerialMarketDataStore) handleMarketTrade(trade types.Trade) { - store.lock.Lock() + store.mu.Lock() store.price = trade.Price store.c = store.price if store.price.Compare(store.h) > 0 { @@ -74,7 +74,7 @@ func (store *SerialMarketDataStore) handleMarketTrade(trade types.Trade) { } store.v.Add(trade.Quantity) store.qv.Add(trade.QuoteQuantity) - store.lock.Unlock() + store.mu.Unlock() } func (store *SerialMarketDataStore) tickerProcessor(ctx context.Context) { @@ -92,7 +92,7 @@ func (store *SerialMarketDataStore) tickerProcessor(ctx context.Context) { Interval: store.MinInterval, Closed: true, } - store.lock.Lock() + store.mu.Lock() if store.c.IsZero() { kline.Open = store.price kline.Close = store.price @@ -114,7 +114,7 @@ func (store *SerialMarketDataStore) tickerProcessor(ctx context.Context) { store.v = fixedpoint.Zero store.qv = fixedpoint.Zero } - store.lock.Unlock() + store.mu.Unlock() store.AddKLine(kline, true) case <-ctx.Done(): return From d247e1cb97f2d87b5f59cbfb00277b6630006889 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 19 Oct 2022 14:48:46 +0900 Subject: [PATCH 0017/1392] fix: show error message when aggTrade is used in backtesting --- pkg/bbgo/serialmarketdatastore.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/bbgo/serialmarketdatastore.go b/pkg/bbgo/serialmarketdatastore.go index 51258ec724..a9ee20e29f 100644 --- a/pkg/bbgo/serialmarketdatastore.go +++ b/pkg/bbgo/serialmarketdatastore.go @@ -7,6 +7,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + log "github.com/sirupsen/logrus" ) type SerialMarketDataStore struct { @@ -44,6 +45,11 @@ func (store *SerialMarketDataStore) Subscribe(interval types.Interval) { func (store *SerialMarketDataStore) BindStream(ctx context.Context, stream types.Stream) { if store.UseAggTrade { + if IsBackTesting { + log.Errorf("Right now in backtesting, aggTrade event is not yet supported. Use OnKLineClosed instead.") + stream.OnKLineClosed(store.handleKLineClosed) + return + } go store.tickerProcessor(ctx) stream.OnAggTrade(store.handleMarketTrade) } else { From 3d672ea51818550d131cbe73e693d44d02840c20 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 19 Oct 2022 14:59:37 +0900 Subject: [PATCH 0018/1392] fix: comment format, dbg logs in session --- pkg/bbgo/session.go | 3 --- pkg/types/interval.go | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 7457f9a0c6..e305adef54 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -422,7 +422,6 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ // always subscribe the 1m kline so we can make sure the connection persists. klineSubscriptions[minInterval] = struct{}{} - log.Warnf("sub: %v", klineSubscriptions) for interval := range klineSubscriptions { // avoid querying the last unclosed kline @@ -451,8 +450,6 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ session.lastPrices[symbol] = lastKLine.Close } - log.Warnf("load %s", interval) - for _, k := range kLines { // let market data store trigger the update, so that the indicator could be updated too. marketDataStore.AddKLine(k) diff --git a/pkg/types/interval.go b/pkg/types/interval.go index 300ca08655..10c0243afd 100644 --- a/pkg/types/interval.go +++ b/pkg/types/interval.go @@ -25,7 +25,8 @@ func (i Interval) Seconds() int { return m } -// specially handled, for better precision +// Milliseconds is specially handled, for better precision +// for ms level interval, calling Seconds and Minutes directly might trigger panic error func (i Interval) Milliseconds() int { t := 0 index := 0 From 17825fbde1caf66a6631ce80595e7b07bde6328f Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 19 Oct 2022 17:59:00 +0900 Subject: [PATCH 0019/1392] fix: rate settings in telegram, make elliottwave draw async --- config/elliottwave.yaml | 32 +++++----- pkg/bbgo/serialmarketdatastore.go | 6 +- pkg/bbgo/trader.go | 68 ++++++-------------- pkg/notifier/telegramnotifier/telegram.go | 2 +- pkg/strategy/drift/strategy.go | 10 +-- pkg/strategy/elliottwave/draw.go | 74 +++++++++++----------- pkg/strategy/elliottwave/strategy.go | 77 ++++++++++++----------- 7 files changed, 127 insertions(+), 142 deletions(-) diff --git a/config/elliottwave.yaml b/config/elliottwave.yaml index 1d57a20dd1..a2f37d1ceb 100644 --- a/config/elliottwave.yaml +++ b/config/elliottwave.yaml @@ -24,17 +24,17 @@ exchangeStrategies: - on: binance elliottwave: minInterval: 1s - symbol: BNBBUSD + symbol: BTCUSDT limitOrder: true - quantity: 0.16 + #quantity: 0.16 # kline interval for indicators - interval: 1m + interval: 1s stoploss: 0.01% windowATR: 14 - windowQuick: 5 - windowSlow: 9 + windowQuick: 4 + windowSlow: 155 source: hl2 - pendingMinutes: 10 + pendingMinInterval: 5 useHeikinAshi: true drawGraph: true @@ -47,12 +47,12 @@ exchangeStrategies: # when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop #trailingActivationRatio: [0.01, 0.016, 0.05] #trailingActivationRatio: [0.001, 0.0081, 0.022] - trailingActivationRatio: [0.0017, 0.01, 0.015] - #trailingActivationRatio: [] - #trailingCallbackRate: [] + #trailingActivationRatio: [0.0017, 0.01, 0.015] + trailingActivationRatio: [] + trailingCallbackRate: [] #trailingCallbackRate: [0.002, 0.01, 0.1] #trailingCallbackRate: [0.0004, 0.0009, 0.018] - trailingCallbackRate: [0.0006, 0.0019, 0.006] + #trailingCallbackRate: [0.0006, 0.0019, 0.006] #exits: # - roiStopLoss: @@ -108,13 +108,13 @@ sync: sessions: - binance symbols: - - BNBBUSD + - BTCUSDT backtest: - startTime: "2022-09-01" - endTime: "2022-09-30" + startTime: "2022-10-15" + endTime: "2022-10-19" symbols: - - BNBBUSD + - BTCUSDT sessions: [binance] syncSecKLines: true accounts: @@ -122,5 +122,5 @@ backtest: makerFeeRate: 0.000 takerFeeRate: 0.000 balances: - BNB: 0 - BUSD: 100 + BTC: 0 + USDT: 100 diff --git a/pkg/bbgo/serialmarketdatastore.go b/pkg/bbgo/serialmarketdatastore.go index a9ee20e29f..588c2ad8f2 100644 --- a/pkg/bbgo/serialmarketdatastore.go +++ b/pkg/bbgo/serialmarketdatastore.go @@ -46,7 +46,7 @@ func (store *SerialMarketDataStore) Subscribe(interval types.Interval) { func (store *SerialMarketDataStore) BindStream(ctx context.Context, stream types.Stream) { if store.UseAggTrade { if IsBackTesting { - log.Errorf("Right now in backtesting, aggTrade event is not yet supported. Use OnKLineClosed instead.") + log.Errorf("right now in backtesting, aggTrade event is not yet supported. Use OnKLineClosed instead.") stream.OnKLineClosed(store.handleKLineClosed) return } @@ -85,6 +85,10 @@ func (store *SerialMarketDataStore) handleMarketTrade(trade types.Trade) { func (store *SerialMarketDataStore) tickerProcessor(ctx context.Context) { duration := store.MinInterval.Duration() + relativeTime := time.Now().UnixNano() % int64(duration) + waitTime := int64(duration) - relativeTime + ch := time.After(time.Duration(waitTime)) + <-ch intervalCloseTicker := time.NewTicker(duration) defer intervalCloseTicker.Stop() diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index b80e15dc94..b8cecdea5a 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -166,52 +166,6 @@ func (trader *Trader) SetRiskControls(riskControls *RiskControls) { trader.riskControls = riskControls } -func (trader *Trader) Subscribe() { - // pre-subscribe the data - for sessionName, strategies := range trader.exchangeStrategies { - session := trader.environment.sessions[sessionName] - for _, strategy := range strategies { - if defaulter, ok := strategy.(StrategyDefaulter); ok { - if err := defaulter.Defaults(); err != nil { - panic(err) - } - } - - if initializer, ok := strategy.(StrategyInitializer); ok { - if err := initializer.Initialize(); err != nil { - panic(err) - } - } - - if subscriber, ok := strategy.(ExchangeSessionSubscriber); ok { - subscriber.Subscribe(session) - } else { - log.Errorf("strategy %s does not implement ExchangeSessionSubscriber", strategy.ID()) - } - } - } - - for _, strategy := range trader.crossExchangeStrategies { - if defaulter, ok := strategy.(StrategyDefaulter); ok { - if err := defaulter.Defaults(); err != nil { - panic(err) - } - } - - if initializer, ok := strategy.(StrategyInitializer); ok { - if err := initializer.Initialize(); err != nil { - panic(err) - } - } - - if subscriber, ok := strategy.(CrossExchangeSessionSubscriber); ok { - subscriber.CrossSubscribe(trader.environment.sessions) - } else { - log.Errorf("strategy %s does not implement CrossExchangeSessionSubscriber", strategy.ID()) - } - } -} - func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy SingleExchangeStrategy, session *ExchangeSession, orderExecutor OrderExecutor) error { if v, ok := strategy.(StrategyValidator); ok { if err := v.Validate(); err != nil { @@ -262,7 +216,7 @@ func (trader *Trader) RunAllSingleExchangeStrategy(ctx context.Context) error { return nil } -func (trader *Trader) injectFields(ctx context.Context) error { +func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error { // load and run Session strategies for sessionName, strategies := range trader.exchangeStrategies { var session = trader.environment.sessions[sessionName] @@ -350,6 +304,24 @@ func (trader *Trader) injectFields(ctx context.Context) error { if err := trader.injectCommonServices(strategy); err != nil { return err } + + if defaulter, ok := strategy.(StrategyDefaulter); ok { + if err := defaulter.Defaults(); err != nil { + return err + } + } + + if initializer, ok := strategy.(StrategyInitializer); ok { + if err := initializer.Initialize(); err != nil { + return err + } + } + + if subscriber, ok := strategy.(CrossExchangeSessionSubscriber); ok { + subscriber.CrossSubscribe(trader.environment.sessions) + } else { + log.Errorf("strategy %s does not implement CrossExchangeSessionSubscriber", strategy.ID()) + } } return nil @@ -361,7 +333,7 @@ func (trader *Trader) Run(ctx context.Context) error { // trader.environment.Connect will call interact.Start interact.AddCustomInteraction(NewCoreInteraction(trader.environment, trader)) - if err := trader.injectFields(ctx); err != nil { + if err := trader.injectFieldsAndSubscribe(ctx); err != nil { return err } diff --git a/pkg/notifier/telegramnotifier/telegram.go b/pkg/notifier/telegramnotifier/telegram.go index e2018b4ca2..82bab5d244 100644 --- a/pkg/notifier/telegramnotifier/telegram.go +++ b/pkg/notifier/telegramnotifier/telegram.go @@ -15,7 +15,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -var apiLimiter = rate.NewLimiter(rate.Every(1*time.Second), 1) +var apiLimiter = rate.NewLimiter(rate.Every(50*time.Millisecond), 20) var log = logrus.WithField("service", "telegram") diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 1113e94332..40d62c24b8 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -98,10 +98,10 @@ type Strategy struct { SmootherWindow int `json:"smootherWindow"` FisherTransformWindow int `json:"fisherTransformWindow"` ATRWindow int `json:"atrWindow"` - PendingMinutes int `json:"pendingMinutes" modifiable:"true"` // if order not be traded for pendingMinutes of time, cancel it. - NoRebalance bool `json:"noRebalance" modifiable:"true"` // disable rebalance - TrendWindow int `json:"trendWindow"` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote - RebalanceFilter float64 `json:"rebalanceFilter" modifiable:"true"` // beta filter on the Linear Regression of trendLine + PendingMinInterval int `json:"pendingMinInterval" modifiable:"true"` // if order not be traded for pendingMinInterval of time, cancel it. + NoRebalance bool `json:"noRebalance" modifiable:"true"` // disable rebalance + TrendWindow int `json:"trendWindow"` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote + RebalanceFilter float64 `json:"rebalanceFilter" modifiable:"true"` // beta filter on the Linear Regression of trendLine TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"` TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"` @@ -276,7 +276,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e continue } log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.counter) - if s.counter-s.orderPendingCounter[order.OrderID] > s.PendingMinutes { + if s.counter-s.orderPendingCounter[order.OrderID] > s.PendingMinInterval { toCancel = true } else if order.Side == types.SideTypeBuy { // 75% of the probability diff --git a/pkg/strategy/elliottwave/draw.go b/pkg/strategy/elliottwave/draw.go index 2bf767ce91..6378839dcc 100644 --- a/pkg/strategy/elliottwave/draw.go +++ b/pkg/strategy/elliottwave/draw.go @@ -13,47 +13,49 @@ import ( func (s *Strategy) InitDrawCommands(store *bbgo.SerialMarketDataStore, profit, cumProfit types.Series) { bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { - canvas := s.DrawIndicators(store) - if canvas == nil { - reply.Message("cannot render indicators") - return - } - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render indicators in ewo") - reply.Message(fmt.Sprintf("[error] cannot render indicators in ewo: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + go func() { + canvas := s.DrawIndicators(store) + if canvas == nil { + reply.Message("cannot render indicators") + return + } + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render indicators in ewo") + reply.Message(fmt.Sprintf("[error] cannot render indicators in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }() }) bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { - canvas := s.DrawPNL(profit) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render pnl in drift") - reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + go func() { + canvas := s.DrawPNL(profit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in ewo") + reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }() }) bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { - canvas := s.DrawCumPNL(cumProfit) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render cumpnl in drift") - reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + go func() { + canvas := s.DrawCumPNL(cumProfit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render cumpnl in ewo") + reply.Message(fmt.Sprintf("[error] canot render cumpnl in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }() }) } func (s *Strategy) DrawIndicators(store *bbgo.SerialMarketDataStore) *types.Canvas { - klines, ok := store.KLinesOfInterval(types.Interval1m) - if !ok { - return nil - } - time := (*klines)[len(*klines)-1].StartTime + time := types.Time(s.startTime) canvas := types.NewCanvas(s.InstanceID(), s.Interval) Length := s.priceLines.Length() if Length > 300 { @@ -109,10 +111,10 @@ func (s *Strategy) Draw(store *bbgo.SerialMarketDataStore, profit, cumProfit typ log.WithError(err).Errorf("cannot create on path " + s.GraphIndicatorPath) return } - defer f.Close() if err = canvas.Render(chart.PNG, f); err != nil { log.WithError(err).Errorf("cannot render elliottwave") } + f.Close() canvas = s.DrawPNL(profit) f, err = os.Create(s.GraphPNLPath) @@ -120,19 +122,19 @@ func (s *Strategy) Draw(store *bbgo.SerialMarketDataStore, profit, cumProfit typ log.WithError(err).Errorf("cannot create on path " + s.GraphPNLPath) return } - defer f.Close() if err = canvas.Render(chart.PNG, f); err != nil { log.WithError(err).Errorf("cannot render pnl") return } + f.Close() canvas = s.DrawCumPNL(cumProfit) f, err = os.Create(s.GraphCumPNLPath) if err != nil { log.WithError(err).Errorf("cannot create on path " + s.GraphCumPNLPath) return } - defer f.Close() if err = canvas.Render(chart.PNG, f); err != nil { log.WithError(err).Errorf("cannot render cumpnl") } + f.Close() } diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go index 7128affcc1..f135d47399 100644 --- a/pkg/strategy/elliottwave/strategy.go +++ b/pkg/strategy/elliottwave/strategy.go @@ -43,14 +43,14 @@ type Strategy struct { types.Market Session *bbgo.ExchangeSession - Interval types.Interval `json:"interval"` - MinInterval types.Interval `json:"minInterval"` - Stoploss fixedpoint.Value `json:"stoploss" modifiable:"true"` - WindowATR int `json:"windowATR"` - WindowQuick int `json:"windowQuick"` - WindowSlow int `json:"windowSlow"` - PendingMinutes int `json:"pendingMinutes" modifiable:"true"` - UseHeikinAshi bool `json:"useHeikinAshi"` + Interval types.Interval `json:"interval"` + MinInterval types.Interval `json:"minInterval"` + Stoploss fixedpoint.Value `json:"stoploss" modifiable:"true"` + WindowATR int `json:"windowATR"` + WindowQuick int `json:"windowQuick"` + WindowSlow int `json:"windowSlow"` + PendingMinInterval int `json:"pendingMinInterval" modifiable:"true"` + UseHeikinAshi bool `json:"useHeikinAshi"` // whether to draw graph or not by the end of backtest DrawGraph bool `json:"drawGraph"` @@ -196,7 +196,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int { } log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.counter) toCancel := false - if s.counter-s.orderPendingCounter[order.OrderID] >= s.PendingMinutes { + if s.counter-s.orderPendingCounter[order.OrderID] >= s.PendingMinInterval { toCancel = true } else if order.Side == types.SideTypeBuy { if order.Price.Float64()+s.atr.Last()*2 <= pricef { @@ -211,7 +211,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int { panic("not supported side for the order") } if toCancel { - err := s.GeneralOrderExecutor.GracefulCancel(ctx, order) + err := s.GeneralOrderExecutor.CancelNoWait(ctx, order) if err == nil { delete(s.orderPendingCounter, order.OrderID) } else { @@ -235,6 +235,9 @@ func (s *Strategy) trailingCheck(price float64, direction string) bool { s.lowestPrice = price } isShort := direction == "short" + if isShort && s.sellPrice == 0 || !isShort && s.buyPrice == 0 { + return false + } for i := len(s.TrailingCallbackRate) - 1; i >= 0; i-- { trailingCallbackRate := s.TrailingCallbackRate[i] trailingActivationRatio := s.TrailingActivationRatio[i] @@ -244,7 +247,7 @@ func (s *Strategy) trailingCheck(price float64, direction string) bool { } } else { if (s.highestPrice-s.buyPrice)/s.buyPrice > trailingActivationRatio { - return (s.highestPrice-price)/price > trailingCallbackRate + return (s.highestPrice-price)/s.buyPrice > trailingCallbackRate } } } @@ -351,15 +354,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.highestPrice = 0 s.lowestPrice = 0 } else if s.Position.IsLong() { - s.buyPrice = price + s.buyPrice = s.Position.ApproximateAverageCost.Float64() s.sellPrice = 0 - s.highestPrice = s.buyPrice + s.highestPrice = math.Max(s.buyPrice, s.highestPrice) s.lowestPrice = 0 } else { - s.sellPrice = price + s.sellPrice = s.Position.ApproximateAverageCost.Float64() s.buyPrice = 0 s.highestPrice = 0 - s.lowestPrice = s.sellPrice + if s.lowestPrice == 0 { + s.lowestPrice = s.sellPrice + } else { + s.lowestPrice = math.Min(s.lowestPrice, s.sellPrice) + } } }) s.initTickerFunctions() @@ -477,15 +484,18 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { bull := kline.Close.Compare(kline.Open) > 0 balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() - bbgo.Notify("source: %.4f, price: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", sourcef, pricef, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) - bbgo.Notify("balances: [Total] %v %s [Base] %s(%v %s) [Quote] %s", - s.CalcAssetValue(price), - s.Market.QuoteCurrency, - balances[s.Market.BaseCurrency].String(), - balances[s.Market.BaseCurrency].Total().Mul(price), - s.Market.QuoteCurrency, - balances[s.Market.QuoteCurrency].String(), - ) + startTime := kline.StartTime.Time() + if startTime.Round(time.Second) == startTime.Round(time.Minute) { + bbgo.Notify("source: %.4f, price: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", sourcef, pricef, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) + bbgo.Notify("balances: [Total] %v %s [Base] %s(%v %s) [Quote] %s", + s.CalcAssetValue(price), + s.Market.QuoteCurrency, + balances[s.Market.BaseCurrency].String(), + balances[s.Market.BaseCurrency].Total().Mul(price), + s.Market.QuoteCurrency, + balances[s.Market.QuoteCurrency].String(), + ) + } shortCondition := ewo[0] < ewo[1] && ewo[1] >= ewo[2] && (ewo[1] <= ewo[2] || ewo[2] >= ewo[3]) || s.sellPrice == 0 && ewo[0] < ewo[1] && ewo[1] < ewo[2] longCondition := ewo[0] > ewo[1] && ewo[1] <= ewo[2] && (ewo[1] >= ewo[2] || ewo[2] <= ewo[3]) || s.buyPrice == 0 && ewo[0] > ewo[1] && ewo[1] > ewo[2] @@ -493,18 +503,19 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { exitShortCondition := s.sellPrice > 0 && !shortCondition && s.sellPrice*(1.+stoploss) <= highf || s.sellPrice+atr <= highf || s.trailingCheck(highf, "short") exitLongCondition := s.buyPrice > 0 && !longCondition && s.buyPrice*(1.-stoploss) >= lowf || s.buyPrice-atr >= lowf || s.trailingCheck(lowf, "long") - if exitShortCondition || exitLongCondition { - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { - log.WithError(err).Errorf("cannot cancel orders") + if exitShortCondition || exitLongCondition || (longCondition && bull) || (shortCondition && !bull) { + if hold := s.smartCancel(ctx, pricef); hold > 0 { return } + } else { + s.smartCancel(ctx, pricef) + return + } + if exitShortCondition || exitLongCondition { s.ClosePosition(ctx, fixedpoint.One) } + if longCondition && bull { - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - return - } if source.Compare(price) > 0 { source = price } @@ -527,10 +538,6 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { return } if shortCondition && !bull { - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - return - } if source.Compare(price) < 0 { source = price } From a8d60b251fb34d08ac173b94641f7d63e94e9a91 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 20 Oct 2022 17:43:17 +0900 Subject: [PATCH 0020/1392] fix: binance market/aggregated trade parsing for QuoteQuantity. fix related bugs in timestamp in serialmarketdatastore. --- pkg/bbgo/serialmarketdatastore.go | 10 +++++----- pkg/exchange/binance/parse.go | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/bbgo/serialmarketdatastore.go b/pkg/bbgo/serialmarketdatastore.go index 588c2ad8f2..48f3502cd9 100644 --- a/pkg/bbgo/serialmarketdatastore.go +++ b/pkg/bbgo/serialmarketdatastore.go @@ -51,7 +51,7 @@ func (store *SerialMarketDataStore) BindStream(ctx context.Context, stream types return } go store.tickerProcessor(ctx) - stream.OnAggTrade(store.handleMarketTrade) + stream.OnMarketTrade(store.handleMarketTrade) } else { stream.OnKLineClosed(store.handleKLineClosed) } @@ -78,8 +78,8 @@ func (store *SerialMarketDataStore) handleMarketTrade(trade types.Trade) { if store.o.IsZero() { store.o = store.price } - store.v.Add(trade.Quantity) - store.qv.Add(trade.QuoteQuantity) + store.v = store.v.Add(trade.Quantity) + store.qv = store.qv.Add(trade.QuoteQuantity) store.mu.Unlock() } @@ -97,7 +97,7 @@ func (store *SerialMarketDataStore) tickerProcessor(ctx context.Context) { case time := <-intervalCloseTicker.C: kline := types.KLine{ Symbol: store.Symbol, - StartTime: types.Time(time.Add(-1 * duration)), + StartTime: types.Time(time.Add(-1 * duration).Round(duration)), EndTime: types.Time(time), Interval: store.MinInterval, Closed: true, @@ -143,7 +143,7 @@ func (store *SerialMarketDataStore) AddKLine(kline types.KLine, async ...bool) { } // endtime duration := store.MinInterval.Duration() - timestamp := kline.StartTime.Time().Round(duration).Add(duration) + timestamp := kline.StartTime.Time().Add(duration) for _, val := range store.Subscription { k, ok := store.KLines[val] if !ok { diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index efbe780945..3be3564f33 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -538,7 +538,7 @@ func (e *MarketTradeEvent) Trade() types.Trade { Side: side, Price: e.Price, Quantity: e.Quantity, - QuoteQuantity: e.Quantity, + QuoteQuantity: e.Quantity.Mul(e.Price), IsBuyer: isBuyer, IsMaker: e.IsMaker, Time: types.Time(tt), @@ -595,7 +595,7 @@ func (e *AggTradeEvent) Trade() types.Trade { Side: side, Price: e.Price, Quantity: e.Quantity, - QuoteQuantity: e.Quantity, + QuoteQuantity: e.Quantity.Mul(e.Price), IsBuyer: isBuyer, IsMaker: e.IsMaker, Time: types.Time(tt), From a15d12567977a5bb9533b08daa0ab829f31075dd Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 20 Oct 2022 17:45:18 +0900 Subject: [PATCH 0021/1392] fix: instead of aggTrade, use market trade to match kline result --- pkg/bbgo/serialmarketdatastore.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/bbgo/serialmarketdatastore.go b/pkg/bbgo/serialmarketdatastore.go index 48f3502cd9..f660e49fa9 100644 --- a/pkg/bbgo/serialmarketdatastore.go +++ b/pkg/bbgo/serialmarketdatastore.go @@ -12,7 +12,7 @@ import ( type SerialMarketDataStore struct { *MarketDataStore - UseAggTrade bool + UseMarketTrade bool KLines map[types.Interval]*types.KLine MinInterval types.Interval Subscription []types.Interval @@ -22,12 +22,12 @@ type SerialMarketDataStore struct { // @param symbol: symbol to trace on // @param minInterval: unit interval, related to your signal timeframe -// @param useAggTrade: if not assigned, default to false. if assigned to true, will use AggTrade signal to generate klines -func NewSerialMarketDataStore(symbol string, minInterval types.Interval, useAggTrade ...bool) *SerialMarketDataStore { +// @param useMarketTrade: if not assigned, default to false. if assigned to true, will use MarketTrade signal to generate klines +func NewSerialMarketDataStore(symbol string, minInterval types.Interval, useMarketTrade ...bool) *SerialMarketDataStore { return &SerialMarketDataStore{ MarketDataStore: NewMarketDataStore(symbol), KLines: make(map[types.Interval]*types.KLine), - UseAggTrade: len(useAggTrade) > 0 && useAggTrade[0], + UseMarketTrade: len(useMarketTrade) > 0 && useMarketTrade[0], Subscription: []types.Interval{}, MinInterval: minInterval, } @@ -44,7 +44,7 @@ func (store *SerialMarketDataStore) Subscribe(interval types.Interval) { } func (store *SerialMarketDataStore) BindStream(ctx context.Context, stream types.Stream) { - if store.UseAggTrade { + if store.UseMarketTrade { if IsBackTesting { log.Errorf("right now in backtesting, aggTrade event is not yet supported. Use OnKLineClosed instead.") stream.OnKLineClosed(store.handleKLineClosed) From ce86544c43c76acc051d3bbf27016eae39ead168 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 20 Oct 2022 20:03:10 +0900 Subject: [PATCH 0022/1392] optimize: drift strategy to use market trade signals --- config/drift.yaml | 102 --------- config/driftBTC.yaml | 27 ++- pkg/strategy/drift/strategy.go | 307 ++++++++++++--------------- pkg/strategy/elliottwave/strategy.go | 2 +- 4 files changed, 148 insertions(+), 290 deletions(-) delete mode 100644 config/drift.yaml diff --git a/config/drift.yaml b/config/drift.yaml deleted file mode 100644 index 0ace1ba49c..0000000000 --- a/config/drift.yaml +++ /dev/null @@ -1,102 +0,0 @@ ---- -persistence: - redis: - host: 127.0.0.1 - port: 6379 - db: 0 - -sessions: - binance: - exchange: binance - futures: false - envVarPrefix: binance - heikinAshi: false - - # Drift strategy intends to place buy/sell orders as much value mas it could be. To exchanges that requires to - # calculate fees before placing limit orders (e.g. FTX Pro), make sure the fee rate is configured correctly and - # enable modifyOrderAmountForFee to prevent order rejection. - makerFeeRate: 0.0002 - takerFeeRate: 0.0007 - modifyOrderAmountForFee: false - -exchangeStrategies: - -- on: binance - drift: - canvasPath: "./output.png" - symbol: ETHBUSD - limitOrder: false - quantity: 0.01 - # kline interval for indicators - interval: 1m - window: 1 - useAtr: true - useStopLoss: true - stoploss: 0.23% - source: ohlc4 - predictOffset: 2 - noTrailingStopLoss: false - trailingStopLossType: kline - # stddev on high/low-source - hlVarianceMultiplier: 0.13 - hlRangeWindow: 4 - smootherWindow: 19 - fisherTransformWindow: 73 - atrWindow: 14 - # orders not been traded will be canceled after `pendingMinutes` minutes - pendingMinutes: 5 - noRebalance: true - trendWindow: 12 - rebalanceFilter: 2 - - trailingActivationRatio: [0.0015, 0.002, 0.004, 0.01] - trailingCallbackRate: [0.0001, 0.00012, 0.001, 0.002] - - generateGraph: true - graphPNLDeductFee: true - graphPNLPath: "./pnl.png" - graphCumPNLPath: "./cumpnl.png" - #exits: - #- roiStopLoss: - # percentage: 0.8% - #- roiTakeProfit: - # percentage: 35% - #- protectiveStopLoss: - # activationRatio: 0.6% - # stopLossRatio: 0.1% - # placeStopOrder: false - #- protectiveStopLoss: - # activationRatio: 5% - # stopLossRatio: 1% - # placeStopOrder: false - #- cumulatedVolumeTakeProfit: - # interval: 5m - # window: 2 - # minQuoteVolume: 200_000_000 - #- protectiveStopLoss: - # activationRatio: 2% - # stopLossRatio: 1% - # placeStopOrder: false - -sync: - userDataStream: - trades: true - filledOrders: true - sessions: - - binance - symbols: - - ETHBUSD - -backtest: - startTime: "2022-09-25" - endTime: "2022-09-30" - symbols: - - ETHBUSD - sessions: [binance] - accounts: - binance: - makerFeeRate: 0.0000 - takerFeeRate: 0.0000 - balances: - ETH: 0.03 - BUSD: 0 diff --git a/config/driftBTC.yaml b/config/driftBTC.yaml index ffe9733675..898bbd4a2a 100644 --- a/config/driftBTC.yaml +++ b/config/driftBTC.yaml @@ -26,6 +26,7 @@ exchangeStrategies: - on: binance drift: + debug: false minInterval: 1s limitOrder: true #quantity: 0.0012 @@ -33,26 +34,24 @@ exchangeStrategies: symbol: BTCUSDT # kline interval for indicators interval: 1s - window: 6 + window: 2 useAtr: true useStopLoss: true - stoploss: 0.05% + stoploss: 0.01% source: hl2 predictOffset: 2 - noTrailingStopLoss: false - trailingStopLossType: kline + noTrailingStopLoss: true # stddev on high/low-source - hlVarianceMultiplier: 0.14 - hlRangeWindow: 4 - smootherWindow: 3 - fisherTransformWindow: 125 - #fisherTransformWindow: 117 + hlVarianceMultiplier: 0.7 + hlRangeWindow: 6 + smootherWindow: 10 + fisherTransformWindow: 45 atrWindow: 24 # orders not been traded will be canceled after `pendingMinutes` minutes - pendingMinutes: 10 + pendingMinInterval: 6 noRebalance: true - trendWindow: 15 - rebalanceFilter: -0.1 + trendWindow: 4 + rebalanceFilter: 2 # ActivationRatio should be increasing order # when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop @@ -126,8 +125,8 @@ sync: - BTCUSDT backtest: - startTime: "2022-10-18" - endTime: "2022-10-19" + startTime: "2022-10-19" + endTime: "2022-10-20" symbols: - BTCUSDT sessions: [binance] diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 40d62c24b8..cf4287c87a 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -66,57 +66,58 @@ type Strategy struct { *types.TradeStats `persistence:"trade_stats"` p *types.Position - MinInterval types.Interval `json:"MinInterval"` - - priceLines *types.Queue - trendLine types.UpdatableSeriesExtend - ma types.UpdatableSeriesExtend - stdevHigh *indicator.StdDev - stdevLow *indicator.StdDev - drift *DriftMA - atr *indicator.ATR - midPrice fixedpoint.Value - lock sync.RWMutex `ignore:"true"` - positionLock sync.RWMutex `ignore:"true"` - startTime time.Time - counter int - orderPendingCounter map[uint64]int - frameKLine *types.KLine - klineMin *types.KLine - - beta float64 - - UseStopLoss bool `json:"useStopLoss" modifiable:"true"` - UseAtr bool `json:"useAtr" modifiable:"true"` - StopLoss fixedpoint.Value `json:"stoploss" modifiable:"true"` - CanvasPath string `json:"canvasPath"` - PredictOffset int `json:"predictOffset"` - HighLowVarianceMultiplier float64 `json:"hlVarianceMultiplier" modifiable:"true"` - NoTrailingStopLoss bool `json:"noTrailingStopLoss" modifiable:"true"` - TrailingStopLossType string `json:"trailingStopLossType" modifiable:"true"` // trailing stop sources. Possible options are `kline` for 1m kline and `realtime` from order updates - HLRangeWindow int `json:"hlRangeWindow"` - SmootherWindow int `json:"smootherWindow"` - FisherTransformWindow int `json:"fisherTransformWindow"` - ATRWindow int `json:"atrWindow"` - PendingMinInterval int `json:"pendingMinInterval" modifiable:"true"` // if order not be traded for pendingMinInterval of time, cancel it. - NoRebalance bool `json:"noRebalance" modifiable:"true"` // disable rebalance - TrendWindow int `json:"trendWindow"` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote - RebalanceFilter float64 `json:"rebalanceFilter" modifiable:"true"` // beta filter on the Linear Regression of trendLine + MinInterval types.Interval `json:"MinInterval"` // minimum interval referred for doing stoploss/trailing exists and updating highest/lowest + + priceLines *types.Queue + trendLine types.UpdatableSeriesExtend + ma types.UpdatableSeriesExtend + stdevHigh *indicator.StdDev + stdevLow *indicator.StdDev + drift *DriftMA + atr *indicator.ATR + midPrice fixedpoint.Value // the midPrice is the average of bestBid and bestAsk in public orderbook + lock sync.RWMutex `ignore:"true"` // lock for midPrice + positionLock sync.RWMutex `ignore:"true"` // lock for highest/lowest and p + startTime time.Time // trading start time + counter int // number of MinInterval since startTime + maxCounterBuyCanceled int // the largest counter of the order on the buy side been cancelled. meaning the latest cancelled buy order. + maxCounterSellCanceled int // the largest counter of the order on the sell side been cancelled. meaning the latest cancelled sell order. + orderPendingCounter map[uint64]int // records the timepoint when the orders are created, using the counter at the time. + frameKLine *types.KLine // last kline in Interval + klineMin *types.KLine // last kline in MinInterval + + beta float64 // last beta value from trendline's linear regression (previous slope of the trendline) + + Debug bool `json:"debug" modifiable:"true"` // to print debug message or not + UseStopLoss bool `json:"useStopLoss" modifiable:"true"` // whether to use stoploss rate to do stoploss + UseAtr bool `json:"useAtr" modifiable:"true"` // use atr as stoploss + StopLoss fixedpoint.Value `json:"stoploss" modifiable:"true"` // stoploss rate + PredictOffset int `json:"predictOffset"` // the lookback length for the prediction using linear regression + HighLowVarianceMultiplier float64 `json:"hlVarianceMultiplier" modifiable:"true"` // modifier to set the limit order price + NoTrailingStopLoss bool `json:"noTrailingStopLoss" modifiable:"true"` // turn off the trailing exit and stoploss + HLRangeWindow int `json:"hlRangeWindow"` // ma window for kline high/low changes + SmootherWindow int `json:"smootherWindow"` // window that controls the smoothness of drift + FisherTransformWindow int `json:"fisherTransformWindow"` // fisher transform window to filter drift's negative signals + ATRWindow int `json:"atrWindow"` // window for atr indicator + PendingMinInterval int `json:"pendingMinInterval" modifiable:"true"` // if order not be traded for pendingMinInterval of time, cancel it. + NoRebalance bool `json:"noRebalance" modifiable:"true"` // disable rebalance + TrendWindow int `json:"trendWindow"` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote + RebalanceFilter float64 `json:"rebalanceFilter" modifiable:"true"` // beta filter on the Linear Regression of trendLine TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"` TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"` - buyPrice float64 `persistence:"buy_price"` - sellPrice float64 `persistence:"sell_price"` - highestPrice float64 `persistence:"highest_price"` - lowestPrice float64 `persistence:"lowest_price"` + buyPrice float64 `persistence:"buy_price"` // price when a long position is opened + sellPrice float64 `persistence:"sell_price"` // price when a short position is opened + highestPrice float64 `persistence:"highest_price"` // highestPrice when the position is opened + lowestPrice float64 `persistence:"lowest_price"` // lowestPrice when the position is opened // This is not related to trade but for statistics graph generation // Will deduct fee in percentage from every trade GraphPNLDeductFee bool `json:"graphPNLDeductFee"` - GraphPNLPath string `json:"graphPNLPath"` - GraphCumPNLPath string `json:"graphCumPNLPath"` - // Whether to generate graph when shutdown - GenerateGraph bool `json:"generateGraph"` + CanvasPath string `json:"canvasPath"` // backtest related. the path to store the indicator graph + GraphPNLPath string `json:"graphPNLPath"` // backtest related. the path to store the pnl % graph per trade graph. + GraphCumPNLPath string `json:"graphCumPNLPath"` // backtest related. the path to store the asset changes in graph + GenerateGraph bool `json:"generateGraph"` // whether to generate graph when shutdown ExitMethods bbgo.ExitMethodSet `json:"exits"` Session *bbgo.ExchangeSession @@ -139,7 +140,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { if !bbgo.IsBackTesting { session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) - session.Subscribe(types.AggTradeChannel, s.Symbol, types.SubscribeOptions{}) + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) // able to preload if s.MinInterval.Milliseconds() >= types.Interval1s.Milliseconds() && s.MinInterval.Milliseconds()%types.Interval1s.Milliseconds() == 0 { maxWindow := (s.Window + s.SmootherWindow + s.FisherTransformWindow) * (s.Interval.Milliseconds() / s.MinInterval.Milliseconds()) @@ -296,6 +297,15 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e err := s.GeneralOrderExecutor.CancelNoWait(ctx) // TODO: clean orderPendingCounter on cancel/trade for _, order := range nonTraded { + if order.Side == types.SideTypeSell { + if s.maxCounterSellCanceled < s.orderPendingCounter[order.OrderID] { + s.maxCounterSellCanceled = s.orderPendingCounter[order.OrderID] + } + } else { + if s.maxCounterBuyCanceled < s.orderPendingCounter[order.OrderID] { + s.maxCounterBuyCanceled = s.orderPendingCounter[order.OrderID] + } + } delete(s.orderPendingCounter, order.OrderID) } log.Warnf("cancel all %v", err) @@ -346,7 +356,6 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { bestBid := ticker.Buy bestAsk := ticker.Sell - var pricef float64 if !util.TryLock(&s.lock) { return } @@ -357,37 +366,10 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { } else { s.midPrice = bestBid } - pricef = s.midPrice.Float64() - s.lock.Unlock() - if !util.TryLock(&s.positionLock) { - return - } + // we removed realtime stoploss and trailingStop. - if s.highestPrice > 0 && s.highestPrice < pricef { - s.highestPrice = pricef - } - if s.lowestPrice > 0 && s.lowestPrice > pricef { - s.lowestPrice = pricef - } - if s.CheckStopLoss() { - s.positionLock.Unlock() - s.ClosePosition(ctx, fixedpoint.One) - return - } - // for trailing stoploss during the realtime - if s.NoTrailingStopLoss || s.TrailingStopLossType == "kline" { - s.positionLock.Unlock() - return - } - - exitCondition := s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long") - - s.positionLock.Unlock() - if exitCondition { - s.ClosePosition(ctx, fixedpoint.One) - } }) s.getLastPrice = func() (lastPrice fixedpoint.Value) { var ok bool @@ -585,35 +567,29 @@ func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) { if s.highestPrice > 0 && highf > s.highestPrice { s.highestPrice = highf } + s.positionLock.Unlock() numPending := 0 var err error if numPending, err = s.smartCancel(ctx, pricef, atr); err != nil { log.WithError(err).Errorf("cannot cancel orders") - s.positionLock.Unlock() return } if numPending > 0 { - s.positionLock.Unlock() return } - if s.NoTrailingStopLoss || s.TrailingStopLossType == "realtime" { - s.positionLock.Unlock() + if s.NoTrailingStopLoss { return } exitCondition := s.CheckStopLoss() || s.trailingCheck(highf, "short") || s.trailingCheck(lowf, "long") - s.positionLock.Unlock() if exitCondition { _ = s.ClosePosition(ctx, fixedpoint.One) } } func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { - var driftPred, atr float64 - var drift []float64 - s.frameKLine.Set(&kline) source := s.GetSource(&kline) @@ -625,9 +601,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { s.atr.PushK(kline) - driftPred = s.drift.Predict(s.PredictOffset) - ddriftPred := s.drift.drift.Predict(s.PredictOffset) - atr = s.atr.Last() + atr := s.atr.Last() price := s.getLastPrice() pricef := price.Float64() lowf := math.Min(kline.Low.Float64(), pricef) @@ -636,7 +610,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { s.stdevLow.Update(lowdiff) highdiff := highf - s.ma.Last() s.stdevHigh.Update(highdiff) - drift = s.drift.Array(2) + drift := s.drift.Array(2) if len(drift) < 2 || len(drift) < s.PredictOffset { return } @@ -649,77 +623,78 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { return } - s.positionLock.Lock() log.Infof("highdiff: %3.2f ma: %.2f, open: %8v, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(), s.ma.Last(), kline.Open, kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime) + + s.positionLock.Lock() if s.lowestPrice > 0 && lowf < s.lowestPrice { s.lowestPrice = lowf } if s.highestPrice > 0 && highf > s.highestPrice { s.highestPrice = highf } + s.positionLock.Unlock() if !s.NoRebalance { s.Rebalance(ctx) } - balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() - bbgo.Notify("source: %.4f, price: %.4f, driftPred: %.4f, ddriftPred: %.4f, drift[1]: %.4f, ddrift[1]: %.4f, atr: %.4f, lowf %.4f, highf: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", - sourcef, pricef, driftPred, ddriftPred, drift[1], ddrift[1], atr, lowf, highf, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) - // Notify will parse args to strings and process separately - bbgo.Notify("balances: [Total] %v %s [Base] %s(%v %s) [Quote] %s", - s.CalcAssetValue(price), - s.Market.QuoteCurrency, - balances[s.Market.BaseCurrency].String(), - balances[s.Market.BaseCurrency].Total().Mul(price), - s.Market.QuoteCurrency, - balances[s.Market.QuoteCurrency].String(), - ) + if s.Debug { + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + bbgo.Notify("source: %.4f, price: %.4f, drift[0]: %.4f, ddrift[0]: %.4f, lowf %.4f, highf: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", + sourcef, pricef, drift[0], ddrift[0], atr, lowf, highf, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) + // Notify will parse args to strings and process separately + bbgo.Notify("balances: [Total] %v %s [Base] %s(%v %s) [Quote] %s", + s.CalcAssetValue(price), + s.Market.QuoteCurrency, + balances[s.Market.BaseCurrency].String(), + balances[s.Market.BaseCurrency].Total().Mul(price), + s.Market.QuoteCurrency, + balances[s.Market.QuoteCurrency].String(), + ) + } shortCondition := drift[1] >= 0 && drift[0] <= 0 || (drift[1] >= drift[0] && drift[1] <= 0) || ddrift[1] >= 0 && ddrift[0] <= 0 || (ddrift[1] >= ddrift[0] && ddrift[1] <= 0) longCondition := drift[1] <= 0 && drift[0] >= 0 || (drift[1] <= drift[0] && drift[1] >= 0) || ddrift[1] <= 0 && ddrift[0] >= 0 || (ddrift[1] <= ddrift[0] && ddrift[1] >= 0) if shortCondition && longCondition { - if drift[1] > drift[0] { + if s.priceLines.Index(1) > s.priceLines.Last() { longCondition = false } else { shortCondition = false } } - exitCondition := s.CheckStopLoss() || s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long") + exitCondition := !s.NoTrailingStopLoss && (s.CheckStopLoss() || s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long")) - if exitCondition { - s.positionLock.Unlock() - if err := s.GeneralOrderExecutor.CancelNoWait(ctx); err != nil { + if exitCondition || longCondition || shortCondition { + var err error + var hold int + if hold, err = s.smartCancel(ctx, sourcef, atr); err != nil { log.WithError(err).Errorf("cannot cancel orders") - return } - _ = s.ClosePosition(ctx, fixedpoint.One) - if shortCondition || longCondition { - s.positionLock.Lock() - } else { + if hold > 0 { return } + } else { + if _, err := s.smartCancel(ctx, sourcef, atr); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + } + return } if longCondition { - if err := s.GeneralOrderExecutor.CancelNoWait(ctx); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - s.positionLock.Unlock() - return - } source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier)) if source.Compare(price) > 0 { source = price } - /*source = fixedpoint.NewFromFloat(s.ma.Last() - s.stdevLow.Last()*s.HighLowVarianceMultiplier) - if source.Compare(price) > 0 { - source = price - } - sourcef = source.Float64()*/ + log.Infof("source in long %v %v %f", source, price, s.stdevLow.Last()) - s.positionLock.Unlock() opt := s.OpenPositionOptions opt.Long = true + opt.LimitOrder = true + // force to use market taker + if s.counter-s.maxCounterBuyCanceled <= 1 { + opt.LimitOrder = false + } opt.Price = source opt.Tags = []string{"long"} createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) @@ -738,28 +713,20 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { return } if shortCondition { - if err := s.GeneralOrderExecutor.CancelNoWait(ctx); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - s.positionLock.Unlock() - return - } - source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier)) if source.Compare(price) < 0 { source = price } - /*source = fixedpoint.NewFromFloat(s.ma.Last() + s.stdevHigh.Last()*s.HighLowVarianceMultiplier) - if source.Compare(price) < 0 { - source = price - } - sourcef = source.Float64()*/ log.Infof("source in short: %v", source) - s.positionLock.Unlock() opt := s.OpenPositionOptions opt.Short = true opt.Price = source + opt.LimitOrder = true + if s.counter-s.maxCounterSellCanceled <= 1 { + opt.LimitOrder = false + } opt.Tags = []string{"short"} createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) if err != nil { @@ -775,7 +742,6 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { } return } - s.positionLock.Unlock() } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { @@ -842,13 +808,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } s.GeneralOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _profit, _netProfit fixedpoint.Value) { s.p.AddTrade(trade) - order, ok := s.GeneralOrderExecutor.TradeCollector().OrderStore().Get(trade.OrderID) - if !ok { - panic(fmt.Sprintf("cannot find order: %v", trade)) - } - tag := order.Tag - price := trade.Price.Float64() + delete(s.orderPendingCounter, trade.OrderID) if s.buyPrice > 0 { profit.Update(modify(price / s.buyPrice)) @@ -858,19 +819,18 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se cumProfit.Update(s.CalcAssetValue(trade.Price).Float64()) } s.positionLock.Lock() - defer s.positionLock.Unlock() if s.p.IsDust(trade.Price) { s.buyPrice = 0 s.sellPrice = 0 s.highestPrice = 0 s.lowestPrice = 0 } else if s.p.IsLong() { - s.buyPrice = s.p.ApproximateAverageCost.Float64() // trade.Price.Float64() + s.buyPrice = s.p.ApproximateAverageCost.Float64() s.sellPrice = 0 s.highestPrice = math.Max(s.buyPrice, s.highestPrice) s.lowestPrice = s.buyPrice } else if s.p.IsShort() { - s.sellPrice = s.p.ApproximateAverageCost.Float64() // trade.Price.Float64() + s.sellPrice = s.p.ApproximateAverageCost.Float64() s.buyPrice = 0 s.highestPrice = s.sellPrice if s.lowestPrice == 0 { @@ -879,7 +839,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.lowestPrice = math.Min(s.lowestPrice, s.sellPrice) } } - bbgo.Notify("tag: %s, sp: %.4f bp: %.4f hp: %.4f lp: %.4f, trade: %s, pos: %s", tag, s.sellPrice, s.buyPrice, s.highestPrice, s.lowestPrice, trade.String(), s.p.String()) + s.positionLock.Unlock() }) s.frameKLine = &types.KLine{} @@ -891,42 +851,43 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, s.startTime)) s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, s.startTime)) - // default value: use 1m kline - if !s.NoTrailingStopLoss && s.IsBackTesting() || s.TrailingStopLossType == "" { - s.TrailingStopLossType = "kline" - } - bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { - canvas := s.DrawIndicators(s.frameKLine.StartTime) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render indicators in drift") - reply.Message(fmt.Sprintf("[error] cannot render indicators in drift: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + go func() { + canvas := s.DrawIndicators(s.frameKLine.StartTime) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render indicators in drift") + reply.Message(fmt.Sprintf("[error] cannot render indicators in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }() }) bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { - canvas := s.DrawPNL(&profit) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render pnl in drift") - reply.Message(fmt.Sprintf("[error] cannot render pnl in drift: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + go func() { + canvas := s.DrawPNL(&profit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in drift") + reply.Message(fmt.Sprintf("[error] cannot render pnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }() }) bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { - canvas := s.DrawCumPNL(&cumProfit) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render cumpnl in drift") - reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + go func() { + canvas := s.DrawCumPNL(&cumProfit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render cumpnl in drift") + reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }() }) bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) { @@ -965,7 +926,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } store.OnKLineClosed(func(kline types.KLine) { - s.counter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) + s.counter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) / s.MinInterval.Milliseconds() if kline.Interval == s.Interval { s.klineHandler(ctx, kline) } else if kline.Interval == s.MinInterval { diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go index f135d47399..40703017ad 100644 --- a/pkg/strategy/elliottwave/strategy.go +++ b/pkg/strategy/elliottwave/strategy.go @@ -104,7 +104,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // this is not enough if we're subscribing 30m intervals using SerialMarketDataStore if !bbgo.IsBackTesting { session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) - session.Subscribe(types.AggTradeChannel, s.Symbol, types.SubscribeOptions{}) + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) if s.MinInterval.Milliseconds() >= types.Interval1s.Milliseconds() && s.MinInterval.Milliseconds()%types.Interval1s.Milliseconds() == 0 { bbgo.KLinePreloadLimit = int64(((s.Interval.Milliseconds()/s.MinInterval.Milliseconds())*s.WindowSlow/1000 + 1) + 1000) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ From 493b81f16c0352182cfad0de8af8d9dfa97412ba Mon Sep 17 00:00:00 2001 From: zenix Date: Fri, 21 Oct 2022 16:28:21 +0900 Subject: [PATCH 0023/1392] fix: remove redundant notification --- pkg/bbgo/serialmarketdatastore.go | 7 +++++-- pkg/strategy/drift/strategy.go | 3 --- pkg/strategy/elliottwave/draw.go | 3 --- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pkg/bbgo/serialmarketdatastore.go b/pkg/bbgo/serialmarketdatastore.go index f660e49fa9..0b4e321ac3 100644 --- a/pkg/bbgo/serialmarketdatastore.go +++ b/pkg/bbgo/serialmarketdatastore.go @@ -87,8 +87,11 @@ func (store *SerialMarketDataStore) tickerProcessor(ctx context.Context) { duration := store.MinInterval.Duration() relativeTime := time.Now().UnixNano() % int64(duration) waitTime := int64(duration) - relativeTime - ch := time.After(time.Duration(waitTime)) - <-ch + select { + case <-time.After(time.Duration(waitTime)): + case <-ctx.Done(): + return + } intervalCloseTicker := time.NewTicker(duration) defer intervalCloseTicker.Stop() diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index cf4287c87a..6c5e97243f 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -857,7 +857,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se var buffer bytes.Buffer if err := canvas.Render(chart.PNG, &buffer); err != nil { log.WithError(err).Errorf("cannot render indicators in drift") - reply.Message(fmt.Sprintf("[error] cannot render indicators in drift: %v", err)) return } bbgo.SendPhoto(&buffer) @@ -870,7 +869,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se var buffer bytes.Buffer if err := canvas.Render(chart.PNG, &buffer); err != nil { log.WithError(err).Errorf("cannot render pnl in drift") - reply.Message(fmt.Sprintf("[error] cannot render pnl in drift: %v", err)) return } bbgo.SendPhoto(&buffer) @@ -883,7 +881,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se var buffer bytes.Buffer if err := canvas.Render(chart.PNG, &buffer); err != nil { log.WithError(err).Errorf("cannot render cumpnl in drift") - reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) return } bbgo.SendPhoto(&buffer) diff --git a/pkg/strategy/elliottwave/draw.go b/pkg/strategy/elliottwave/draw.go index 6378839dcc..fa14274244 100644 --- a/pkg/strategy/elliottwave/draw.go +++ b/pkg/strategy/elliottwave/draw.go @@ -22,7 +22,6 @@ func (s *Strategy) InitDrawCommands(store *bbgo.SerialMarketDataStore, profit, c var buffer bytes.Buffer if err := canvas.Render(chart.PNG, &buffer); err != nil { log.WithError(err).Errorf("cannot render indicators in ewo") - reply.Message(fmt.Sprintf("[error] cannot render indicators in ewo: %v", err)) return } bbgo.SendPhoto(&buffer) @@ -34,7 +33,6 @@ func (s *Strategy) InitDrawCommands(store *bbgo.SerialMarketDataStore, profit, c var buffer bytes.Buffer if err := canvas.Render(chart.PNG, &buffer); err != nil { log.WithError(err).Errorf("cannot render pnl in ewo") - reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err)) return } bbgo.SendPhoto(&buffer) @@ -46,7 +44,6 @@ func (s *Strategy) InitDrawCommands(store *bbgo.SerialMarketDataStore, profit, c var buffer bytes.Buffer if err := canvas.Render(chart.PNG, &buffer); err != nil { log.WithError(err).Errorf("cannot render cumpnl in ewo") - reply.Message(fmt.Sprintf("[error] canot render cumpnl in ewo: %v", err)) return } bbgo.SendPhoto(&buffer) From b2e867e51c3c8da998cdc94e6dc4457670e8e9ca Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 27 Oct 2022 17:31:39 +0900 Subject: [PATCH 0024/1392] fix: unlimited length of indicators, add draw elapsed to drift --- config/driftBTC.yaml | 1 + pkg/indicator/atr.go | 6 + pkg/indicator/ewma.go | 4 +- pkg/indicator/rma.go | 6 + pkg/indicator/sma.go | 3 + pkg/indicator/stddev.go | 6 + pkg/strategy/drift/draw.go | 175 ++++++++++++++++++++++ pkg/strategy/drift/strategy.go | 236 +++++++++--------------------- pkg/strategy/elliottwave/draw.go | 2 +- pkg/strategy/ewoDgtrd/strategy.go | 4 +- 10 files changed, 271 insertions(+), 172 deletions(-) create mode 100644 pkg/strategy/drift/draw.go diff --git a/config/driftBTC.yaml b/config/driftBTC.yaml index 898bbd4a2a..04478258fb 100644 --- a/config/driftBTC.yaml +++ b/config/driftBTC.yaml @@ -68,6 +68,7 @@ exchangeStrategies: graphPNLDeductFee: false graphPNLPath: "./pnl.png" graphCumPNLPath: "./cumpnl.png" + graphElapsedPath: "./elapsed.png" #exits: # - roiStopLoss: # percentage: 0.35% diff --git a/pkg/indicator/atr.go b/pkg/indicator/atr.go index 3a9b853656..0d5b264cbf 100644 --- a/pkg/indicator/atr.go +++ b/pkg/indicator/atr.go @@ -8,6 +8,9 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +const MaxNumOfATR = 1000 +const MaxNumOfATRTruncateSize = 500 + //go:generate callbackgen -type ATR type ATR struct { types.SeriesBase @@ -73,6 +76,9 @@ func (inc *ATR) Update(high, low, cloze float64) { inc.RMA.Update(trueRange) atr := inc.RMA.Last() inc.PercentageVolatility.Push(atr / cloze) + if len(inc.PercentageVolatility) > MaxNumOfATR { + inc.PercentageVolatility = inc.PercentageVolatility[MaxNumOfATRTruncateSize-1:] + } } func (inc *ATR) Last() float64 { diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index 84b92fbb5e..533c27bd28 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -8,8 +8,8 @@ import ( ) // These numbers should be aligned with bbgo MaxNumOfKLines and MaxNumOfKLinesTruncate -const MaxNumOfEWMA = 5_000 -const MaxNumOfEWMATruncateSize = 100 +const MaxNumOfEWMA = 1_000 +const MaxNumOfEWMATruncateSize = 500 //go:generate callbackgen -type EWMA type EWMA struct { diff --git a/pkg/indicator/rma.go b/pkg/indicator/rma.go index fa47d94d98..63d7514ed8 100644 --- a/pkg/indicator/rma.go +++ b/pkg/indicator/rma.go @@ -7,6 +7,9 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +const MaxNumOfRMA = 1000 +const MaxNumOfRMATruncateSize = 500 + // Running Moving Average // Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/overlap/rma.py#L5 // Refer: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html#pandas-dataframe-ewm @@ -62,6 +65,9 @@ func (inc *RMA) Update(x float64) { } inc.Values.Push(inc.tmp) + if len(inc.Values) > MaxNumOfRMA { + inc.Values = inc.Values[MaxNumOfRMATruncateSize-1:] + } } func (inc *RMA) Last() float64 { diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index 279e112806..508a0ce7eb 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -65,6 +65,9 @@ func (inc *SMA) Update(value float64) { } inc.Values.Push(types.Mean(inc.rawValues)) + if len(inc.Values) > MaxNumOfSMA { + inc.Values = inc.Values[MaxNumOfSMATruncateSize-1:] + } } func (inc *SMA) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { diff --git a/pkg/indicator/stddev.go b/pkg/indicator/stddev.go index 8811fb208e..a63aa89ef2 100644 --- a/pkg/indicator/stddev.go +++ b/pkg/indicator/stddev.go @@ -7,6 +7,9 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +const MaxNumOfStdev = 600 +const MaxNumOfStdevTruncateSize = 300 + //go:generate callbackgen -type StdDev type StdDev struct { types.SeriesBase @@ -49,6 +52,9 @@ func (inc *StdDev) Update(value float64) { var std = inc.rawValues.Stdev() inc.Values.Push(std) + if len(inc.Values) > MaxNumOfStdev { + inc.Values = inc.Values[MaxNumOfStdevTruncateSize-1:] + } } func (inc *StdDev) PushK(k types.KLine) { diff --git a/pkg/strategy/drift/draw.go b/pkg/strategy/drift/draw.go new file mode 100644 index 0000000000..61fcc667f2 --- /dev/null +++ b/pkg/strategy/drift/draw.go @@ -0,0 +1,175 @@ +package drift + +import ( + "bytes" + "fmt" + "os" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/types" + "github.com/wcharczuk/go-chart/v2" +) + +func (s *Strategy) InitDrawCommands(profit, cumProfit types.Series) { + bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { + go func() { + canvas := s.DrawIndicators(s.frameKLine.StartTime) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render indicators in drift") + return + } + bbgo.SendPhoto(&buffer) + }() + }) + + bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { + go func() { + canvas := s.DrawPNL(profit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in drift") + return + } + bbgo.SendPhoto(&buffer) + }() + }) + + bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { + go func() { + canvas := s.DrawCumPNL(cumProfit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render cumpnl in drift") + return + } + bbgo.SendPhoto(&buffer) + }() + }) + + bbgo.RegisterCommand("/elapsed", "Draw Elapsed time for handlers for each kline close event", func(reply interact.Reply) { + go func() { + canvas := s.DrawElapsed() + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render elapsed in drift") + return + } + bbgo.SendPhoto(&buffer) + }() + }) +} + +func (s *Strategy) DrawIndicators(time types.Time) *types.Canvas { + canvas := types.NewCanvas(s.InstanceID(), s.Interval) + Length := s.priceLines.Length() + if Length > 300 { + Length = 300 + } + log.Infof("draw indicators with %d data", Length) + mean := s.priceLines.Mean(Length) + highestPrice := s.priceLines.Minus(mean).Abs().Highest(Length) + highestDrift := s.drift.Abs().Highest(Length) + hi := s.drift.drift.Abs().Highest(Length) + ratio := highestPrice / highestDrift + + // canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length) + canvas.Plot("ma", s.ma, time, Length) + // canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length) + fmt.Printf("%f %f\n", highestPrice, hi) + + canvas.Plot("trend", s.trendLine, time, Length) + canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length) + canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length) + canvas.Plot("zero", types.NumberSeries(mean), time, Length) + canvas.Plot("price", s.priceLines, time, Length) + return canvas +} + +func (s *Strategy) DrawPNL(profit types.Series) *types.Canvas { + canvas := types.NewCanvas(s.InstanceID()) + log.Errorf("pnl Highest: %f, Lowest: %f", types.Highest(profit, profit.Length()), types.Lowest(profit, profit.Length())) + length := profit.Length() + if s.GraphPNLDeductFee { + canvas.PlotRaw("pnl % (with Fee Deducted)", profit, length) + } else { + canvas.PlotRaw("pnl %", profit, length) + } + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + canvas.PlotRaw("1", types.NumberSeries(1), length) + return canvas +} + +func (s *Strategy) DrawCumPNL(cumProfit types.Series) *types.Canvas { + canvas := types.NewCanvas(s.InstanceID()) + canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length()) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + return canvas +} + +func (s *Strategy) DrawElapsed() *types.Canvas { + canvas := types.NewCanvas(s.InstanceID()) + canvas.PlotRaw("elapsed time(ms)", s.elapsed, s.elapsed.Length()) + return canvas +} + +func (s *Strategy) Draw(time types.Time, profit types.Series, cumProfit types.Series) { + canvas := s.DrawIndicators(time) + f, err := os.Create(s.CanvasPath) + if err != nil { + log.WithError(err).Errorf("cannot create on %s", s.CanvasPath) + return + } + if err := canvas.Render(chart.PNG, f); err != nil { + log.WithError(err).Errorf("cannot render in drift") + } + f.Close() + + canvas = s.DrawPNL(profit) + f, err = os.Create(s.GraphPNLPath) + if err != nil { + log.WithError(err).Errorf("open pnl") + return + } + if err := canvas.Render(chart.PNG, f); err != nil { + log.WithError(err).Errorf("render pnl") + } + f.Close() + + canvas = s.DrawCumPNL(cumProfit) + f, err = os.Create(s.GraphCumPNLPath) + if err != nil { + log.WithError(err).Errorf("open cumpnl") + return + } + if err := canvas.Render(chart.PNG, f); err != nil { + log.WithError(err).Errorf("render cumpnl") + } + f.Close() + + canvas = s.DrawElapsed() + f, err = os.Create(s.GraphElapsedPath) + if err != nil { + log.WithError(err).Errorf("open elapsed") + return + } + if err := canvas.Render(chart.PNG, f); err != nil { + log.WithError(err).Errorf("render elapsed") + } + f.Close() +} diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 6c5e97243f..2ebe07915e 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -12,7 +12,6 @@ import ( "time" "github.com/sirupsen/logrus" - "github.com/wcharczuk/go-chart/v2" "go.uber.org/multierr" "github.com/c9s/bbgo/pkg/bbgo" @@ -68,6 +67,7 @@ type Strategy struct { p *types.Position MinInterval types.Interval `json:"MinInterval"` // minimum interval referred for doing stoploss/trailing exists and updating highest/lowest + elapsed *types.Queue priceLines *types.Queue trendLine types.UpdatableSeriesExtend ma types.UpdatableSeriesExtend @@ -78,8 +78,8 @@ type Strategy struct { midPrice fixedpoint.Value // the midPrice is the average of bestBid and bestAsk in public orderbook lock sync.RWMutex `ignore:"true"` // lock for midPrice positionLock sync.RWMutex `ignore:"true"` // lock for highest/lowest and p + pendingLock sync.Mutex `ignore:"true"` startTime time.Time // trading start time - counter int // number of MinInterval since startTime maxCounterBuyCanceled int // the largest counter of the order on the buy side been cancelled. meaning the latest cancelled buy order. maxCounterSellCanceled int // the largest counter of the order on the sell side been cancelled. meaning the latest cancelled sell order. orderPendingCounter map[uint64]int // records the timepoint when the orders are created, using the counter at the time. @@ -114,10 +114,11 @@ type Strategy struct { // This is not related to trade but for statistics graph generation // Will deduct fee in percentage from every trade GraphPNLDeductFee bool `json:"graphPNLDeductFee"` - CanvasPath string `json:"canvasPath"` // backtest related. the path to store the indicator graph - GraphPNLPath string `json:"graphPNLPath"` // backtest related. the path to store the pnl % graph per trade graph. - GraphCumPNLPath string `json:"graphCumPNLPath"` // backtest related. the path to store the asset changes in graph - GenerateGraph bool `json:"generateGraph"` // whether to generate graph when shutdown + CanvasPath string `json:"canvasPath"` // backtest related. the path to store the indicator graph + GraphPNLPath string `json:"graphPNLPath"` // backtest related. the path to store the pnl % graph per trade graph. + GraphCumPNLPath string `json:"graphCumPNLPath"` // backtest related. the path to store the asset changes in graph + GraphElapsedPath string `json:"graphElapsedPath"` // the path to store the elapsed time in ms + GenerateGraph bool `json:"generateGraph"` // whether to generate graph when shutdown ExitMethods bbgo.ExitMethodSet `json:"exits"` Session *bbgo.ExchangeSession @@ -264,7 +265,7 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { return nil } -func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, error) { +func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64, syscounter int) (int, error) { nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders() if len(nonTraded) > 0 { if len(nonTraded) > 1 { @@ -276,17 +277,21 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e if order.Status != types.OrderStatusNew && order.Status != types.OrderStatusPartiallyFilled { continue } - log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.counter) - if s.counter-s.orderPendingCounter[order.OrderID] > s.PendingMinInterval { + s.pendingLock.Lock() + counter := s.orderPendingCounter[order.OrderID] + s.pendingLock.Unlock() + + log.Warnf("%v | counter: %d, system: %d", order, counter, syscounter) + if syscounter-counter > s.PendingMinInterval { toCancel = true } else if order.Side == types.SideTypeBuy { // 75% of the probability - if order.Price.Float64()+s.stdevHigh.Last()*2 <= pricef { + if order.Price.Float64()+atr*2 <= pricef { toCancel = true } } else if order.Side == types.SideTypeSell { // 75% of the probability - if order.Price.Float64()-s.stdevLow.Last()*2 >= pricef { + if order.Price.Float64()-atr*2 >= pricef { toCancel = true } } else { @@ -297,16 +302,19 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e err := s.GeneralOrderExecutor.CancelNoWait(ctx) // TODO: clean orderPendingCounter on cancel/trade for _, order := range nonTraded { + s.pendingLock.Lock() + counter := s.orderPendingCounter[order.OrderID] + delete(s.orderPendingCounter, order.OrderID) + s.pendingLock.Unlock() if order.Side == types.SideTypeSell { - if s.maxCounterSellCanceled < s.orderPendingCounter[order.OrderID] { - s.maxCounterSellCanceled = s.orderPendingCounter[order.OrderID] + if s.maxCounterSellCanceled < counter { + s.maxCounterSellCanceled = counter } } else { - if s.maxCounterBuyCanceled < s.orderPendingCounter[order.OrderID] { - s.maxCounterBuyCanceled = s.orderPendingCounter[order.OrderID] + if s.maxCounterBuyCanceled < counter { + s.maxCounterBuyCanceled = counter } } - delete(s.orderPendingCounter, order.OrderID) } log.Warnf("cancel all %v", err) return 0, err @@ -390,102 +398,6 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { } -func (s *Strategy) DrawIndicators(time types.Time) *types.Canvas { - canvas := types.NewCanvas(s.InstanceID(), s.Interval) - Length := s.priceLines.Length() - if Length > 300 { - Length = 300 - } - log.Infof("draw indicators with %d data", Length) - mean := s.priceLines.Mean(Length) - highestPrice := s.priceLines.Minus(mean).Abs().Highest(Length) - highestDrift := s.drift.Abs().Highest(Length) - hi := s.drift.drift.Abs().Highest(Length) - ratio := highestPrice / highestDrift - - // canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length) - canvas.Plot("ma", s.ma, time, Length) - // canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length) - fmt.Printf("%f %f\n", highestPrice, hi) - - canvas.Plot("trend", s.trendLine, time, Length) - canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length) - canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length) - canvas.Plot("zero", types.NumberSeries(mean), time, Length) - canvas.Plot("price", s.priceLines, time, Length) - return canvas -} - -func (s *Strategy) DrawPNL(profit types.Series) *types.Canvas { - canvas := types.NewCanvas(s.InstanceID()) - log.Errorf("pnl Highest: %f, Lowest: %f", types.Highest(profit, profit.Length()), types.Lowest(profit, profit.Length())) - length := profit.Length() - if s.GraphPNLDeductFee { - canvas.PlotRaw("pnl % (with Fee Deducted)", profit, length) - } else { - canvas.PlotRaw("pnl %", profit, length) - } - canvas.YAxis = chart.YAxis{ - ValueFormatter: func(v interface{}) string { - if vf, isFloat := v.(float64); isFloat { - return fmt.Sprintf("%.4f", vf) - } - return "" - }, - } - canvas.PlotRaw("1", types.NumberSeries(1), length) - return canvas -} - -func (s *Strategy) DrawCumPNL(cumProfit types.Series) *types.Canvas { - canvas := types.NewCanvas(s.InstanceID()) - canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length()) - canvas.YAxis = chart.YAxis{ - ValueFormatter: func(v interface{}) string { - if vf, isFloat := v.(float64); isFloat { - return fmt.Sprintf("%.4f", vf) - } - return "" - }, - } - return canvas -} - -func (s *Strategy) Draw(time types.Time, profit types.Series, cumProfit types.Series) { - canvas := s.DrawIndicators(time) - f, err := os.Create(s.CanvasPath) - if err != nil { - log.WithError(err).Errorf("cannot create on %s", s.CanvasPath) - return - } - defer f.Close() - if err := canvas.Render(chart.PNG, f); err != nil { - log.WithError(err).Errorf("cannot render in drift") - } - - canvas = s.DrawPNL(profit) - f, err = os.Create(s.GraphPNLPath) - if err != nil { - log.WithError(err).Errorf("open pnl") - return - } - defer f.Close() - if err := canvas.Render(chart.PNG, f); err != nil { - log.WithError(err).Errorf("render pnl") - } - - canvas = s.DrawCumPNL(cumProfit) - f, err = os.Create(s.GraphCumPNLPath) - if err != nil { - log.WithError(err).Errorf("open cumpnl") - return - } - defer f.Close() - if err := canvas.Render(chart.PNG, f); err != nil { - log.WithError(err).Errorf("render cumpnl") - } -} - // Sending new rebalance orders cost too much. // Modify the position instead to expect the strategy itself rebalance on Close func (s *Strategy) Rebalance(ctx context.Context) { @@ -548,7 +460,7 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) } -func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) { +func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine, counter int) { s.klineMin.Set(&kline) if s.Status != types.StrategyStatusRunning { return @@ -571,7 +483,7 @@ func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) { numPending := 0 var err error - if numPending, err = s.smartCancel(ctx, pricef, atr); err != nil { + if numPending, err = s.smartCancel(ctx, pricef, atr, counter); err != nil { log.WithError(err).Errorf("cannot cancel orders") return } @@ -589,28 +501,37 @@ func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) { } } -func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { +func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter int) { + start := time.Now() + defer func() { + end := time.Now() + elapsed := end.Sub(start) + s.elapsed.Update(float64(elapsed) / 1000000) + }() s.frameKLine.Set(&kline) source := s.GetSource(&kline) sourcef := source.Float64() + s.priceLines.Update(sourcef) - s.ma.Update(sourcef) + //s.ma.Update(sourcef) s.trendLine.Update(sourcef) - s.drift.Update(sourcef, kline.Volume.Abs().Float64()) + s.drift.Update(sourcef, kline.Volume.Abs().Float64()) s.atr.PushK(kline) - atr := s.atr.Last() - price := s.getLastPrice() + + price := kline.Close //s.getLastPrice() pricef := price.Float64() lowf := math.Min(kline.Low.Float64(), pricef) highf := math.Max(kline.High.Float64(), pricef) - lowdiff := s.ma.Last() - lowf + lowdiff := pricef - lowf s.stdevLow.Update(lowdiff) - highdiff := highf - s.ma.Last() + highdiff := highf - pricef s.stdevHigh.Update(highdiff) + drift := s.drift.Array(2) + if len(drift) < 2 || len(drift) < s.PredictOffset { return } @@ -623,7 +544,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { return } - log.Infof("highdiff: %3.2f ma: %.2f, open: %8v, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(), s.ma.Last(), kline.Open, kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime) + log.Infof("highdiff: %3.2f open: %8v, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(), kline.Open, kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime) s.positionLock.Lock() if s.lowestPrice > 0 && lowf < s.lowestPrice { @@ -667,14 +588,14 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { if exitCondition || longCondition || shortCondition { var err error var hold int - if hold, err = s.smartCancel(ctx, sourcef, atr); err != nil { + if hold, err = s.smartCancel(ctx, pricef, atr, counter); err != nil { log.WithError(err).Errorf("cannot cancel orders") } if hold > 0 { return } } else { - if _, err := s.smartCancel(ctx, sourcef, atr); err != nil { + if _, err := s.smartCancel(ctx, pricef, atr, counter); err != nil { log.WithError(err).Errorf("cannot cancel orders") } return @@ -692,11 +613,12 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { opt.Long = true opt.LimitOrder = true // force to use market taker - if s.counter-s.maxCounterBuyCanceled <= 1 { + if counter-s.maxCounterBuyCanceled <= s.PendingMinInterval { opt.LimitOrder = false } opt.Price = source opt.Tags = []string{"long"} + createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) if err != nil { errs := filterErrors(multierr.Errors(err)) @@ -706,9 +628,16 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { } return } + log.Infof("orders %v", createdOrders) if createdOrders != nil { - s.orderPendingCounter[createdOrders[0].OrderID] = s.counter + for _, o := range createdOrders { + if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled { + s.pendingLock.Lock() + s.orderPendingCounter[o.OrderID] = counter + s.pendingLock.Unlock() + } + } } return } @@ -724,7 +653,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { opt.Short = true opt.Price = source opt.LimitOrder = true - if s.counter-s.maxCounterSellCanceled <= 1 { + if counter-s.maxCounterSellCanceled <= s.PendingMinInterval { opt.LimitOrder = false } opt.Tags = []string{"short"} @@ -738,7 +667,13 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { } log.Infof("orders %v", createdOrders) if createdOrders != nil { - s.orderPendingCounter[createdOrders[0].OrderID] = s.counter + for _, o := range createdOrders { + if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled { + s.pendingLock.Lock() + s.orderPendingCounter[o.OrderID] = counter + s.pendingLock.Unlock() + } + } } return } @@ -787,7 +722,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.GeneralOrderExecutor.Bind() s.orderPendingCounter = make(map[uint64]int) - s.counter = 0 // Exit methods from config for _, method := range s.ExitMethods { @@ -845,47 +779,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.frameKLine = &types.KLine{} s.klineMin = &types.KLine{} s.priceLines = types.NewQueue(300) + s.elapsed = types.NewQueue(60000) s.initTickerFunctions(ctx) s.startTime = s.Environment.StartTime() s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, s.startTime)) s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, s.startTime)) - bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { - go func() { - canvas := s.DrawIndicators(s.frameKLine.StartTime) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render indicators in drift") - return - } - bbgo.SendPhoto(&buffer) - }() - }) - - bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { - go func() { - canvas := s.DrawPNL(&profit) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render pnl in drift") - return - } - bbgo.SendPhoto(&buffer) - }() - }) - - bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { - go func() { - canvas := s.DrawCumPNL(&cumProfit) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render cumpnl in drift") - return - } - bbgo.SendPhoto(&buffer) - }() - }) + s.InitDrawCommands(&profit, &cumProfit) bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) { var buffer bytes.Buffer @@ -922,12 +823,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } + //var lastK types.KLine store.OnKLineClosed(func(kline types.KLine) { - s.counter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) / s.MinInterval.Milliseconds() + counter := int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) / s.MinInterval.Milliseconds() if kline.Interval == s.Interval { - s.klineHandler(ctx, kline) + s.klineHandler(ctx, kline, counter) } else if kline.Interval == s.MinInterval { - s.klineHandlerMin(ctx, kline) + s.klineHandlerMin(ctx, kline, counter) } }) diff --git a/pkg/strategy/elliottwave/draw.go b/pkg/strategy/elliottwave/draw.go index fa14274244..20fc8a0f62 100644 --- a/pkg/strategy/elliottwave/draw.go +++ b/pkg/strategy/elliottwave/draw.go @@ -16,7 +16,7 @@ func (s *Strategy) InitDrawCommands(store *bbgo.SerialMarketDataStore, profit, c go func() { canvas := s.DrawIndicators(store) if canvas == nil { - reply.Message("cannot render indicators") + reply.Send("cannot render indicators") return } var buffer bytes.Buffer diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 6af38bc119..e13b84b0e1 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -145,7 +145,7 @@ func (inc *CCISTOCH) Update(cloze float64) { func (inc *CCISTOCH) BuySignal() bool { hasGrey := false - for i := 0; i < len(inc.ma.Values); i++ { + for i := 0; i < inc.ma.Values.Length(); i++ { v := inc.ma.Index(i) if v > inc.filterHigh { return false @@ -161,7 +161,7 @@ func (inc *CCISTOCH) BuySignal() bool { func (inc *CCISTOCH) SellSignal() bool { hasGrey := false - for i := 0; i < len(inc.ma.Values); i++ { + for i := 0; i < inc.ma.Values.Length(); i++ { v := inc.ma.Index(i) if v < inc.filterLow { return false From 532f3c11e7e1cb8dbde2b173d04a53dda38935a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Fri, 28 Oct 2022 15:15:55 +0800 Subject: [PATCH 0025/1392] fix backtest --- config/rebalance.yaml | 32 ++++++++++++++++++++++-------- pkg/strategy/rebalance/strategy.go | 21 ++++++++++---------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/config/rebalance.yaml b/config/rebalance.yaml index d18ce1819c..340c02dc4e 100644 --- a/config/rebalance.yaml +++ b/config/rebalance.yaml @@ -8,18 +8,34 @@ notifications: orderUpdate: true submitOrder: true +backtest: + startTime: "2022-01-01" + endTime: "2022-10-01" + symbols: + - BTCUSDT + - ETHUSDT + - MAXUSDT + account: + max: + makerFeeRate: 0.075% + takerFeeRate: 0.075% + balances: + BTC: 0.0 + ETH: 0.0 + MAX: 0.0 + USDT: 10000.0 + exchangeStrategies: - on: max rebalance: interval: 1d - quoteCurrency: TWD + quoteCurrency: USDT targetWeights: - BTC: 40% - ETH: 20% - MAX: 10% - USDT: 15% - TWD: 15% - threshold: 2% + BTC: 50% + ETH: 25% + MAX: 15% + USDT: 10% + threshold: 1% # max amount to buy or sell per order - maxAmount: 10_000 + maxAmount: 1_000 dryRun: false diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index fba79f2968..cb1c395593 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -66,7 +66,9 @@ func (s *Strategy) Validate() error { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.symbols()[0], types.SubscribeOptions{Interval: s.Interval}) + for _, symbol := range s.symbols() { + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: s.Interval}) + } } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { @@ -113,21 +115,20 @@ func (s *Strategy) rebalance(ctx context.Context, orderExecutor bbgo.OrderExecut func (s *Strategy) prices(ctx context.Context, session *bbgo.ExchangeSession) types.ValueMap { m := make(types.ValueMap) - - tickers, err := session.Exchange.QueryTickers(ctx, s.symbols()...) - if err != nil { - log.WithError(err).Error("failed to query tickers") - return nil - } - for currency := range s.TargetWeights { if currency == s.QuoteCurrency { m[s.QuoteCurrency] = fixedpoint.One continue } - m[currency] = tickers[currency+s.QuoteCurrency].Last - } + ticker, err := session.Exchange.QueryTicker(ctx, currency+s.QuoteCurrency) + if err != nil { + log.WithError(err).Error("failed to query tickers") + return nil + } + + m[currency] = ticker.Last + } return m } From a5555cf35adc679ee9a497dd4247960c16e3380a Mon Sep 17 00:00:00 2001 From: grorge Date: Fri, 28 Oct 2022 17:56:07 +0800 Subject: [PATCH 0026/1392] feat: cancel order for exit roi take profit and loss --- pkg/bbgo/exit_roi_stop_loss.go | 8 ++++++-- pkg/bbgo/exit_roi_take_profit.go | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/exit_roi_stop_loss.go b/pkg/bbgo/exit_roi_stop_loss.go index 6060022182..b19832a08d 100644 --- a/pkg/bbgo/exit_roi_stop_loss.go +++ b/pkg/bbgo/exit_roi_stop_loss.go @@ -8,8 +8,9 @@ import ( ) type RoiStopLoss struct { - Symbol string - Percentage fixedpoint.Value `json:"percentage"` + Symbol string + Percentage fixedpoint.Value `json:"percentage"` + CancelActiveOrders bool `json:"cancelActiveOrders"` session *ExchangeSession orderExecutor *GeneralOrderExecutor @@ -50,6 +51,9 @@ func (s *RoiStopLoss) checkStopPrice(closePrice fixedpoint.Value, position *type if roi.Compare(s.Percentage.Neg()) < 0 { // stop loss Notify("[RoiStopLoss] %s stop loss triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Neg().Percentage(), closePrice.Float64()) + if s.CancelActiveOrders { + _ = s.orderExecutor.GracefulCancel(context.Background()) + } _ = s.orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "roiStopLoss") return } diff --git a/pkg/bbgo/exit_roi_take_profit.go b/pkg/bbgo/exit_roi_take_profit.go index d6af7d7713..d0d4e8e72a 100644 --- a/pkg/bbgo/exit_roi_take_profit.go +++ b/pkg/bbgo/exit_roi_take_profit.go @@ -9,8 +9,9 @@ import ( // RoiTakeProfit force takes the profit by the given ROI percentage. type RoiTakeProfit struct { - Symbol string `json:"symbol"` - Percentage fixedpoint.Value `json:"percentage"` + Symbol string `json:"symbol"` + Percentage fixedpoint.Value `json:"percentage"` + CancelActiveOrders bool `json:"cancelActiveOrders"` session *ExchangeSession orderExecutor *GeneralOrderExecutor @@ -36,6 +37,9 @@ func (s *RoiTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrd if roi.Compare(s.Percentage) >= 0 { // stop loss Notify("[RoiTakeProfit] %s take profit is triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Percentage(), kline.Close.Float64()) + if s.CancelActiveOrders { + _ = s.orderExecutor.GracefulCancel(context.Background()) + } _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "roiTakeProfit") return } From 5b7712503fd59ccaf33a1443ffbd3d64cf42c17a Mon Sep 17 00:00:00 2001 From: zenix Date: Mon, 31 Oct 2022 11:05:55 +0900 Subject: [PATCH 0027/1392] fix: pendingLock on orderPendingCounter delete --- pkg/strategy/drift/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 2ebe07915e..8d81a51ebf 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -743,7 +743,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.GeneralOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _profit, _netProfit fixedpoint.Value) { s.p.AddTrade(trade) price := trade.Price.Float64() + s.pendingLock.Lock() delete(s.orderPendingCounter, trade.OrderID) + s.pendingLock.Unlock() if s.buyPrice > 0 { profit.Update(modify(price / s.buyPrice)) From 3695644f97984a011461ddf9ab4ebcc1870abee9 Mon Sep 17 00:00:00 2001 From: zenix Date: Mon, 31 Oct 2022 18:50:27 +0900 Subject: [PATCH 0028/1392] fix: capitalization of drift variable --- pkg/strategy/drift/draw.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/strategy/drift/draw.go b/pkg/strategy/drift/draw.go index 61fcc667f2..3ad7c377cb 100644 --- a/pkg/strategy/drift/draw.go +++ b/pkg/strategy/drift/draw.go @@ -63,27 +63,27 @@ func (s *Strategy) InitDrawCommands(profit, cumProfit types.Series) { func (s *Strategy) DrawIndicators(time types.Time) *types.Canvas { canvas := types.NewCanvas(s.InstanceID(), s.Interval) - Length := s.priceLines.Length() - if Length > 300 { - Length = 300 + length := s.priceLines.Length() + if length > 300 { + length = 300 } - log.Infof("draw indicators with %d data", Length) - mean := s.priceLines.Mean(Length) - highestPrice := s.priceLines.Minus(mean).Abs().Highest(Length) - highestDrift := s.drift.Abs().Highest(Length) - hi := s.drift.drift.Abs().Highest(Length) + log.Infof("draw indicators with %d data", length) + mean := s.priceLines.Mean(length) + highestPrice := s.priceLines.Minus(mean).Abs().Highest(length) + highestDrift := s.drift.Abs().Highest(length) + hi := s.drift.drift.Abs().Highest(length) ratio := highestPrice / highestDrift - // canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length) - canvas.Plot("ma", s.ma, time, Length) - // canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length) + // canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, length) + canvas.Plot("ma", s.ma, time, length) + // canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, length) fmt.Printf("%f %f\n", highestPrice, hi) - canvas.Plot("trend", s.trendLine, time, Length) - canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length) - canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length) - canvas.Plot("zero", types.NumberSeries(mean), time, Length) - canvas.Plot("price", s.priceLines, time, Length) + canvas.Plot("trend", s.trendLine, time, length) + canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, length) + canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, length) + canvas.Plot("zero", types.NumberSeries(mean), time, length) + canvas.Plot("price", s.priceLines, time, length) return canvas } From 7b9edd04562b12afa816ddb7f42f1a7c6a673c7b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Nov 2022 12:25:34 +0800 Subject: [PATCH 0029/1392] all: rename cancelNoWait to fastCancel --- pkg/bbgo/activeorderbook.go | 4 +++- pkg/bbgo/order_executor_general.go | 13 +++++++------ pkg/strategy/drift/strategy.go | 8 ++++---- pkg/strategy/elliottwave/strategy.go | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 3196943b89..659223a581 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -93,7 +93,7 @@ func (b *ActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout ti } // Cancel orders without confirmation -func (b *ActiveOrderBook) CancelNoWait(ctx context.Context, ex types.Exchange, orders ...types.Order) error { +func (b *ActiveOrderBook) FastCancel(ctx context.Context, ex types.Exchange, orders ...types.Order) error { // if no orders are given, set to cancelAll if len(orders) == 0 { orders = b.Orders() @@ -109,11 +109,13 @@ func (b *ActiveOrderBook) CancelNoWait(ctx context.Context, ex types.Exchange, o if IsBackTesting { return ex.CancelOrders(context.Background(), orders...) } + log.Debugf("[ActiveOrderBook] no wait cancelling %s orders...", b.Symbol) // since ctx might be canceled, we should use background context here if err := ex.CancelOrders(context.Background(), orders...); err != nil { log.WithError(err).Errorf("[ActiveOrderBook] no wait can not cancel %s orders", b.Symbol) } + for _, o := range orders { b.Remove(o) } diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 1c7072246e..6f253eab48 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -362,6 +362,7 @@ func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context if activeOrders.NumOfOrders() == 0 { return nil } + if err := activeOrders.GracefulCancel(ctx, e.session.Exchange, orders...); err != nil { // Retry once if err = activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil { @@ -373,12 +374,12 @@ func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context return nil } -// CancelActiveOrderBookNoWait cancels the orders from the active orderbook without waiting -func (e *GeneralOrderExecutor) CancelActiveOrderBookNoWait(ctx context.Context, activeOrders *ActiveOrderBook, orders ...types.Order) error { +// FastCancelActiveOrderBook cancels the orders from the active orderbook without waiting +func (e *GeneralOrderExecutor) FastCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook, orders ...types.Order) error { if activeOrders.NumOfOrders() == 0 { return nil } - if err := activeOrders.CancelNoWait(ctx, e.session.Exchange, orders...); err != nil { + if err := activeOrders.FastCancel(ctx, e.session.Exchange, orders...); err != nil { return fmt.Errorf("cancel order error: %w", err) } return nil @@ -389,9 +390,9 @@ func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context, orders ...typ return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders, orders...) } -// CancelNoWait cancels all active maker orders if orders is not given, otherwise cancel the given orders -func (e *GeneralOrderExecutor) CancelNoWait(ctx context.Context, orders ...types.Order) error { - return e.CancelActiveOrderBookNoWait(ctx, e.activeMakerOrders, orders...) +// FastCancel cancels all active maker orders if orders is not given, otherwise cancel the given orders +func (e *GeneralOrderExecutor) FastCancel(ctx context.Context, orders ...types.Order) error { + return e.FastCancelActiveOrderBook(ctx, e.activeMakerOrders, orders...) } // ClosePosition closes the current position by a percentage. diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 8d81a51ebf..74782e2e97 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -299,7 +299,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64, syscoun } } if toCancel { - err := s.GeneralOrderExecutor.CancelNoWait(ctx) + err := s.GeneralOrderExecutor.FastCancel(ctx) // TODO: clean orderPendingCounter on cancel/trade for _, order := range nonTraded { s.pendingLock.Lock() @@ -514,14 +514,14 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter sourcef := source.Float64() s.priceLines.Update(sourcef) - //s.ma.Update(sourcef) + // s.ma.Update(sourcef) s.trendLine.Update(sourcef) s.drift.Update(sourcef, kline.Volume.Abs().Float64()) s.atr.PushK(kline) atr := s.atr.Last() - price := kline.Close //s.getLastPrice() + price := kline.Close // s.getLastPrice() pricef := price.Float64() lowf := math.Min(kline.Low.Float64(), pricef) highf := math.Max(kline.High.Float64(), pricef) @@ -825,7 +825,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } - //var lastK types.KLine + // var lastK types.KLine store.OnKLineClosed(func(kline types.KLine) { counter := int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) / s.MinInterval.Milliseconds() if kline.Interval == s.Interval { diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go index 40703017ad..3c42cfd6a9 100644 --- a/pkg/strategy/elliottwave/strategy.go +++ b/pkg/strategy/elliottwave/strategy.go @@ -211,7 +211,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int { panic("not supported side for the order") } if toCancel { - err := s.GeneralOrderExecutor.CancelNoWait(ctx, order) + err := s.GeneralOrderExecutor.FastCancel(ctx, order) if err == nil { delete(s.orderPendingCounter, order.OrderID) } else { From 1120821977fce73c09c2e93cce67d0a3eed26c01 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Nov 2022 12:27:36 +0800 Subject: [PATCH 0030/1392] add activeOrderBook.Symbol check --- pkg/bbgo/activeorderbook.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 659223a581..c05f4e89c1 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -92,7 +92,8 @@ func (b *ActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout ti } } -// Cancel orders without confirmation +// FastCancel cancels the orders without verification +// It calls the exchange cancel order api and then remove the orders from the active orderbook directly. func (b *ActiveOrderBook) FastCancel(ctx context.Context, ex types.Exchange, orders ...types.Order) error { // if no orders are given, set to cancelAll if len(orders) == 0 { @@ -100,11 +101,12 @@ func (b *ActiveOrderBook) FastCancel(ctx context.Context, ex types.Exchange, ord } else { // simple check on given input for _, o := range orders { - if o.Symbol != b.Symbol { - return errors.New("[ActiveOrderBook] cancel " + b.Symbol + " orderbook with different symbol: " + o.Symbol) + if b.Symbol != "" && o.Symbol != b.Symbol { + return errors.New("[ActiveOrderBook] cancel " + b.Symbol + " orderbook with different order symbol: " + o.Symbol) } } } + // optimize order cancel for back-testing if IsBackTesting { return ex.CancelOrders(context.Background(), orders...) From 8707fcaa978a5fe18a4e8fda1c67212bb3f70074 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Nov 2022 12:31:35 +0800 Subject: [PATCH 0031/1392] bbgo: drop FastCancelActiveOrderBook --- pkg/bbgo/order_executor_general.go | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 6f253eab48..5d1d238f12 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -2,12 +2,12 @@ package bbgo import ( "context" - "errors" "fmt" "strings" "sync/atomic" "time" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" "go.uber.org/multierr" @@ -374,17 +374,6 @@ func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context return nil } -// FastCancelActiveOrderBook cancels the orders from the active orderbook without waiting -func (e *GeneralOrderExecutor) FastCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook, orders ...types.Order) error { - if activeOrders.NumOfOrders() == 0 { - return nil - } - if err := activeOrders.FastCancel(ctx, e.session.Exchange, orders...); err != nil { - return fmt.Errorf("cancel order error: %w", err) - } - return nil -} - // GracefulCancel cancels all active maker orders if orders are not given, otherwise cancel all the given orders func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context, orders ...types.Order) error { return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders, orders...) @@ -392,7 +381,15 @@ func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context, orders ...typ // FastCancel cancels all active maker orders if orders is not given, otherwise cancel the given orders func (e *GeneralOrderExecutor) FastCancel(ctx context.Context, orders ...types.Order) error { - return e.FastCancelActiveOrderBook(ctx, e.activeMakerOrders, orders...) + if e.activeMakerOrders.NumOfOrders() == 0 { + return nil + } + + if err := e.activeMakerOrders.FastCancel(ctx, e.session.Exchange, orders...); err != nil { + return errors.Wrap(err, "fast cancel order error") + } + + return nil } // ClosePosition closes the current position by a percentage. From 9bf070172a2a83cb9371024f377d74ec7ac9a7a4 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Nov 2022 12:34:04 +0800 Subject: [PATCH 0032/1392] bbgo: remove extra order arguments from GracefulCancelActiveOrderBook to avoid confusion --- pkg/bbgo/order_executor_general.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 5d1d238f12..00a46bc90e 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -358,15 +358,15 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos } // GracefulCancelActiveOrderBook cancels the orders from the active orderbook. -func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook, orders ...types.Order) error { +func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook) error { if activeOrders.NumOfOrders() == 0 { return nil } - if err := activeOrders.GracefulCancel(ctx, e.session.Exchange, orders...); err != nil { + if err := activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil { // Retry once if err = activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil { - return fmt.Errorf("graceful cancel order error: %w", err) + return errors.Wrap(err, "graceful cancel error") } } @@ -376,7 +376,11 @@ func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context // GracefulCancel cancels all active maker orders if orders are not given, otherwise cancel all the given orders func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context, orders ...types.Order) error { - return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders, orders...) + if err := e.activeMakerOrders.GracefulCancel(ctx, e.session.Exchange, orders...); err != nil { + return errors.Wrap(err, "graceful cancel error") + } + + return nil } // FastCancel cancels all active maker orders if orders is not given, otherwise cancel the given orders From 3704f3f897665b440875925644e4d4f7dca0ef10 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Nov 2022 12:42:09 +0800 Subject: [PATCH 0033/1392] bbgo: emit sigchan when new order is added or an order is removed --- pkg/bbgo/activeorderbook.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index c05f4e89c1..664b302e19 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/sigchan" "github.com/c9s/bbgo/pkg/types" ) @@ -19,12 +20,17 @@ type ActiveOrderBook struct { Symbol string orders *types.SyncOrderMap filledCallbacks []func(o types.Order) + + // sig is the order update signal + // this signal will be emitted when a new order is added or removed. + C sigchan.Chan } func NewActiveOrderBook(symbol string) *ActiveOrderBook { return &ActiveOrderBook{ Symbol: symbol, orders: types.NewSyncOrderMap(), + C: sigchan.New(1), } } @@ -215,13 +221,19 @@ func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) { if b.Remove(order) { b.EmitFilled(order) } + b.C.Emit() + + case types.OrderStatusPartiallyFilled: + b.Update(order) - case types.OrderStatusPartiallyFilled, types.OrderStatusNew: + case types.OrderStatusNew: b.Update(order) + b.C.Emit() case types.OrderStatusCanceled, types.OrderStatusRejected: log.Debugf("[ActiveOrderBook] order status %s, removing order %s", order.Status, order) b.Remove(order) + b.C.Emit() default: log.Warnf("unhandled order status: %s", order.Status) From 04855b023ab024c8ed3c1726931c2c9221d188eb Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Nov 2022 12:55:13 +0800 Subject: [PATCH 0034/1392] bbgo: listen to both order signal and the wait time channel --- pkg/bbgo/activeorderbook.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 664b302e19..70d15ad43f 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -54,8 +54,13 @@ func (b *ActiveOrderBook) waitClear(ctx context.Context, order types.Order, wait timeoutC := time.After(timeout) for { - time.Sleep(waitTime) + select { + case <-time.After(waitTime): + case <-b.C: + } + clear := !b.Exists(order) + select { case <-timeoutC: return clear, nil @@ -71,18 +76,24 @@ func (b *ActiveOrderBook) waitClear(ctx context.Context, order types.Order, wait } } +// waitAllClear waits for the order book be clear (meaning every order is removed) +// if err != nil, it's the context error. func (b *ActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout time.Duration) (bool, error) { - numOfOrders := b.NumOfOrders() - clear := numOfOrders == 0 + clear := b.NumOfOrders() == 0 if clear { return clear, nil } timeoutC := time.After(timeout) for { - time.Sleep(waitTime) - numOfOrders = b.NumOfOrders() - clear = numOfOrders == 0 + select { + case <-time.After(waitTime): + case <-b.C: + } + + // update clear flag + clear = b.NumOfOrders() == 0 + select { case <-timeoutC: return clear, nil From 5467c8ef0105c71a8674bc37f7df1f05b0e8787a Mon Sep 17 00:00:00 2001 From: Austin Liu Date: Wed, 2 Nov 2022 16:48:50 +0800 Subject: [PATCH 0035/1392] strategy:irr rollback to original nirr and consume kline --- config/irr.yaml | 32 +++-- pkg/strategy/irr/neg_return_rate.go | 26 ++-- pkg/strategy/irr/strategy.go | 180 +++++++--------------------- 3 files changed, 75 insertions(+), 163 deletions(-) diff --git a/config/irr.yaml b/config/irr.yaml index 302980d289..ff91681781 100644 --- a/config/irr.yaml +++ b/config/irr.yaml @@ -10,23 +10,31 @@ sessions: binance: exchange: binance envVarPrefix: binance - max: - exchange: max - envVarPrefix: max - ftx: - exchange: ftx - envVarPrefix: ftx exchangeStrategies: - on: binance irr: - symbol: BTCBUSD - # in milliseconds(ms) - # must > 10 ms - hftInterval: 1000 - # qty per trade - quantity: 0.001 + symbol: BTCUSDT + interval: 1m + window: 10 + amount: 5000 # Draw pnl drawGraph: true graphPNLPath: "./pnl.png" graphCumPNLPath: "./cumpnl.png" + + +backtest: + startTime: "2022-01-01" + endTime: "2022-11-01" + symbols: + - BTCUSDT + sessions: [binance] +# syncSecKLines: true + accounts: + binance: + makerFeeRate: 0.0000 + takerFeeRate: 0.0000 + balances: + BTC: 0.0 + USDT: 5000 \ No newline at end of file diff --git a/pkg/strategy/irr/neg_return_rate.go b/pkg/strategy/irr/neg_return_rate.go index b3ec5edcc7..473f2ecddc 100644 --- a/pkg/strategy/irr/neg_return_rate.go +++ b/pkg/strategy/irr/neg_return_rate.go @@ -18,10 +18,12 @@ type NRR struct { types.SeriesBase RankingWindow int + delay bool + prices *types.Queue - Prices *types.Queue Values floats.Slice RankedValues floats.Slice + ReturnValues floats.Slice EndTime time.Time @@ -33,20 +35,22 @@ var _ types.SeriesExtend = &NRR{} func (inc *NRR) Update(openPrice, closePrice float64) { if inc.SeriesBase.Series == nil { inc.SeriesBase.Series = inc - inc.Prices = types.NewQueue(inc.Window) - } - inc.Prices.Update(closePrice) - if inc.Prices.Length() < inc.Window { - return + inc.prices = types.NewQueue(inc.Window) } + inc.prices.Update(closePrice) + // D0 - irr := openPrice - closePrice - // D1 - // -1*((inc.Prices.Last() / inc.Prices.Index(inc.Window-1)) - 1) + nirr := (openPrice - closePrice) / openPrice + irr := (closePrice - openPrice) / openPrice + if inc.prices.Length() >= inc.Window && inc.delay { + // D1 + nirr = -1 * ((inc.prices.Last() / inc.prices.Index(inc.Window-1)) - 1) + irr = (inc.prices.Last() / inc.prices.Index(inc.Window-1)) - 1 + } - inc.Values.Push(irr) // neg ret here + inc.Values.Push(nirr) // neg ret here inc.RankedValues.Push(inc.Rank(inc.RankingWindow).Last() / float64(inc.RankingWindow)) // ranked neg ret here - + inc.ReturnValues.Push(irr) } func (inc *NRR) Last() float64 { diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index a155eb3f8a..3fc4c8e9ad 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -5,8 +5,6 @@ import ( "fmt" "os" "sync" - "sync/atomic" - "time" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/data/tsv" @@ -14,6 +12,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" + "github.com/sirupsen/logrus" ) @@ -42,23 +41,14 @@ type Strategy struct { activeOrders *bbgo.ActiveOrderBook - ExitMethods bbgo.ExitMethodSet `json:"exits"` - + ExitMethods bbgo.ExitMethodSet `json:"exits"` session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor bbgo.QuantityOrAmount - Interval int `json:"hftInterval"` - - // realtime book ticker to submit order - obBuyPrice uint64 - obSellPrice uint64 - // for getting close price - currentTradePrice uint64 // for negative return rate - openPrice float64 - closePrice float64 + nrr *NRR stopC chan struct{} @@ -207,11 +197,7 @@ func (r *AccumulatedProfitReport) Output(symbol string) { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - if !bbgo.IsBackTesting { - session.Subscribe(types.AggTradeChannel, s.Symbol, types.SubscribeOptions{}) - session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) - } - //session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } func (s *Strategy) ID() string { @@ -339,130 +325,44 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.Bind() s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - atomic.SwapUint64(&s.currentTradePrice, 0.) - s.closePrice = 0. - s.openPrice = 0. - klinDirections := types.NewQueue(100) - started := false - boxOpenPrice := 0. - boxClosePrice := 0. - boxCounter := 0 - - if !bbgo.IsBackTesting { - - s.session.MarketDataStream.OnBookTickerUpdate(func(bt types.BookTicker) { - // quote order book price - newBid := uint64(bt.Buy.Float64()) - newAsk := uint64(bt.Sell.Float64()) - atomic.SwapUint64(&s.obBuyPrice, newBid) - atomic.SwapUint64(&s.obSellPrice, newAsk) - }) - - s.session.MarketDataStream.OnAggTrade(func(trade types.Trade) { - tradePrice := uint64(trade.Price.Float64()) - atomic.SwapUint64(&s.currentTradePrice, tradePrice) - }) - - closeTime := <-time.After(time.Duration(s.Interval-int(time.Now().UnixMilli())%s.Interval) * time.Millisecond) - log.Infof("kline close timing synced @ %s", closeTime.Format("2006-01-02 15:04:05.000000")) - go func() { - intervalCloseTicker := time.NewTicker(time.Duration(s.Interval) * time.Millisecond) - defer intervalCloseTicker.Stop() - for { - select { - case <-intervalCloseTicker.C: - log.Infof("kline close time @ %s", time.Now().Format("2006-01-02 15:04:05.000000")) - - s.orderExecutor.CancelNoWait(context.Background()) - - if s.currentTradePrice > 0 { - s.closePrice = float64(s.currentTradePrice) - log.Infof("Close Price: %f", s.closePrice) - if s.closePrice > 0 && s.openPrice > 0 { - direction := s.closePrice - s.openPrice - klinDirections.Update(direction) - regimeShift := klinDirections.Index(0)*klinDirections.Index(1) < 0 - if regimeShift && !started { - boxOpenPrice = s.openPrice - started = true - boxCounter = 0 - log.Infof("box started at price: %f", boxOpenPrice) - } else if regimeShift && started { - boxClosePrice = s.closePrice - started = false - log.Infof("box ended at price: %f with time length: %d", boxClosePrice, boxCounter) - // box ending, should re-balance position - nirr := fixedpoint.NewFromFloat(((boxOpenPrice - boxClosePrice) / boxOpenPrice) / (float64(boxCounter) + 1)) - log.Infof("Alpha: %f", nirr.Float64()) - if nirr.Float64() < 0 { - _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Quantity: s.Quantity, - Type: types.OrderTypeLimitMaker, - Price: fixedpoint.NewFromFloat(float64(s.obSellPrice)), - Tag: "irrSell", - }) - if err != nil { - log.WithError(err) - } - } else if nirr.Float64() > 0 { - _, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Quantity: s.Quantity, - Type: types.OrderTypeLimitMaker, - Price: fixedpoint.NewFromFloat(float64(s.obBuyPrice)), - Tag: "irrBuy", - }) - if err != nil { - log.WithError(err) - } - } - } else { - boxCounter++ - } - } - } - case <-s.stopC: - log.Warnf("%s goroutine stopped, due to the stop signal", s.Symbol) - return - - case <-ctx.Done(): - log.Warnf("%s goroutine stopped, due to the cancelled context", s.Symbol) - return - } - } - - }() - - openTime := <-time.After(time.Duration(s.Interval-int(time.Now().UnixMilli())%s.Interval) * time.Millisecond) - log.Infof("kline open timing synced @ %s", openTime.Format("2006-01-02 15:04:05.000000")) - go func() { - intervalOpenTicker := time.NewTicker(time.Duration(s.Interval) * time.Millisecond) - defer intervalOpenTicker.Stop() - for { - select { - case <-intervalOpenTicker.C: - time.Sleep(10 * time.Millisecond) - log.Infof("kline open time @ %s", time.Now().Format("2006-01-02 15:04:05.000000")) - - if s.currentTradePrice > 0 && s.closePrice > 0 { - s.openPrice = float64(s.currentTradePrice) - log.Infof("Open Price: %f", s.openPrice) - } - case <-s.stopC: - log.Warnf("%s goroutine stopped, due to the stop signal", s.Symbol) - return - - case <-ctx.Done(): - log.Warnf("%s goroutine stopped, due to the cancelled context", s.Symbol) - return - } - } - }() + kLineStore, _ := s.session.MarketDataStore(s.Symbol) + // window = 2 means day-to-day return, previousClose/currentClose -1 + // delay = false means use open/close-1 as D0 return (default) + // delay = true means use open/close-1 as 10 return + s.nrr = &NRR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 2}, RankingWindow: s.Window, delay: true} + s.nrr.BindK(s.session.MarketDataStream, s.Symbol, s.nrr.Interval) + if klines, ok := kLineStore.KLinesOfInterval(s.nrr.Interval); ok { + s.nrr.LoadK((*klines)[0:]) } + s.session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + alphaNrr := fixedpoint.NewFromFloat(s.nrr.RankedValues.Index(1)) + + // alpha-weighted inventory and cash + targetBase := s.QuantityOrAmount.CalculateQuantity(kline.Close).Mul(alphaNrr) + diffQty := targetBase.Sub(s.Position.Base) + log.Info(alphaNrr.Float64(), s.Position.Base, diffQty.Float64()) + + s.orderExecutor.CancelNoWait(ctx) + if diffQty.Sign() > 0 { + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Quantity: diffQty.Abs(), + Type: types.OrderTypeMarket, + Tag: "irrBuy", + }) + } else if diffQty.Sign() < 0 { + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Quantity: diffQty.Abs(), + Type: types.OrderTypeMarket, + Tag: "irrSell", + }) + } + })) + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() // Output accumulated profit report From 6c8addc4ee5d046eab2a9c8c74d1d8b17313a5df Mon Sep 17 00:00:00 2001 From: Austin Liu Date: Wed, 2 Nov 2022 16:51:06 +0800 Subject: [PATCH 0036/1392] strategy:irr: refactor fast cancel from no wait --- pkg/strategy/irr/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index 3fc4c8e9ad..6a7698eef3 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -343,7 +343,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se diffQty := targetBase.Sub(s.Position.Base) log.Info(alphaNrr.Float64(), s.Position.Base, diffQty.Float64()) - s.orderExecutor.CancelNoWait(ctx) + s.orderExecutor.FastCancel(ctx) if diffQty.Sign() > 0 { _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, From c8aa4ae400edd958d223a9fcc3f3b162e20a51bc Mon Sep 17 00:00:00 2001 From: austin362667 Date: Fri, 7 Oct 2022 00:33:18 +0800 Subject: [PATCH 0037/1392] strategy: improve harmonic by adding HMM filter to denoise shark signal strategy: improve harmonic by adding HMM filter to denoise shark signal --- config/harmonic.yaml | 10 +- pkg/strategy/harmonic/strategy.go | 157 +++++++++++++++++++++--------- 2 files changed, 118 insertions(+), 49 deletions(-) diff --git a/config/harmonic.yaml b/config/harmonic.yaml index be52b01b05..9f7fa11924 100644 --- a/config/harmonic.yaml +++ b/config/harmonic.yaml @@ -16,8 +16,8 @@ exchangeStrategies: harmonic: symbol: BTCBUSD interval: 1s - window: 500 - quantity: 0.05 + window: 60 + quantity: 0.005 # Draw pnl drawGraph: true graphPNLPath: "./pnl.png" @@ -26,12 +26,12 @@ exchangeStrategies: backtest: sessions: - binance - startTime: "2022-09-30" - endTime: "2022-10-01" + startTime: "2022-10-01" + endTime: "2022-10-07" symbols: - BTCBUSD accounts: binance: balances: BTC: 1.0 - BUSD: 40_000.0 \ No newline at end of file + BUSD: 60_000.0 \ No newline at end of file diff --git a/pkg/strategy/harmonic/strategy.go b/pkg/strategy/harmonic/strategy.go index 49469bddb0..d379f3262f 100644 --- a/pkg/strategy/harmonic/strategy.go +++ b/pkg/strategy/harmonic/strategy.go @@ -3,14 +3,17 @@ package harmonic import ( "context" "fmt" + "os" + "sync" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/data/tsv" "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" - "github.com/sirupsen/logrus" + floats2 "gonum.org/v1/gonum/floats" ) const ID = "harmonic" @@ -317,6 +320,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) + s.InitDrawCommands(&profitSlice, &cumProfitSlice) s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) @@ -332,66 +336,131 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if klines, ok := kLineStore.KLinesOfInterval(s.shark.Interval); ok { s.shark.LoadK((*klines)[0:]) } + + states := types.NewQueue(s.Window) + + states.Update(0) s.session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { log.Infof("Shark Score: %f, Current Price: %f", s.shark.Last(), kline.Close.Float64()) - //previousRegime := s.shark.Values.Tail(10).Mean() - //zeroThreshold := 5. + nextState := alpha(s.shark.Array(s.Window), states.Array(s.Window), s.Window) + states.Update(nextState) + log.Infof("Denoised signal via HMM: %f", states.Last()) - if s.shark.Rank(s.Window).Last()/float64(s.Window) > 0.99 { // && ((previousRegime < zeroThreshold && previousRegime > -zeroThreshold) || s.shark.Index(1) < 0) - if s.Position.IsShort() { - _ = s.orderExecutor.GracefulCancel(ctx) - s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "close short position") - } - _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + if states.Length() < s.Window { + return + } + direction := 0. + if s.Position.IsLong() { + direction = 1. + } else if s.Position.IsShort() { + direction = -1. + } + + if s.Position.IsOpened(kline.Close) && states.Mean(5) == 0 { + s.orderExecutor.ClosePosition(ctx, fixedpoint.One) + } + if states.Mean(5) == 1 && direction != 1 { + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, Quantity: s.Quantity, Type: types.OrderTypeMarket, - Tag: "shark long: buy in", + Tag: "shark long", }) - if err == nil { - _, err = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Quantity: s.Quantity, - Price: fixedpoint.NewFromFloat(s.shark.Highs.Tail(100).Max()), - Type: types.OrderTypeLimit, - Tag: "shark long: sell back", - }) - } - if err != nil { - log.Errorln(err) - } - - } else if s.shark.Rank(s.Window).Last()/float64(s.Window) < 0.01 { // && ((previousRegime < zeroThreshold && previousRegime > -zeroThreshold) || s.shark.Index(1) > 0) - if s.Position.IsLong() { - _ = s.orderExecutor.GracefulCancel(ctx) - s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "close long position") - } - _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + } else if states.Mean(5) == -1 && direction != -1 { + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, Quantity: s.Quantity, Type: types.OrderTypeMarket, - Tag: "shark short: sell in", + Tag: "shark short", }) - if err == nil { - _, err = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Quantity: s.Quantity, - Price: fixedpoint.NewFromFloat(s.shark.Lows.Tail(100).Min()), - Type: types.OrderTypeLimit, - Tag: "shark short: buy back", - }) - } - if err != nil { - log.Errorln(err) - } } })) + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + // Output accumulated profit report + if bbgo.IsBackTesting { + defer s.AccumulatedProfitReport.Output(s.Symbol) + + if s.DrawGraph { + if err := s.Draw(&profitSlice, &cumProfitSlice); err != nil { + log.WithError(err).Errorf("cannot draw graph") + } + } + } + + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + _ = s.orderExecutor.GracefulCancel(ctx) + }) + return nil } + +// TODO: dirichlet distribution is a too naive solution +func observationDistribution(y_t, x_t float64) float64 { + if x_t == 0. && y_t == 0 { + // observed zero value from indicator when in neutral state + return 1. + } else if x_t > 0. && y_t > 0. { + // observed positive value from indicator when in long state + return 1. + } else if x_t < 0. && y_t < 0. { + // observed negative value from indicator when in short state + return 1. + } else { + return 0. + } +} + +func transitionProbability(x_t0, x_t1 int) float64 { + // stick to the same sate + if x_t0 == x_t1 { + return 0.99 + } + // transit to next new state + return 1 - 0.99 +} + +func alpha(y_t []float64, x_t []float64, l int) float64 { + al := make([]float64, l) + an := make([]float64, l) + as := make([]float64, l) + long := 0. + neut := 0. + short := 0. + // n is the incremental time steps + for n := 2; n <= len(x_t); n++ { + for j := -1; j <= 1; j++ { + sil := make([]float64, 3) + sin := make([]float64, 3) + sis := make([]float64, 3) + for i := -1; i <= 1; i++ { + sil = append(sil, x_t[n-1-1]*transitionProbability(i, j)) + sin = append(sin, x_t[n-1-1]*transitionProbability(i, j)) + sis = append(sis, x_t[n-1-1]*transitionProbability(i, j)) + } + if j > 0 { + long = floats2.Max(sil) * observationDistribution(y_t[n-1], float64(j)) + al = append(al, long) + } else if j == 0 { + neut = floats2.Max(sin) * observationDistribution(y_t[n-1], float64(j)) + an = append(an, neut) + } else if j < 0 { + short = floats2.Max(sis) * observationDistribution(y_t[n-1], float64(j)) + as = append(as, short) + } + } + } + maximum := floats2.Max([]float64{long, neut, short}) + if maximum == long { + return 1 + } else if maximum == short { + return -1 + } + return 0 +} From 7d03c6940678ee50317a9303334eb73c5f5167c1 Mon Sep 17 00:00:00 2001 From: Austin Liu Date: Wed, 2 Nov 2022 17:16:32 +0800 Subject: [PATCH 0038/1392] strategy:harmonic: fix --- pkg/strategy/harmonic/strategy.go | 50 ++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/pkg/strategy/harmonic/strategy.go b/pkg/strategy/harmonic/strategy.go index d379f3262f..4d98fda34a 100644 --- a/pkg/strategy/harmonic/strategy.go +++ b/pkg/strategy/harmonic/strategy.go @@ -13,7 +13,6 @@ import ( "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" "github.com/sirupsen/logrus" - floats2 "gonum.org/v1/gonum/floats" ) const ID = "harmonic" @@ -342,9 +341,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se states.Update(0) s.session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - log.Infof("Shark Score: %f, Current Price: %f", s.shark.Last(), kline.Close.Float64()) + log.Infof("shark score: %f, current price: %f", s.shark.Last(), kline.Close.Float64()) - nextState := alpha(s.shark.Array(s.Window), states.Array(s.Window), s.Window) + nextState := hmm(s.shark.Array(s.Window), states.Array(s.Window), s.Window) states.Update(nextState) log.Infof("Denoised signal via HMM: %f", states.Last()) @@ -367,7 +366,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Side: types.SideTypeBuy, Quantity: s.Quantity, Type: types.OrderTypeMarket, - Tag: "shark long", + Tag: "sharkLong", }) } else if states.Mean(5) == -1 && direction != -1 { _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ @@ -375,7 +374,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Side: types.SideTypeSell, Quantity: s.Quantity, Type: types.OrderTypeMarket, - Tag: "shark short", + Tag: "sharkShort", }) } })) @@ -402,7 +401,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } // TODO: dirichlet distribution is a too naive solution -func observationDistribution(y_t, x_t float64) float64 { +func observeDistribution(y_t, x_t float64) float64 { if x_t == 0. && y_t == 0 { // observed zero value from indicator when in neutral state return 1. @@ -417,7 +416,7 @@ func observationDistribution(y_t, x_t float64) float64 { } } -func transitionProbability(x_t0, x_t1 int) float64 { +func transitProbability(x_t0, x_t1 int) float64 { // stick to the same sate if x_t0 == x_t1 { return 0.99 @@ -426,7 +425,21 @@ func transitionProbability(x_t0, x_t1 int) float64 { return 1 - 0.99 } -func alpha(y_t []float64, x_t []float64, l int) float64 { +// HMM main function, ref: https://tr8dr.github.io/HMMFiltering/ +/* +# initialize time step 0 using state priors and observation dist p(y | x = s) +for si in states: + alpha[t = 0, state = si] = pi[si] * p(y[0] | x = si) + +# determine alpha for t = 1 .. n +for t in 1 .. n: + for sj in states: + alpha[t,sj] = max([alpha[t-1,si] * M[si,sj] for si in states]) * p(y[t] | x = sj) + +# determine current state at time t +return argmax(alpha[t,si] over si) +*/ +func hmm(y_t []float64, x_t []float64, l int) float64 { al := make([]float64, l) an := make([]float64, l) as := make([]float64, l) @@ -440,26 +453,29 @@ func alpha(y_t []float64, x_t []float64, l int) float64 { sin := make([]float64, 3) sis := make([]float64, 3) for i := -1; i <= 1; i++ { - sil = append(sil, x_t[n-1-1]*transitionProbability(i, j)) - sin = append(sin, x_t[n-1-1]*transitionProbability(i, j)) - sis = append(sis, x_t[n-1-1]*transitionProbability(i, j)) + sil = append(sil, x_t[n-1-1]*transitProbability(i, j)) + sin = append(sin, x_t[n-1-1]*transitProbability(i, j)) + sis = append(sis, x_t[n-1-1]*transitProbability(i, j)) } if j > 0 { - long = floats2.Max(sil) * observationDistribution(y_t[n-1], float64(j)) + _, longArr := floats.MinMax(sil, 3) + long = longArr[0] * observeDistribution(y_t[n-1], float64(j)) al = append(al, long) } else if j == 0 { - neut = floats2.Max(sin) * observationDistribution(y_t[n-1], float64(j)) + _, neutArr := floats.MinMax(sin, 3) + neut = neutArr[0] * observeDistribution(y_t[n-1], float64(j)) an = append(an, neut) } else if j < 0 { - short = floats2.Max(sis) * observationDistribution(y_t[n-1], float64(j)) + _, shortArr := floats.MinMax(sis, 3) + short = shortArr[0] * observeDistribution(y_t[n-1], float64(j)) as = append(as, short) } } } - maximum := floats2.Max([]float64{long, neut, short}) - if maximum == long { + _, maximum := floats.MinMax([]float64{long, neut, short}, 3) + if maximum[0] == long { return 1 - } else if maximum == short { + } else if maximum[0] == short { return -1 } return 0 From 7aaea257dff9abb3ca27d3185183915d8e3ff8e5 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 10 Nov 2022 18:17:35 +0900 Subject: [PATCH 0039/1392] feature: optimizer add profitFactor optimization. Optimization value use float64 instead to save memory and boost performance --- pkg/backtest/report.go | 2 ++ pkg/cmd/backtest.go | 27 ++++++++++++++-------- pkg/optimizer/config.go | 2 +- pkg/optimizer/grid.go | 44 ++++++++++++++++++++++++------------ pkg/optimizer/hpoptimizer.go | 12 +++++++--- 5 files changed, 58 insertions(+), 29 deletions(-) diff --git a/pkg/backtest/report.go b/pkg/backtest/report.go index 7af0340c3d..bce827699d 100644 --- a/pkg/backtest/report.go +++ b/pkg/backtest/report.go @@ -81,6 +81,8 @@ type SessionSymbolReport struct { Manifests Manifests `json:"manifests,omitempty"` Sharpe fixedpoint.Value `json:"sharpeRatio"` Sortino fixedpoint.Value `json:"sortinoRatio"` + ProfitFactor fixedpoint.Value `json:"profitFactor"` + WinningRatio fixedpoint.Value `json:"winningRatio"` } func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value { diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 192261e8b7..3e2c24a428 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -4,11 +4,6 @@ import ( "bufio" "context" "fmt" - "github.com/c9s/bbgo/pkg/cmd/cmdutil" - "github.com/c9s/bbgo/pkg/data/tsv" - "github.com/c9s/bbgo/pkg/util" - "github.com/fatih/color" - "github.com/google/uuid" "os" "path/filepath" "sort" @@ -16,6 +11,12 @@ import ( "syscall" "time" + "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/util" + "github.com/fatih/color" + "github.com/google/uuid" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -523,8 +524,11 @@ var BacktestCmd = &cobra.Command{ for _, session := range environ.Sessions() { for symbol, trades := range session.Trades { - intervalProfits := sessionTradeStats[session.Name][symbol].IntervalProfits[types.Interval1d] - symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Trades, intervalProfits) + tradeState := sessionTradeStats[session.Name][symbol] + profitFactor := tradeState.ProfitFactor + winningRatio := tradeState.WinningRatio + intervalProfits := tradeState.IntervalProfits[types.Interval1d] + symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Trades, intervalProfits, profitFactor, winningRatio) if err != nil { return err } @@ -615,7 +619,8 @@ func collectSubscriptionIntervals(environ *bbgo.Environment) (allKLineIntervals return allKLineIntervals, requiredInterval, backTestIntervals } -func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, intervalProfit *types.IntervalProfitCollector) ( +func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, intervalProfit *types.IntervalProfitCollector, + profitFactor, winningRatio fixedpoint.Value) ( *backtest.SessionSymbolReport, error, ) { @@ -661,8 +666,10 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, InitialBalances: initBalances, FinalBalances: finalBalances, // Manifests: manifests, - Sharpe: sharpeRatio, - Sortino: sortinoRatio, + Sharpe: sharpeRatio, + Sortino: sortinoRatio, + ProfitFactor: profitFactor, + WinningRatio: winningRatio, } for _, s := range session.Subscriptions { diff --git a/pkg/optimizer/config.go b/pkg/optimizer/config.go index a9e03b3786..4d5e0515b2 100644 --- a/pkg/optimizer/config.go +++ b/pkg/optimizer/config.go @@ -79,7 +79,7 @@ func LoadConfig(yamlConfigFileName string) (*Config, error) { switch objective := strings.ToLower(optConfig.Objective); objective { case "", "default": optConfig.Objective = HpOptimizerObjectiveEquity - case HpOptimizerObjectiveEquity, HpOptimizerObjectiveProfit, HpOptimizerObjectiveVolume: + case HpOptimizerObjectiveEquity, HpOptimizerObjectiveProfit, HpOptimizerObjectiveVolume, HpOptimizerObjectiveProfitFactor: optConfig.Objective = objective default: return nil, fmt.Errorf(`unknown objective "%s"`, optConfig.Objective) diff --git a/pkg/optimizer/grid.go b/pkg/optimizer/grid.go index eab8352f7a..0d40d7fdd9 100644 --- a/pkg/optimizer/grid.go +++ b/pkg/optimizer/grid.go @@ -14,30 +14,43 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) -type MetricValueFunc func(summaryReport *backtest.SummaryReport) fixedpoint.Value +type MetricValueFunc func(summaryReport *backtest.SummaryReport) float64 -var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { - return summaryReport.TotalProfit +var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) float64 { + return summaryReport.TotalProfit.Float64() } -var TotalVolume = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { +var TotalVolume = func(summaryReport *backtest.SummaryReport) float64 { if len(summaryReport.SymbolReports) == 0 { - return fixedpoint.Zero + return 0 } - buyVolume := summaryReport.SymbolReports[0].PnL.BuyVolume - sellVolume := summaryReport.SymbolReports[0].PnL.SellVolume - return buyVolume.Add(sellVolume) + buyVolume := summaryReport.SymbolReports[0].PnL.BuyVolume.Float64() + sellVolume := summaryReport.SymbolReports[0].PnL.SellVolume.Float64() + return buyVolume + sellVolume } -var TotalEquityDiff = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { +var TotalEquityDiff = func(summaryReport *backtest.SummaryReport) float64 { if len(summaryReport.SymbolReports) == 0 { - return fixedpoint.Zero + return 0 } - initEquity := summaryReport.InitialEquityValue - finalEquity := summaryReport.FinalEquityValue - return finalEquity.Sub(initEquity) + initEquity := summaryReport.InitialEquityValue.Float64() + finalEquity := summaryReport.FinalEquityValue.Float64() + return finalEquity - initEquity +} + +var ProfitFactorMetricValueFunc = func(summaryReport *backtest.SummaryReport) float64 { + if len(summaryReport.SymbolReports) == 0 { + return 0 + } + if len(summaryReport.SymbolReports) > 1 { + panic("multiple symbols' profitfactor optimization not supported") + } + report := summaryReport.SymbolReports[0] + pf := report.ProfitFactor.Float64() + win := report.WinningRatio.Float64() + return pf*0.9 + win*0.1 } type Metric struct { @@ -51,7 +64,7 @@ type Metric struct { Key string `json:"key"` // Value is the metric value of the metric - Value fixedpoint.Value `json:"value,omitempty"` + Value float64 `json:"value,omitempty"` } func copyParams(params []interface{}) []interface{} { @@ -199,6 +212,7 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][] "totalProfit": TotalProfitMetricValueFunc, "totalVolume": TotalVolume, "totalEquityDiff": TotalEquityDiff, + "profitFactor": ProfitFactorMetricValueFunc, } var metrics = map[string][]Metric{} @@ -287,7 +301,7 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][] sort.Slice(metrics[n], func(i, j int) bool { a := metrics[n][i].Value b := metrics[n][j].Value - return a.Compare(b) > 0 + return a > b }) } diff --git a/pkg/optimizer/hpoptimizer.go b/pkg/optimizer/hpoptimizer.go index ecf806ac43..6f8236da03 100644 --- a/pkg/optimizer/hpoptimizer.go +++ b/pkg/optimizer/hpoptimizer.go @@ -3,6 +3,9 @@ package optimizer import ( "context" "fmt" + "math" + "sync" + "github.com/c-bata/goptuna" goptunaCMAES "github.com/c-bata/goptuna/cmaes" goptunaSOBOL "github.com/c-bata/goptuna/sobol" @@ -11,10 +14,9 @@ import ( "github.com/cheggaaa/pb/v3" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" - "math" - "sync" ) +// WARNING: the text here could only be lower cases const ( // HpOptimizerObjectiveEquity optimize the parameters to maximize equity gain HpOptimizerObjectiveEquity = "equity" @@ -22,6 +24,8 @@ const ( HpOptimizerObjectiveProfit = "profit" // HpOptimizerObjectiveVolume optimize the parameters to maximize trading volume HpOptimizerObjectiveVolume = "volume" + // HpOptimizerObjectiveProfitFactor optimize the parameters to maximize profit factor + HpOptimizerObjectiveProfitFactor = "profitfactor" ) const ( @@ -198,6 +202,8 @@ func (o *HyperparameterOptimizer) buildObjective(executor Executor, configJson [ metricValueFunc = TotalVolume case HpOptimizerObjectiveEquity: metricValueFunc = TotalEquityDiff + case HpOptimizerObjectiveProfitFactor: + metricValueFunc = ProfitFactorMetricValueFunc } return func(trial goptuna.Trial) (float64, error) { @@ -225,7 +231,7 @@ func (o *HyperparameterOptimizer) buildObjective(executor Executor, configJson [ return 0.0, err } // By config, the Goptuna optimize the parameters by maximize the objective output. - return metricValueFunc(summary).Float64(), nil + return metricValueFunc(summary), nil } } From 48c6326ac12034990e66cfa640e3aeb3aa71c7fa Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 17 Nov 2022 17:59:23 +0800 Subject: [PATCH 0040/1392] strategy/linregmaker: draft --- pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/linregmaker/doc.go | 6 + pkg/strategy/linregmaker/strategy.go | 493 ++++++++++++++++++++++ pkg/strategy/linregmaker/strategy_test.go | 69 +++ 4 files changed, 569 insertions(+) create mode 100644 pkg/strategy/linregmaker/doc.go create mode 100644 pkg/strategy/linregmaker/strategy.go create mode 100644 pkg/strategy/linregmaker/strategy_test.go diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 1c29ab684c..0c2a27b880 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -20,6 +20,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/harmonic" _ "github.com/c9s/bbgo/pkg/strategy/irr" _ "github.com/c9s/bbgo/pkg/strategy/kline" + _ "github.com/c9s/bbgo/pkg/strategy/linregmaker" _ "github.com/c9s/bbgo/pkg/strategy/marketcap" _ "github.com/c9s/bbgo/pkg/strategy/pivotshort" _ "github.com/c9s/bbgo/pkg/strategy/pricealert" diff --git a/pkg/strategy/linregmaker/doc.go b/pkg/strategy/linregmaker/doc.go new file mode 100644 index 0000000000..a4bddf911e --- /dev/null +++ b/pkg/strategy/linregmaker/doc.go @@ -0,0 +1,6 @@ +// Linregmaker is a maker strategy depends on the linear regression baseline slopes +// +// Linregmaker uses two linear regression baseline slopes for trading: +// 1) The fast linReg is to determine the short-term trend. It controls whether placing buy/sell orders or not. +// 2) The slow linReg is to determine the mid-term trend. It controls whether the creation of opposite direction position is allowed. +package linregmaker diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go new file mode 100644 index 0000000000..6857bd4c90 --- /dev/null +++ b/pkg/strategy/linregmaker/strategy.go @@ -0,0 +1,493 @@ +package linregmaker + +import ( + "context" + "fmt" + "github.com/c9s/bbgo/pkg/dynamicmetric" + "sync" + + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/util" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// TODO: +// - TradeInBand: no buy order above the band, no sell order below the band +// - DynamicQuantity +// - Validate() + +const ID = "linregmaker" + +var notionModifier = fixedpoint.NewFromFloat(1.1) +var two = fixedpoint.NewFromInt(2) + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type BollingerSetting struct { + types.IntervalWindow + BandWidth float64 `json:"bandWidth"` +} + +type Strategy struct { + Environment *bbgo.Environment + StandardIndicatorSet *bbgo.StandardIndicatorSet + Market types.Market + + // Symbol is the market symbol you want to trade + Symbol string `json:"symbol"` + + types.IntervalWindow + + bbgo.QuantityOrAmount + + // ReverseEMA is used to determine the long-term trend. + // Above the ReverseEMA is the long trend and vise versa. + // All the opposite trend position will be closed upon the trend change + ReverseEMA *indicator.EWMA `json:"reverseEMA"` + + // mainTrendCurrent is the current long-term trend + mainTrendCurrent types.Direction + // mainTrendPrevious is the long-term trend of previous kline + mainTrendPrevious types.Direction + + // FastLinReg is to determine the short-term trend. + // Buy/sell orders are placed if the FastLinReg and the ReverseEMA trend are in the same direction, and only orders + // that reduce position are placed if the FastLinReg and the ReverseEMA trend are in different directions. + FastLinReg *indicator.LinReg `json:"fastLinReg,omitempty"` + + // SlowLinReg is to determine the midterm trend. + // When the SlowLinReg and the ReverseEMA trend are in different directions, creation of opposite position is + // allowed. + SlowLinReg *indicator.LinReg `json:"slowLinReg,omitempty"` + + // NeutralBollinger is the smaller range of the bollinger band + // If price is in this band, it usually means the price is oscillating. + // If price goes out of this band, we tend to not place sell orders or buy orders + NeutralBollinger *BollingerSetting `json:"neutralBollinger"` + + // TradeInBand + // When this is on, places orders only when the current price is in the bollinger band. + TradeInBand bool `json:"tradeInBand"` + + // useTickerPrice use the ticker api to get the mid price instead of the closed kline price. + // The back-test engine is kline-based, so the ticker price api is not supported. + // Turn this on if you want to do real trading. + useTickerPrice bool + + // Spread is the price spread from the middle price. + // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) + // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) + // Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 + Spread fixedpoint.Value `json:"spread"` + + // BidSpread overrides the spread setting, this spread will be used for the buy order + BidSpread fixedpoint.Value `json:"bidSpread,omitempty"` + + // AskSpread overrides the spread setting, this spread will be used for the sell order + AskSpread fixedpoint.Value `json:"askSpread,omitempty"` + + // DynamicSpread enables the automatic adjustment to bid and ask spread. + // Overrides Spread, BidSpread, and AskSpread + DynamicSpread dynamicmetric.DynamicSpread `json:"dynamicSpread,omitempty"` + + // MaxExposurePosition is the maximum position you can hold + // +10 means you can hold 10 ETH long position by maximum + // -10 means you can hold -10 ETH short position by maximum + MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` + + // DynamicExposure is used to define the exposure position range with the given percentage. + // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically according to the bollinger + // band you set. + DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` + + session *bbgo.ExchangeSession + + // ExitMethods are various TP/SL methods + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + orderExecutor *bbgo.GeneralOrderExecutor + + groupID uint32 + + // defaultBoll is the BOLLINGER indicator we used for predicting the price. + defaultBoll *indicator.BOLL + + // neutralBoll is the neutral price section + neutralBoll *indicator.BOLL + + // StrategyController + bbgo.StrategyController +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // Subscribe for ReverseEMA + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.ReverseEMA.Interval, + }) + // Initialize ReverseEMA + s.ReverseEMA = s.StandardIndicatorSet.EWMA(s.ReverseEMA.IntervalWindow) + + // Subscribe for LinRegs + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.FastLinReg.Interval, + }) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.SlowLinReg.Interval, + }) + // Initialize LinRegs + kLineStore, _ := session.MarketDataStore(s.Symbol) + s.FastLinReg.BindK(session.MarketDataStream, s.Symbol, s.FastLinReg.Interval) + if klines, ok := kLineStore.KLinesOfInterval(s.FastLinReg.Interval); ok { + s.FastLinReg.LoadK((*klines)[0:]) + } + s.SlowLinReg.BindK(session.MarketDataStream, s.Symbol, s.SlowLinReg.Interval) + if klines, ok := kLineStore.KLinesOfInterval(s.SlowLinReg.Interval); ok { + s.SlowLinReg.LoadK((*klines)[0:]) + } + + // Subscribe for BBs + if s.NeutralBollinger != nil && s.NeutralBollinger.Interval != "" { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.NeutralBollinger.Interval, + }) + } + // Initialize BBs + s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) + + // Setup Exits + s.ExitMethods.SetAndSubscribe(session, s) + + // Setup dynamic spread + if s.DynamicSpread.IsEnabled() { + s.DynamicSpread.Initialize(s.Symbol, session) + } + + // Setup dynamic exposure + if s.DynamicExposure.IsEnabled() { + s.DynamicExposure.Initialize(s.Symbol, session) + } +} + +func (s *Strategy) Validate() error { + if len(s.Symbol) == 0 { + return errors.New("symbol is required") + } + + return nil +} + +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + return s.orderExecutor.ClosePosition(ctx, percentage) +} + +// updateSpread for ask and bid price +func (s *Strategy) updateSpread() { + // Update spreads with dynamic spread + if s.DynamicSpread.IsEnabled() { + dynamicBidSpread, err := s.DynamicSpread.GetBidSpread() + if err == nil && dynamicBidSpread > 0 { + s.BidSpread = fixedpoint.NewFromFloat(dynamicBidSpread) + log.Infof("%s dynamic bid spread updated: %s", s.Symbol, s.BidSpread.Percentage()) + } + dynamicAskSpread, err := s.DynamicSpread.GetAskSpread() + if err == nil && dynamicAskSpread > 0 { + s.AskSpread = fixedpoint.NewFromFloat(dynamicAskSpread) + log.Infof("%s dynamic ask spread updated: %s", s.Symbol, s.AskSpread.Percentage()) + } + } + + if s.BidSpread.Sign() <= 0 { + s.BidSpread = s.Spread + } + + if s.BidSpread.Sign() <= 0 { + s.AskSpread = s.Spread + } +} + +// updateMaxExposure with dynamic exposure +func (s *Strategy) updateMaxExposure(midPrice fixedpoint.Value) { + // Calculate max exposure + if s.DynamicExposure.IsEnabled() { + var err error + maxExposurePosition, err := s.DynamicExposure.GetMaxExposure(midPrice.Float64()) + if err != nil { + log.WithError(err).Errorf("can not calculate DynamicExposure of %s, use previous MaxExposurePosition instead", s.Symbol) + } else { + s.MaxExposurePosition = maxExposurePosition + } + log.Infof("calculated %s max exposure position: %v", s.Symbol, s.MaxExposurePosition) + } +} + +// getOrderPrices returns ask and bid prices +func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoint.Value, bidPrice fixedpoint.Value) { + askPrice = midPrice.Mul(fixedpoint.One.Add(s.AskSpread)) + bidPrice = midPrice.Mul(fixedpoint.One.Sub(s.BidSpread)) + log.Infof("mid price:%v ask:%v bid: %v", midPrice, askPrice, bidPrice) + + return askPrice, bidPrice +} + +// getOrderQuantities returns sell and buy qty +func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { + // TODO: dynamic qty to determine qty + sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + log.Infof("sell qty:%v buy qty: %v", sellQuantity, buyQuantity) + + return sellQuantity, buyQuantity +} + +// getCanBuySell returns the buy sell switches +func (s *Strategy) getCanBuySell(midPrice fixedpoint.Value) (canBuy bool, canSell bool) { + // By default, both buy and sell are on, which means we will place buy and sell orders + canBuy = true + canSell = true + + // Check if current position > maxExposurePosition + if s.Position.GetBase().Abs().Compare(s.MaxExposurePosition) > 0 { + if s.mainTrendCurrent == types.DirectionUp { + canBuy = false + } else if s.mainTrendCurrent == types.DirectionDown { + canSell = false + } + } + + if s.TradeInBand { + // Price too high + if midPrice.Float64() > s.neutralBoll.UpBand.Last() { + canBuy = false + log.Infof("tradeInBand is set, skip buy when the price is higher than the neutralBB") + } + // Price too low in uptrend + if midPrice.Float64() < s.neutralBoll.DownBand.Last() { + canSell = false + log.Infof("tradeInBand is set, skip sell when the price is lower than the neutralBB") + } + } + + return canBuy, canSell +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + // initial required information + s.session = session + + // Calculate group id for orders + instanceID := s.InstanceID() + s.groupID = util.FNV32(instanceID) + + // If position is nil, we need to allocate a new position for calculation + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + // Set fee rate + if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { + s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ + MakerFeeRate: s.session.MakerFeeRate, + TakerFeeRate: s.session.TakerFeeRate, + }) + } + + // If position is nil, we need to allocate a new position for calculation + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + // Always update the position fields + s.Position.Strategy = ID + s.Position.StrategyInstanceID = s.InstanceID() + + // Profit stats + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.Bind() + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.ExitMethods.Bind(session, s.orderExecutor) + + if bbgo.IsBackTesting { + s.useTickerPrice = false + } else { + s.useTickerPrice = true + } + + // StrategyController + s.Status = types.StrategyStatusRunning + s.OnSuspend(func() { + _ = s.orderExecutor.GracefulCancel(ctx) + bbgo.Sync(ctx, s) + }) + s.OnEmergencyStop(func() { + // Close whole position + _ = s.ClosePosition(ctx, fixedpoint.NewFromFloat(1.0)) + }) + + // Main interval + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + _ = s.orderExecutor.GracefulCancel(ctx) + + // closePrice is the close price of current kline + closePrice := kline.GetClose() + // priceReverseEMA is the current ReverseEMA price + priceReverseEMA := fixedpoint.NewFromFloat(s.ReverseEMA.Last()) + + // Main trend by ReverseEMA + s.mainTrendPrevious = s.mainTrendCurrent + if closePrice.Compare(priceReverseEMA) > 0 { + s.mainTrendCurrent = types.DirectionUp + } else if closePrice.Compare(priceReverseEMA) < 0 { + s.mainTrendCurrent = types.DirectionDown + } + // TODO: everything should works for both direction + + // Trend reversal + if s.mainTrendCurrent != s.mainTrendPrevious { + // Close on-hand position that is not in the same direction as the new trend + if !s.Position.IsDust(closePrice) && + ((s.Position.IsLong() && s.mainTrendCurrent == types.DirectionDown) || + (s.Position.IsShort() && s.mainTrendCurrent == types.DirectionUp)) { + log.Infof("trend reverse to %v. closing on-hand position", s.mainTrendCurrent) + if err := s.ClosePosition(ctx, fixedpoint.One); err != nil { + log.WithError(err).Errorf("cannot close on-hand position of %s", s.Symbol) + // TODO: close position failed. retry? + } + } + } + + // midPrice for ask and bid prices + var midPrice fixedpoint.Value + if s.useTickerPrice { + ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + return + } + + midPrice = ticker.Buy.Add(ticker.Sell).Div(two) + log.Infof("using ticker price: bid %v / ask %v, mid price %v", ticker.Buy, ticker.Sell, midPrice) + } else { + midPrice = closePrice + } + + // Update price spread + s.updateSpread() + + // Update max exposure + s.updateMaxExposure(midPrice) + + // Current position status + log.Infof("position: %s", s.Position) + if !s.Position.IsClosed() && !s.Position.IsDust(midPrice) { + log.Infof("current %s unrealized profit: %f %s", s.Symbol, s.Position.UnrealizedProfit(midPrice).Float64(), s.Market.QuoteCurrency) + } + + // Order prices + askPrice, bidPrice := s.getOrderPrices(midPrice) + + // Order qty + sellQuantity, buyQuantity := s.getOrderQuantities(askPrice, bidPrice) + + // TODO: Reduce only in margin and futures + sellOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: sellQuantity, + Price: askPrice, + Market: s.Market, + GroupID: s.groupID, + } + buyOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: buyQuantity, + Price: bidPrice, + Market: s.Market, + GroupID: s.groupID, + } + + canBuy, canSell := s.getCanBuySell(midPrice) + + // TODO: check enough balance? + + // Submit orders + var submitOrders []types.SubmitOrder + if canSell { + submitOrders = append(submitOrders, adjustOrderQuantity(sellOrder, s.Market)) + } + if canBuy { + submitOrders = append(submitOrders, adjustOrderQuantity(buyOrder, s.Market)) + } + + if len(submitOrders) == 0 { + return + } + _, _ = s.orderExecutor.SubmitOrders(ctx, submitOrders...) + })) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + return nil +} + +// TODO +func adjustOrderQuantity(submitOrder types.SubmitOrder, market types.Market) types.SubmitOrder { + if submitOrder.Quantity.Mul(submitOrder.Price).Compare(market.MinNotional) < 0 { + submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, market.MinNotional.Mul(notionModifier)) + } + + if submitOrder.Quantity.Compare(market.MinQuantity) < 0 { + submitOrder.Quantity = fixedpoint.Max(submitOrder.Quantity, market.MinQuantity) + } + + return submitOrder +} diff --git a/pkg/strategy/linregmaker/strategy_test.go b/pkg/strategy/linregmaker/strategy_test.go new file mode 100644 index 0000000000..317464ad0b --- /dev/null +++ b/pkg/strategy/linregmaker/strategy_test.go @@ -0,0 +1,69 @@ +package linregmaker + +import ( + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +func Test_calculateBandPercentage(t *testing.T) { + type args struct { + up float64 + down float64 + sma float64 + midPrice float64 + } + tests := []struct { + name string + args args + want fixedpoint.Value + }{ + { + name: "positive boundary", + args: args{ + up: 2000.0, + sma: 1500.0, + down: 1000.0, + midPrice: 2000.0, + }, + want: fixedpoint.NewFromFloat(1.0), + }, + { + name: "inside positive boundary", + args: args{ + up: 2000.0, + sma: 1500.0, + down: 1000.0, + midPrice: 1600.0, + }, + want: fixedpoint.NewFromFloat(0.2), // 20% + }, + { + name: "negative boundary", + args: args{ + up: 2000.0, + sma: 1500.0, + down: 1000.0, + midPrice: 1000.0, + }, + want: fixedpoint.NewFromFloat(-1.0), + }, + { + name: "out of negative boundary", + args: args{ + up: 2000.0, + sma: 1500.0, + down: 1000.0, + midPrice: 800.0, + }, + want: fixedpoint.NewFromFloat(-1.4), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := calculateBandPercentage(tt.args.up, tt.args.down, tt.args.sma, tt.args.midPrice); fixedpoint.NewFromFloat(got) != tt.want { + t.Errorf("calculateBandPercentage() = %v, want %v", got, tt.want) + } + }) + } +} From 9be9ea2a471666f30bc563f9fb617ac2cd2bc383 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 18 Nov 2022 15:12:38 +0800 Subject: [PATCH 0041/1392] strategy/linregmaker: add AllowOppositePosition and FasterDecreaseRatio --- pkg/strategy/linregmaker/strategy.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 6857bd4c90..154336d0f9 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -70,6 +70,14 @@ type Strategy struct { // allowed. SlowLinReg *indicator.LinReg `json:"slowLinReg,omitempty"` + // AllowOppositePosition if true, the creation of opposite position is allowed when both fast and slow LinReg are in + // the opposite direction to main trend + AllowOppositePosition bool `json:"allowOppositePosition"` + + // FasterDecreaseRatio the quantity of decreasing position orders are multiplied by this ratio when both fast and + // slow LinReg are in the opposite direction to main trend + FasterDecreaseRatio fixedpoint.Value `json:"FasterDecreaseRatio,omitempty"` + // NeutralBollinger is the smaller range of the bollinger band // If price is in this band, it usually means the price is oscillating. // If price goes out of this band, we tend to not place sell orders or buy orders @@ -259,8 +267,17 @@ func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoin // getOrderQuantities returns sell and buy qty func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { // TODO: dynamic qty to determine qty + // TODO: spot, margin, and futures sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + + // Faster position decrease + if s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0 { + sellQuantity = sellQuantity * s.FasterDecreaseRatio + } else if s.mainTrendCurrent == types.DirectionDown && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0 { + buyQuantity = buyQuantity * s.FasterDecreaseRatio + } + log.Infof("sell qty:%v buy qty: %v", sellQuantity, buyQuantity) return sellQuantity, buyQuantity @@ -294,6 +311,15 @@ func (s *Strategy) getCanBuySell(midPrice fixedpoint.Value) (canBuy bool, canSel } } + // Stop decrease when position closed unless both LinRegs are in the opposite direction to the main trend + if s.Position.IsClosed() || s.Position.IsDust(midPrice) { + if s.mainTrendCurrent == types.DirectionUp && !(s.AllowOppositePosition && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0) { + canSell = false + } else if s.mainTrendCurrent == types.DirectionDown && !(s.AllowOppositePosition && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0) { + canBuy = false + } + } + return canBuy, canSell } From 8a81e68e276ca2955142aaa59c4f3b932bd6a1a5 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 18 Nov 2022 16:42:51 +0800 Subject: [PATCH 0042/1392] strategy/linregmaker: add dynamic quantity --- pkg/dynamicmetric/dynamic_exposure.go | 2 +- pkg/dynamicmetric/dynamic_quantity.go | 93 +++++++++++++++++++++++++++ pkg/strategy/linregmaker/strategy.go | 62 +++++++++++++++--- 3 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 pkg/dynamicmetric/dynamic_quantity.go diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go index aadd1ece3f..c2e1802a18 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -49,7 +49,7 @@ type DynamicExposureBollBand struct { dynamicExposureBollBand *indicator.BOLL } -// Initialize DynamicExposureBollBand +// initialize dynamic exposure with Bollinger Band func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession) { d.dynamicExposureBollBand = d.StandardIndicatorSet.BOLL(d.IntervalWindow, d.BandWidth) diff --git a/pkg/dynamicmetric/dynamic_quantity.go b/pkg/dynamicmetric/dynamic_quantity.go new file mode 100644 index 0000000000..ac2e85b387 --- /dev/null +++ b/pkg/dynamicmetric/dynamic_quantity.go @@ -0,0 +1,93 @@ +package dynamicmetric + +import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" + "github.com/pkg/errors" +) + +// DynamicQuantitySet uses multiple dynamic quantity rules to calculate the total quantity +type DynamicQuantitySet []DynamicQuantity + +// Initialize dynamic quantity set +func (d *DynamicQuantitySet) Initialize(symbol string, session *bbgo.ExchangeSession) { + for i := range *d { + (*d)[i].Initialize(symbol, session) + } +} + +// GetQuantity returns the quantity +func (d *DynamicQuantitySet) GetQuantity() (fixedpoint.Value, error) { + quantity := fixedpoint.Zero + for i := range *d { + v, err := (*d)[i].getQuantity() + if err != nil { + return fixedpoint.Zero, err + } + quantity = quantity.Add(v) + } + + return quantity, nil +} + +type DynamicQuantity struct { + // LinRegQty calculates quantity based on LinReg slope + LinRegDynamicQuantity *DynamicQuantityLinReg `json:"linRegDynamicQuantity"` +} + +// Initialize dynamic quantity +func (d *DynamicQuantity) Initialize(symbol string, session *bbgo.ExchangeSession) { + switch { + case d.LinRegDynamicQuantity != nil: + d.LinRegDynamicQuantity.initialize(symbol, session) + } +} + +func (d *DynamicQuantity) IsEnabled() bool { + return d.LinRegDynamicQuantity != nil +} + +// getQuantity returns quantity +func (d *DynamicQuantity) getQuantity() (fixedpoint.Value, error) { + switch { + case d.LinRegDynamicQuantity != nil: + return d.LinRegDynamicQuantity.getQuantity() + default: + return fixedpoint.Zero, errors.New("dynamic quantity is not enabled") + } +} + +// DynamicQuantityLinReg uses LinReg slope to calculate quantity +type DynamicQuantityLinReg struct { + // DynamicQuantityLinRegScale is used to define the quantity range with the given parameters. + DynamicQuantityLinRegScale *bbgo.PercentageScale `json:"dynamicQuantityLinRegScale"` + + // QuantityLinReg to define the interval and window of the LinReg + QuantityLinReg *indicator.LinReg `json:"quantityLinReg"` +} + +// initialize LinReg dynamic quantity +func (d *DynamicQuantityLinReg) initialize(symbol string, session *bbgo.ExchangeSession) { + // Subscribe for LinReg + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ + Interval: d.QuantityLinReg.Interval, + }) + + // Initialize LinReg + kLineStore, _ := session.MarketDataStore(symbol) + d.QuantityLinReg.BindK(session.MarketDataStream, symbol, d.QuantityLinReg.Interval) + if klines, ok := kLineStore.KLinesOfInterval(d.QuantityLinReg.Interval); ok { + d.QuantityLinReg.LoadK((*klines)[0:]) + } +} + +// getQuantity returns quantity +func (d *DynamicQuantityLinReg) getQuantity() (fixedpoint.Value, error) { + v, err := d.DynamicQuantityLinRegScale.Scale(d.QuantityLinReg.Last()) + if err != nil { + return fixedpoint.Zero, err + } + return fixedpoint.NewFromFloat(v), nil +} diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 154336d0f9..1eca593d81 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -48,8 +48,6 @@ type Strategy struct { types.IntervalWindow - bbgo.QuantityOrAmount - // ReverseEMA is used to determine the long-term trend. // Above the ReverseEMA is the long trend and vise versa. // All the opposite trend position will be closed upon the trend change @@ -109,15 +107,21 @@ type Strategy struct { DynamicSpread dynamicmetric.DynamicSpread `json:"dynamicSpread,omitempty"` // MaxExposurePosition is the maximum position you can hold - // +10 means you can hold 10 ETH long position by maximum - // -10 means you can hold -10 ETH short position by maximum + // 10 means you can hold 10 ETH long/short position by maximum MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` // DynamicExposure is used to define the exposure position range with the given percentage. - // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically according to the bollinger - // band you set. + // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` + bbgo.QuantityOrAmount + + // DynamicQuantityIncrease calculates the increase position order quantity dynamically + DynamicQuantityIncrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityIncrease"` + + // DynamicQuantityDecrease calculates the decrease position order quantity dynamically + DynamicQuantityDecrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityDecrease"` + session *bbgo.ExchangeSession // ExitMethods are various TP/SL methods @@ -197,6 +201,14 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { if s.DynamicExposure.IsEnabled() { s.DynamicExposure.Initialize(s.Symbol, session) } + + // Setup dynamic quantities + if len(s.DynamicQuantityIncrease) > 0 { + s.DynamicQuantityIncrease.Initialize(s.Symbol, session) + } + if len(s.DynamicQuantityDecrease) > 0 { + s.DynamicQuantityDecrease.Initialize(s.Symbol, session) + } } func (s *Strategy) Validate() error { @@ -266,10 +278,42 @@ func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoin // getOrderQuantities returns sell and buy qty func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { - // TODO: dynamic qty to determine qty // TODO: spot, margin, and futures - sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) - buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + + // Dynamic qty + switch { + case s.mainTrendCurrent == types.DirectionUp: + var err error + if len(s.DynamicQuantityIncrease) > 0 { + buyQuantity, err = s.DynamicQuantityIncrease.GetQuantity() + if err != nil { + buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + } + } + if len(s.DynamicQuantityDecrease) > 0 { + sellQuantity, err = s.DynamicQuantityDecrease.GetQuantity() + if err != nil { + sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + } + } + case s.mainTrendCurrent == types.DirectionDown: + var err error + if len(s.DynamicQuantityIncrease) > 0 { + sellQuantity, err = s.DynamicQuantityIncrease.GetQuantity() + if err != nil { + sellQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + } + } + if len(s.DynamicQuantityDecrease) > 0 { + buyQuantity, err = s.DynamicQuantityDecrease.GetQuantity() + if err != nil { + buyQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + } + } + default: + sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + } // Faster position decrease if s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0 { From da4a70109219f54bedf9f58d741f1b1dc184bf17 Mon Sep 17 00:00:00 2001 From: Yo-An Lin Date: Sun, 20 Nov 2022 21:00:58 +0800 Subject: [PATCH 0043/1392] Remove FTX --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 00fa0bcae9..f6e1a78165 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,6 @@ Get your exchange API key and secret after you register the accounts (you can ch - MAX: - Binance: -- FTX: - OKEx: - Kucoin: @@ -178,12 +177,6 @@ BINANCE_US=0 MAX_API_KEY= MAX_API_SECRET= -# for FTX exchange, if you have one -FTX_API_KEY= -FTX_API_SECRET= -# specify it if credentials are for subaccount -FTX_SUBACCOUNT= - # for OKEx exchange, if you have one OKEX_API_KEY= OKEX_API_SECRET= @@ -445,7 +438,6 @@ bbgo submit-order --session=okex --symbol=OKBUSDT --side=buy --price=10.0 --quan ```sh bbgo list-orders open --session=okex --symbol=OKBUSDT -bbgo list-orders open --session=ftx --symbol=FTTUSDT bbgo list-orders open --session=max --symbol=MAXUSDT bbgo list-orders open --session=binance --symbol=BNBUSDT ``` From 026d712f7580ae91b19322725279a058a8cf7b8a Mon Sep 17 00:00:00 2001 From: Yo-An Lin Date: Sun, 20 Nov 2022 21:01:37 +0800 Subject: [PATCH 0044/1392] doc: remove FTX from Supported Exchange --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f6e1a78165..e73bd531f4 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,6 @@ the implementation. ## Supported Exchanges - Binance Spot Exchange (and binance.us) -- FTX Spot Exchange - OKEx Spot Exchange - Kucoin Spot Exchange - MAX Spot Exchange (located in Taiwan) From 27800e95bd97de0c41f67443714f8586993e74ac Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 9 Nov 2022 17:43:51 +0900 Subject: [PATCH 0045/1392] feature: add cancel_replace for binance, add FastSubmitOrders, fix drift leakage on pendingOrderCounter --- pkg/bbgo/order_executor_general.go | 85 ++++- pkg/bbgo/tradecollector.go | 2 + pkg/exchange/binance/binanceapi/alias.go | 15 + .../binanceapi/cancel_replace_request.go | 47 +++ ...l_replace_spot_order_request_requestgen.go | 351 ++++++++++++++++++ pkg/exchange/binance/binanceapi/client.go | 24 +- pkg/exchange/binance/cancel_replace.go | 70 ++++ pkg/statistics/sortino.go | 1 - pkg/strategy/drift/strategy.go | 112 ++++-- pkg/types/order.go | 7 + 10 files changed, 653 insertions(+), 61 deletions(-) create mode 100644 pkg/exchange/binance/binanceapi/cancel_replace_request.go create mode 100644 pkg/exchange/binance/binanceapi/cancel_replace_spot_order_request_requestgen.go create mode 100644 pkg/exchange/binance/cancel_replace.go delete mode 100644 pkg/statistics/sortino.go diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 00a46bc90e..a1266fb4bc 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -144,19 +144,21 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { }) } -func (e *GeneralOrderExecutor) Bind() { +func (e *GeneralOrderExecutor) Bind(notify ...bool) { e.activeMakerOrders.BindStream(e.session.UserDataStream) e.orderStore.BindStream(e.session.UserDataStream) - // trade notify - e.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { - Notify(trade) - }) + if len(notify) > 0 && notify[0] { + // trade notify + e.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { + Notify(trade) + }) - e.tradeCollector.OnPositionUpdate(func(position *types.Position) { - log.Infof("position changed: %s", position) - Notify(position) - }) + e.tradeCollector.OnPositionUpdate(func(position *types.Position) { + log.Infof("position changed: %s", position) + Notify(position) + }) + } e.tradeCollector.BindStream(e.session.UserDataStream) } @@ -170,6 +172,30 @@ func (e *GeneralOrderExecutor) CancelOrders(ctx context.Context, orders ...types return err } +func (e *GeneralOrderExecutor) FastSubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { + formattedOrders, err := e.session.FormatOrders(submitOrders) + if err != nil { + return nil, err + } + createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, formattedOrders...) + if len(errIdx) > 0 { + return nil, err + } + if IsBackTesting { + e.orderStore.Add(createdOrders...) + e.activeMakerOrders.Add(createdOrders...) + e.tradeCollector.Process() + } else { + go func() { + e.orderStore.Add(createdOrders...) + e.activeMakerOrders.Add(createdOrders...) + e.tradeCollector.Process() + }() + } + return createdOrders, err + +} + func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { formattedOrders, err := e.session.FormatOrders(submitOrders) if err != nil { @@ -262,7 +288,7 @@ func (e *GeneralOrderExecutor) reduceQuantityAndSubmitOrder(ctx context.Context, return nil, multierr.Append(ErrExceededSubmitOrderRetryLimit, err) } -func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) (types.OrderSlice, error) { +func (e *GeneralOrderExecutor) OpenPositionPrepare(ctx context.Context, options *OpenPositionOptions) (*types.SubmitOrder, error) { price := options.Price submitOrder := types.SubmitOrder{ Symbol: e.position.Symbol, @@ -284,9 +310,11 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos if options.Long { // use higher price to buy (this ensures that our order will be filled) price = price.Mul(one.Add(options.LimitOrderTakerRatio)) + options.Price = price } else if options.Short { // use lower price to sell (this ensures that our order will be filled) price = price.Mul(one.Sub(options.LimitOrderTakerRatio)) + options.Price = price } } @@ -320,14 +348,7 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos submitOrder.Side = types.SideTypeBuy submitOrder.Quantity = quantity - Notify("Opening %s long position with quantity %v at price %v", e.position.Symbol, quantity, price) - - createdOrder, err := e.SubmitOrders(ctx, submitOrder) - if err == nil { - return createdOrder, nil - } - - return e.reduceQuantityAndSubmitOrder(ctx, price, submitOrder) + return &submitOrder, nil } else if options.Short { if quantity.IsZero() { var err error @@ -350,13 +371,37 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos submitOrder.Side = types.SideTypeSell submitOrder.Quantity = quantity - Notify("Opening %s short position with quantity %v at price %v", e.position.Symbol, quantity, price) - return e.reduceQuantityAndSubmitOrder(ctx, price, submitOrder) + return &submitOrder, nil } return nil, errors.New("options Long or Short must be set") } +func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) (types.OrderSlice, error) { + submitOrder, err := e.OpenPositionPrepare(ctx, &options) + if err != nil { + return nil, err + } + if submitOrder == nil { + return nil, nil + } + price := options.Price + + side := "long" + if submitOrder.Side == types.SideTypeSell { + side = "short" + } + + Notify("Opening %s %s position with quantity %v at price %v", e.position.Symbol, side, submitOrder.Quantity, price) + + createdOrder, err := e.SubmitOrders(ctx, *submitOrder) + if err == nil { + return createdOrder, nil + } + + return e.reduceQuantityAndSubmitOrder(ctx, price, *submitOrder) +} + // GracefulCancelActiveOrderBook cancels the orders from the active orderbook. func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook) error { if activeOrders.NumOfOrders() == 0 { diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go index 14c18d484e..c898f3da6e 100644 --- a/pkg/bbgo/tradecollector.go +++ b/pkg/bbgo/tradecollector.go @@ -197,9 +197,11 @@ func (c *TradeCollector) processTrade(trade types.Trade) bool { func (c *TradeCollector) ProcessTrade(trade types.Trade) bool { key := trade.Key() // if it's already done, remove the trade from the trade store + c.mu.Lock() if _, done := c.doneTrades[key]; done { return false } + c.mu.Unlock() if c.processTrade(trade) { return true diff --git a/pkg/exchange/binance/binanceapi/alias.go b/pkg/exchange/binance/binanceapi/alias.go index f17052bdf3..0f6aa9b621 100644 --- a/pkg/exchange/binance/binanceapi/alias.go +++ b/pkg/exchange/binance/binanceapi/alias.go @@ -32,3 +32,18 @@ const ( OrderStatusTypeRejected OrderStatusType = binance.OrderStatusTypeRejected OrderStatusTypeExpired OrderStatusType = binance.OrderStatusTypeExpired ) + +type CancelReplaceModeType string + +const ( + StopOnFailure CancelReplaceModeType = "STOP_ON_FAILURE" + AllowFailure CancelReplaceModeType = "ALLOW_FAILURE" +) + +type OrderRespType string + +const ( + Ack OrderRespType = "ACK" + Result OrderRespType = "RESULT" + Full OrderRespType = "FULL" +) diff --git a/pkg/exchange/binance/binanceapi/cancel_replace_request.go b/pkg/exchange/binance/binanceapi/cancel_replace_request.go new file mode 100644 index 0000000000..4494a204ac --- /dev/null +++ b/pkg/exchange/binance/binanceapi/cancel_replace_request.go @@ -0,0 +1,47 @@ +package binanceapi + +import ( + "github.com/adshao/go-binance/v2" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +type CancelReplaceSpotOrderData struct { + CancelResult string `json:"cancelResult"` + NewOrderResult string `json:"newOrderResult"` + NewOrderResponse *binance.Order `json:"newOrderResponse"` +} + +type CancelReplaceSpotOrderResponse struct { + Code int `json:"code,omitempty"` + Msg string `json:"msg,omitempty"` + Data *CancelReplaceSpotOrderData `json:"data"` +} + +//go:generate requestgen -method POST -url "/api/v3/order/cancelReplace" -type CancelReplaceSpotOrderRequest -responseType .CancelReplaceSpotOrderResponse +type CancelReplaceSpotOrderRequest struct { + client requestgen.AuthenticatedAPIClient + symbol string `param:"symbol"` + side SideType `param:"side"` + cancelReplaceMode CancelReplaceModeType `param:"cancelReplaceMode"` + timeInForce string `param:"timeInForce"` + quantity string `param:"quantity"` + quoteOrderQty string `param:"quoteOrderQty"` + price string `param:"price"` + cancelNewClientOrderId string `param:"cancelNewClientOrderId"` + cancelOrigClientOrderId string `param:"cancelOrigClientOrderId"` + cancelOrderId int `param:"cancelOrderId"` + newClientOrderId string `param:"newClientOrderId"` + strategyId int `param:"strategyId"` + strategyType int `param:"strategyType"` + stopPrice string `param:"stopPrice"` + trailingDelta int `param:"trailingDelta"` + icebergQty string `param:"icebergQty"` + newOrderRespType OrderRespType `param:"newOrderRespType"` + recvWindow int `param:"recvWindow"` + timestamp types.MillisecondTimestamp `param:"timestamp"` +} + +func (c *RestClient) NewCancelReplaceSpotOrderRequest() *CancelReplaceSpotOrderRequest { + return &CancelReplaceSpotOrderRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/cancel_replace_spot_order_request_requestgen.go b/pkg/exchange/binance/binanceapi/cancel_replace_spot_order_request_requestgen.go new file mode 100644 index 0000000000..bc32eea530 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/cancel_replace_spot_order_request_requestgen.go @@ -0,0 +1,351 @@ +// Code generated by "requestgen -method POST -url /api/v3/order/cancelReplace -type CancelReplaceSpotOrderRequest -responseType .CancelReplaceSpotOrderResponse"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/adshao/go-binance/v2" + "github.com/c9s/bbgo/pkg/types" + "net/url" + "reflect" + "regexp" +) + +func (c *CancelReplaceSpotOrderRequest) Symbol(symbol string) *CancelReplaceSpotOrderRequest { + c.symbol = symbol + return c +} + +func (c *CancelReplaceSpotOrderRequest) Side(side binance.SideType) *CancelReplaceSpotOrderRequest { + c.side = side + return c +} + +func (c *CancelReplaceSpotOrderRequest) CancelReplaceMode(cancelReplaceMode CancelReplaceModeType) *CancelReplaceSpotOrderRequest { + c.cancelReplaceMode = cancelReplaceMode + return c +} + +func (c *CancelReplaceSpotOrderRequest) TimeInForce(timeInForce string) *CancelReplaceSpotOrderRequest { + c.timeInForce = timeInForce + return c +} + +func (c *CancelReplaceSpotOrderRequest) Quantity(quantity string) *CancelReplaceSpotOrderRequest { + c.quantity = quantity + return c +} + +func (c *CancelReplaceSpotOrderRequest) QuoteOrderQty(quoteOrderQty string) *CancelReplaceSpotOrderRequest { + c.quoteOrderQty = quoteOrderQty + return c +} + +func (c *CancelReplaceSpotOrderRequest) Price(price string) *CancelReplaceSpotOrderRequest { + c.price = price + return c +} + +func (c *CancelReplaceSpotOrderRequest) CancelNewClientOrderId(cancelNewClientOrderId string) *CancelReplaceSpotOrderRequest { + c.cancelNewClientOrderId = cancelNewClientOrderId + return c +} + +func (c *CancelReplaceSpotOrderRequest) CancelOrigClientOrderId(cancelOrigClientOrderId string) *CancelReplaceSpotOrderRequest { + c.cancelOrigClientOrderId = cancelOrigClientOrderId + return c +} + +func (c *CancelReplaceSpotOrderRequest) CancelOrderId(cancelOrderId int) *CancelReplaceSpotOrderRequest { + c.cancelOrderId = cancelOrderId + return c +} + +func (c *CancelReplaceSpotOrderRequest) NewClientOrderId(newClientOrderId string) *CancelReplaceSpotOrderRequest { + c.newClientOrderId = newClientOrderId + return c +} + +func (c *CancelReplaceSpotOrderRequest) StrategyId(strategyId int) *CancelReplaceSpotOrderRequest { + c.strategyId = strategyId + return c +} + +func (c *CancelReplaceSpotOrderRequest) StrategyType(strategyType int) *CancelReplaceSpotOrderRequest { + c.strategyType = strategyType + return c +} + +func (c *CancelReplaceSpotOrderRequest) StopPrice(stopPrice string) *CancelReplaceSpotOrderRequest { + c.stopPrice = stopPrice + return c +} + +func (c *CancelReplaceSpotOrderRequest) TrailingDelta(trailingDelta int) *CancelReplaceSpotOrderRequest { + c.trailingDelta = trailingDelta + return c +} + +func (c *CancelReplaceSpotOrderRequest) IcebergQty(icebergQty string) *CancelReplaceSpotOrderRequest { + c.icebergQty = icebergQty + return c +} + +func (c *CancelReplaceSpotOrderRequest) NewOrderRespType(newOrderRespType OrderRespType) *CancelReplaceSpotOrderRequest { + c.newOrderRespType = newOrderRespType + return c +} + +func (c *CancelReplaceSpotOrderRequest) RecvWindow(recvWindow int) *CancelReplaceSpotOrderRequest { + c.recvWindow = recvWindow + return c +} + +func (c *CancelReplaceSpotOrderRequest) Timestamp(timestamp types.MillisecondTimestamp) *CancelReplaceSpotOrderRequest { + c.timestamp = timestamp + return c +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (c *CancelReplaceSpotOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (c *CancelReplaceSpotOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := c.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check side field -> json key side + side := c.side + + // assign parameter of side + params["side"] = side + // check cancelReplaceMode field -> json key cancelReplaceMode + cancelReplaceMode := c.cancelReplaceMode + + // TEMPLATE check-valid-values + switch cancelReplaceMode { + case StopOnFailure, AllowFailure: + params["cancelReplaceMode"] = cancelReplaceMode + + default: + return nil, fmt.Errorf("cancelReplaceMode value %v is invalid", cancelReplaceMode) + + } + // END TEMPLATE check-valid-values + + // assign parameter of cancelReplaceMode + params["cancelReplaceMode"] = cancelReplaceMode + // check timeInForce field -> json key timeInForce + timeInForce := c.timeInForce + + // assign parameter of timeInForce + params["timeInForce"] = timeInForce + // check quantity field -> json key quantity + quantity := c.quantity + + // assign parameter of quantity + params["quantity"] = quantity + // check quoteOrderQty field -> json key quoteOrderQty + quoteOrderQty := c.quoteOrderQty + + // assign parameter of quoteOrderQty + params["quoteOrderQty"] = quoteOrderQty + // check price field -> json key price + price := c.price + + // assign parameter of price + params["price"] = price + // check cancelNewClientOrderId field -> json key cancelNewClientOrderId + cancelNewClientOrderId := c.cancelNewClientOrderId + + // assign parameter of cancelNewClientOrderId + params["cancelNewClientOrderId"] = cancelNewClientOrderId + // check cancelOrigClientOrderId field -> json key cancelOrigClientOrderId + cancelOrigClientOrderId := c.cancelOrigClientOrderId + + // assign parameter of cancelOrigClientOrderId + params["cancelOrigClientOrderId"] = cancelOrigClientOrderId + // check cancelOrderId field -> json key cancelOrderId + cancelOrderId := c.cancelOrderId + + // assign parameter of cancelOrderId + params["cancelOrderId"] = cancelOrderId + // check newClientOrderId field -> json key newClientOrderId + newClientOrderId := c.newClientOrderId + + // assign parameter of newClientOrderId + params["newClientOrderId"] = newClientOrderId + // check strategyId field -> json key strategyId + strategyId := c.strategyId + + // assign parameter of strategyId + params["strategyId"] = strategyId + // check strategyType field -> json key strategyType + strategyType := c.strategyType + + // assign parameter of strategyType + params["strategyType"] = strategyType + // check stopPrice field -> json key stopPrice + stopPrice := c.stopPrice + + // assign parameter of stopPrice + params["stopPrice"] = stopPrice + // check trailingDelta field -> json key trailingDelta + trailingDelta := c.trailingDelta + + // assign parameter of trailingDelta + params["trailingDelta"] = trailingDelta + // check icebergQty field -> json key icebergQty + icebergQty := c.icebergQty + + // assign parameter of icebergQty + params["icebergQty"] = icebergQty + // check newOrderRespType field -> json key newOrderRespType + newOrderRespType := c.newOrderRespType + + // TEMPLATE check-valid-values + switch newOrderRespType { + case Ack, Result, Full: + params["newOrderRespType"] = newOrderRespType + + default: + return nil, fmt.Errorf("newOrderRespType value %v is invalid", newOrderRespType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of newOrderRespType + params["newOrderRespType"] = newOrderRespType + // check recvWindow field -> json key recvWindow + recvWindow := c.recvWindow + + // assign parameter of recvWindow + params["recvWindow"] = recvWindow + // check timestamp field -> json key timestamp + timestamp := c.timestamp + + // assign parameter of timestamp + params["timestamp"] = timestamp + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (c *CancelReplaceSpotOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := c.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if c.isVarSlice(_v) { + c.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (c *CancelReplaceSpotOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := c.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (c *CancelReplaceSpotOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (c *CancelReplaceSpotOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (c *CancelReplaceSpotOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (c *CancelReplaceSpotOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (c *CancelReplaceSpotOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := c.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (c *CancelReplaceSpotOrderRequest) Do(ctx context.Context) (*CancelReplaceSpotOrderResponse, error) { + + params, err := c.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/v3/order/cancelReplace" + + req, err := c.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := c.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse CancelReplaceSpotOrderResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/client.go b/pkg/exchange/binance/binanceapi/client.go index 0fa05db422..d5faf9bb71 100644 --- a/pkg/exchange/binance/binanceapi/client.go +++ b/pkg/exchange/binance/binanceapi/client.go @@ -5,8 +5,10 @@ import ( "context" "crypto/hmac" "crypto/sha256" + "crypto/tls" "encoding/json" "fmt" + "net" "net/http" "net/url" "strconv" @@ -19,13 +21,31 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -const defaultHTTPTimeout = time.Second * 15 +const defaultHTTPTimeout = time.Second * 2 const RestBaseURL = "https://api.binance.com" const SandboxRestBaseURL = "https://testnet.binance.vision" const DebugRequestResponse = false +var Dialer = &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, +} + +var DefaultTransport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: Dialer.DialContext, + MaxIdleConns: 100, + MaxConnsPerHost: 100, + MaxIdleConnsPerHost: 100, + //TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper), + ExpectContinueTimeout: 0, + ForceAttemptHTTP2: true, + TLSClientConfig: &tls.Config{}, +} + var DefaultHttpClient = &http.Client{ - Timeout: defaultHTTPTimeout, + Timeout: defaultHTTPTimeout, + Transport: DefaultTransport, } type RestClient struct { diff --git a/pkg/exchange/binance/cancel_replace.go b/pkg/exchange/binance/cancel_replace.go new file mode 100644 index 0000000000..c993c66d03 --- /dev/null +++ b/pkg/exchange/binance/cancel_replace.go @@ -0,0 +1,70 @@ +package binance + +import ( + "context" + "fmt" + + "github.com/adshao/go-binance/v2" + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" + "github.com/c9s/bbgo/pkg/types" +) + +func (e *Exchange) CancelReplace(ctx context.Context, cancelReplaceMode types.CancelReplaceModeType, o types.Order) (*types.Order, error) { + if err := orderLimiter.Wait(ctx); err != nil { + log.WithError(err).Errorf("order rate limiter wait error") + } + + if e.IsFutures || e.IsMargin { + // Not supported at the moment + return nil, nil + } + var req = e.client2.NewCancelReplaceSpotOrderRequest() + req.Symbol(o.Symbol) + req.Side(binance.SideType(o.Side)) + if o.OrderID > 0 { + req.CancelOrderId(int(o.OrderID)) + } else { + return nil, types.NewOrderError(fmt.Errorf("cannot cancel %s order", o.Symbol), o) + } + req.CancelReplaceMode(binanceapi.CancelReplaceModeType(cancelReplaceMode)) + if len(o.TimeInForce) > 0 { + // TODO: check the TimeInForce value + req.TimeInForce(string(binance.TimeInForceType(o.TimeInForce))) + } else { + switch o.Type { + case types.OrderTypeLimit, types.OrderTypeStopLimit: + req.TimeInForce(string(binance.TimeInForceTypeGTC)) + } + } + if o.Market.Symbol != "" { + req.Quantity(o.Market.FormatQuantity(o.Quantity)) + } else { + req.Quantity(o.Quantity.FormatString(8)) + } + + switch o.Type { + case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker: + if o.Market.Symbol != "" { + req.Price(o.Market.FormatPrice(o.Price)) + } else { + // TODO: report error + req.Price(o.Price.FormatString(8)) + } + } + switch o.Type { + case types.OrderTypeStopLimit, types.OrderTypeStopMarket: + if o.Market.Symbol != "" { + req.StopPrice(o.Market.FormatPrice(o.StopPrice)) + } else { + // TODO report error + req.StopPrice(o.StopPrice.FormatString(8)) + } + } + req.NewOrderRespType(binanceapi.Full) + + resp, err := req.Do(ctx) + if resp != nil && resp.Data != nil && resp.Data.NewOrderResponse != nil { + return toGlobalOrder(resp.Data.NewOrderResponse, e.IsMargin) + } + return nil, err +} diff --git a/pkg/statistics/sortino.go b/pkg/statistics/sortino.go deleted file mode 100644 index 12c0dbc25e..0000000000 --- a/pkg/statistics/sortino.go +++ /dev/null @@ -1 +0,0 @@ -package statistics diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 74782e2e97..3f0adb4083 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -186,10 +186,10 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu order.MarginSideEffect = types.SideEffectTypeAutoRepay for i := 0; i < closeOrderRetryLimit; i++ { price := s.getLastPrice() - balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() - baseBalance := balances[s.Market.BaseCurrency].Available + balances := s.Session.GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Total() if order.Side == types.SideTypeBuy { - quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price) + quoteAmount := balances[s.Market.QuoteCurrency].Total().Div(price) if order.Quantity.Compare(quoteAmount) > 0 { order.Quantity = quoteAmount } @@ -199,11 +199,19 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu if s.Market.IsDustQuantity(order.Quantity, price) { return nil } - _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order) + createdOrders, err := s.GeneralOrderExecutor.FastSubmitOrders(ctx, *order) if err != nil { order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) continue } + + if createdOrders != nil { + for _, o := range createdOrders { + if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled { + log.Errorf("created Order when Close: %v", o) + } + } + } return nil } return errors.New("exceed retry limit") @@ -300,12 +308,13 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64, syscoun } if toCancel { err := s.GeneralOrderExecutor.FastCancel(ctx) + s.pendingLock.Lock() + counters := s.orderPendingCounter + s.orderPendingCounter = make(map[uint64]int) + s.pendingLock.Unlock() // TODO: clean orderPendingCounter on cancel/trade for _, order := range nonTraded { - s.pendingLock.Lock() - counter := s.orderPendingCounter[order.OrderID] - delete(s.orderPendingCounter, order.OrderID) - s.pendingLock.Unlock() + counter := counters[order.OrderID] if order.Side == types.SideTypeSell { if s.maxCounterSellCanceled < counter { s.maxCounterSellCanceled = counter @@ -514,14 +523,14 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter sourcef := source.Float64() s.priceLines.Update(sourcef) - // s.ma.Update(sourcef) + s.ma.Update(sourcef) s.trendLine.Update(sourcef) s.drift.Update(sourcef, kline.Volume.Abs().Float64()) s.atr.PushK(kline) atr := s.atr.Last() - price := kline.Close // s.getLastPrice() + price := kline.Close //s.getLastPrice() pricef := price.Float64() lowf := math.Min(kline.Low.Float64(), pricef) highf := math.Max(kline.High.Float64(), pricef) @@ -600,6 +609,10 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter } return } + if exitCondition { + _ = s.ClosePosition(ctx, fixedpoint.One) + return + } if longCondition { source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier)) @@ -607,19 +620,18 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter source = price } - log.Infof("source in long %v %v %f", source, price, s.stdevLow.Last()) - opt := s.OpenPositionOptions opt.Long = true opt.LimitOrder = true // force to use market taker - if counter-s.maxCounterBuyCanceled <= s.PendingMinInterval { + if counter-s.maxCounterBuyCanceled <= s.PendingMinInterval && s.maxCounterBuyCanceled > s.maxCounterSellCanceled { opt.LimitOrder = false + source = price } opt.Price = source opt.Tags = []string{"long"} - createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) + submitOrder, err := s.GeneralOrderExecutor.OpenPositionPrepare(ctx, &opt) if err != nil { errs := filterErrors(multierr.Errors(err)) if len(errs) > 0 { @@ -628,14 +640,23 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter } return } + if submitOrder == nil { + return + } + + log.Infof("source in long %v %v %f", source, price, s.stdevLow.Last()) + + createdOrders, err := s.GeneralOrderExecutor.FastSubmitOrders(ctx, *submitOrder) + if err != nil { + log.WithError(err).Errorf("cannot place buy order") + return + } log.Infof("orders %v", createdOrders) if createdOrders != nil { for _, o := range createdOrders { if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled { - s.pendingLock.Lock() s.orderPendingCounter[o.OrderID] = counter - s.pendingLock.Unlock() } } } @@ -647,17 +668,18 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter source = price } - log.Infof("source in short: %v", source) + log.Infof("source in short: %v %v %f", source, price, s.stdevLow.Last()) opt := s.OpenPositionOptions opt.Short = true - opt.Price = source opt.LimitOrder = true - if counter-s.maxCounterSellCanceled <= s.PendingMinInterval { + if counter-s.maxCounterSellCanceled <= s.PendingMinInterval && s.maxCounterSellCanceled > s.maxCounterBuyCanceled { opt.LimitOrder = false + source = price } + opt.Price = source opt.Tags = []string{"short"} - createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) + submitOrder, err := s.GeneralOrderExecutor.OpenPositionPrepare(ctx, &opt) if err != nil { errs := filterErrors(multierr.Errors(err)) if len(errs) > 0 { @@ -665,6 +687,14 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter } return } + if submitOrder == nil { + return + } + createdOrders, err := s.GeneralOrderExecutor.FastSubmitOrders(ctx, *submitOrder) + if err != nil { + log.WithError(err).Errorf("cannot place sell order") + return + } log.Infof("orders %v", createdOrders) if createdOrders != nil { for _, o := range createdOrders { @@ -712,22 +742,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se _ = s.ClosePosition(ctx, fixedpoint.One) }) - s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) - s.GeneralOrderExecutor.BindEnvironment(s.Environment) - s.GeneralOrderExecutor.BindProfitStats(s.ProfitStats) - s.GeneralOrderExecutor.BindTradeStats(s.TradeStats) - s.GeneralOrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { - bbgo.Sync(ctx, s) - }) - s.GeneralOrderExecutor.Bind() - - s.orderPendingCounter = make(map[uint64]int) - - // Exit methods from config - for _, method := range s.ExitMethods { - method.Bind(session, s.GeneralOrderExecutor) - } - profit := floats.Slice{1., 1.} price, _ := s.Session.LastPrice(s.Symbol) initAsset := s.CalcAssetValue(price).Float64() @@ -740,9 +754,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return p * (1. - Fee) } } - s.GeneralOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _profit, _netProfit fixedpoint.Value) { - s.p.AddTrade(trade) + s.Session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { + if trade.Symbol != s.Symbol { + return + } price := trade.Price.Float64() + s.p.AddTrade(trade) s.pendingLock.Lock() delete(s.orderPendingCounter, trade.OrderID) s.pendingLock.Unlock() @@ -778,6 +795,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.positionLock.Unlock() }) + s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.GeneralOrderExecutor.BindEnvironment(s.Environment) + s.GeneralOrderExecutor.BindProfitStats(s.ProfitStats) + s.GeneralOrderExecutor.BindTradeStats(s.TradeStats) + s.GeneralOrderExecutor.Bind() + + s.orderPendingCounter = make(map[uint64]int) + + // Exit methods from config + for _, method := range s.ExitMethods { + method.Bind(session, s.GeneralOrderExecutor) + } + s.frameKLine = &types.KLine{} s.klineMin = &types.KLine{} s.priceLines = types.NewQueue(300) @@ -837,6 +867,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + if !bbgo.IsBackTesting { + bbgo.Sync(ctx, s) + } + var buffer bytes.Buffer s.Print(&buffer, true, true) @@ -847,6 +881,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } fmt.Fprintln(&buffer, s.TradeStats.BriefString()) + fmt.Fprintf(&buffer, "%v\n", s.orderPendingCounter) + os.Stdout.Write(buffer.Bytes()) if s.GenerateGraph { diff --git a/pkg/types/order.go b/pkg/types/order.go index b206ace821..1b33ea64f5 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -20,6 +20,13 @@ func init() { _ = PlainText(&Order{}) } +type CancelReplaceModeType string + +var ( + StopOnFailure CancelReplaceModeType = "STOP_ON_FAILURE" + AllowFailure CancelReplaceModeType = "ALLOW_FAILURE" +) + type TimeInForce string var ( From 109f4d0e3ec7c1091d117f27a70b8ce5d84703a6 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 16 Nov 2022 18:10:58 +0900 Subject: [PATCH 0046/1392] fix: Position not synchronized in drift. add DisableNotify for GeneralOrderExecutor --- pkg/bbgo/order_executor_general.go | 15 +- pkg/bbgo/order_store.go | 8 +- pkg/bbgo/tradecollector.go | 4 + pkg/exchange/binance/binanceapi/client.go | 8 +- pkg/strategy/drift/strategy.go | 217 ++++++++++++++-------- 5 files changed, 167 insertions(+), 85 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index a1266fb4bc..d23508105b 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -38,7 +38,8 @@ type GeneralOrderExecutor struct { marginBaseMaxBorrowable, marginQuoteMaxBorrowable fixedpoint.Value - closing int64 + disableNotify bool + closing int64 } func NewGeneralOrderExecutor(session *ExchangeSession, symbol, strategy, strategyInstanceID string, position *types.Position) *GeneralOrderExecutor { @@ -66,6 +67,10 @@ func NewGeneralOrderExecutor(session *ExchangeSession, symbol, strategy, strateg return executor } +func (e *GeneralOrderExecutor) DisableNotify() { + e.disableNotify = true +} + func (e *GeneralOrderExecutor) startMarginAssetUpdater(ctx context.Context) { marginService, ok := e.session.Exchange.(types.MarginBorrowRepayService) if !ok { @@ -110,6 +115,10 @@ func (e *GeneralOrderExecutor) marginAssetMaxBorrowableUpdater(ctx context.Conte } } +func (e *GeneralOrderExecutor) OrderStore() *OrderStore { + return e.orderStore +} + func (e *GeneralOrderExecutor) ActiveMakerOrders() *ActiveOrderBook { return e.activeMakerOrders } @@ -144,11 +153,11 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { }) } -func (e *GeneralOrderExecutor) Bind(notify ...bool) { +func (e *GeneralOrderExecutor) Bind() { e.activeMakerOrders.BindStream(e.session.UserDataStream) e.orderStore.BindStream(e.session.UserDataStream) - if len(notify) > 0 && notify[0] { + if !e.disableNotify { // trade notify e.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { Notify(trade) diff --git a/pkg/bbgo/order_store.go b/pkg/bbgo/order_store.go index 473473e09e..aa1b3c9fbc 100644 --- a/pkg/bbgo/order_store.go +++ b/pkg/bbgo/order_store.go @@ -89,6 +89,10 @@ func (s *OrderStore) Add(orders ...types.Order) { defer s.mu.Unlock() for _, o := range orders { + old, ok := s.orders[o.OrderID] + if ok && o.Tag == "" && old.Tag != "" { + o.Tag = old.Tag + } s.orders[o.OrderID] = o } } @@ -120,11 +124,11 @@ func (s *OrderStore) BindStream(stream types.Stream) { return } - s.handleOrderUpdate(order) + s.HandleOrderUpdate(order) }) } -func (s *OrderStore) handleOrderUpdate(order types.Order) { +func (s *OrderStore) HandleOrderUpdate(order types.Order) { switch order.Status { case types.OrderStatusNew, types.OrderStatusPartiallyFilled, types.OrderStatusFilled: diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go index c898f3da6e..8af9b38f7c 100644 --- a/pkg/bbgo/tradecollector.go +++ b/pkg/bbgo/tradecollector.go @@ -56,6 +56,10 @@ func (c *TradeCollector) Position() *types.Position { return c.position } +func (c *TradeCollector) TradeStore() *TradeStore { + return c.tradeStore +} + func (c *TradeCollector) SetPosition(position *types.Position) { c.position = position } diff --git a/pkg/exchange/binance/binanceapi/client.go b/pkg/exchange/binance/binanceapi/client.go index d5faf9bb71..a587cf7480 100644 --- a/pkg/exchange/binance/binanceapi/client.go +++ b/pkg/exchange/binance/binanceapi/client.go @@ -26,14 +26,14 @@ const RestBaseURL = "https://api.binance.com" const SandboxRestBaseURL = "https://testnet.binance.vision" const DebugRequestResponse = false -var Dialer = &net.Dialer{ +var dialer = &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, } -var DefaultTransport = &http.Transport{ +var defaultTransport = &http.Transport{ Proxy: http.ProxyFromEnvironment, - DialContext: Dialer.DialContext, + DialContext: dialer.DialContext, MaxIdleConns: 100, MaxConnsPerHost: 100, MaxIdleConnsPerHost: 100, @@ -45,7 +45,7 @@ var DefaultTransport = &http.Transport{ var DefaultHttpClient = &http.Client{ Timeout: defaultHTTPTimeout, - Transport: DefaultTransport, + Transport: defaultTransport, } type RestClient struct { diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 3f0adb4083..2579129d0c 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -64,7 +64,6 @@ type Strategy struct { *types.ProfitStats `persistence:"profit_stats"` *types.TradeStats `persistence:"trade_stats"` - p *types.Position MinInterval types.Interval `json:"MinInterval"` // minimum interval referred for doing stoploss/trailing exists and updating highest/lowest elapsed *types.Queue @@ -173,10 +172,22 @@ func (s *Strategy) CurrentPosition() *types.Position { return s.Position } +func (s *Strategy) SubmitOrder(ctx context.Context, submitOrder types.SubmitOrder) (*types.Order, error) { + formattedOrder, err := s.Session.FormatOrder(submitOrder) + if err != nil { + return nil, err + } + createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.Session.Exchange, formattedOrder) + if len(errIdx) > 0 { + return nil, err + } + return &createdOrders[0], err +} + const closeOrderRetryLimit = 5 func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { - order := s.p.NewMarketCloseOrder(percentage) + order := s.Position.NewMarketCloseOrder(percentage) if order == nil { return nil } @@ -199,17 +210,15 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu if s.Market.IsDustQuantity(order.Quantity, price) { return nil } - createdOrders, err := s.GeneralOrderExecutor.FastSubmitOrders(ctx, *order) + o, err := s.SubmitOrder(ctx, *order) if err != nil { order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) continue } - if createdOrders != nil { - for _, o := range createdOrders { - if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled { - log.Errorf("created Order when Close: %v", o) - } + if o != nil { + if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled { + log.Errorf("created Order when Close: %v", o) } } return nil @@ -420,47 +429,47 @@ func (s *Strategy) Rebalance(ctx context.Context) { quoteBalance := balances[s.Market.QuoteCurrency].Total() total := baseBalance.Add(quoteBalance.Div(price)) percentage := fixedpoint.One.Sub(Delta) - log.Infof("rebalance beta %f %v", beta, s.p) + log.Infof("rebalance beta %f %v", beta, s.Position) if beta > s.RebalanceFilter { if total.Mul(percentage).Compare(baseBalance) > 0 { q := total.Mul(percentage).Sub(baseBalance) - s.p.Lock() - defer s.p.Unlock() - s.p.Base = q.Neg() - s.p.Quote = q.Mul(price) - s.p.AverageCost = price + s.Position.Lock() + defer s.Position.Unlock() + s.Position.Base = q.Neg() + s.Position.Quote = q.Mul(price) + s.Position.AverageCost = price } } else if beta <= -s.RebalanceFilter { if total.Mul(percentage).Compare(quoteBalance.Div(price)) > 0 { q := total.Mul(percentage).Sub(quoteBalance.Div(price)) - s.p.Lock() - defer s.p.Unlock() - s.p.Base = q - s.p.Quote = q.Mul(price).Neg() - s.p.AverageCost = price + s.Position.Lock() + defer s.Position.Unlock() + s.Position.Base = q + s.Position.Quote = q.Mul(price).Neg() + s.Position.AverageCost = price } } else { if total.Div(Two).Compare(quoteBalance.Div(price)) > 0 { q := total.Div(Two).Sub(quoteBalance.Div(price)) - s.p.Lock() - defer s.p.Unlock() - s.p.Base = q - s.p.Quote = q.Mul(price).Neg() - s.p.AverageCost = price + s.Position.Lock() + defer s.Position.Unlock() + s.Position.Base = q + s.Position.Quote = q.Mul(price).Neg() + s.Position.AverageCost = price } else if total.Div(Two).Compare(baseBalance) > 0 { q := total.Div(Two).Sub(baseBalance) - s.p.Lock() - defer s.p.Unlock() - s.p.Base = q.Neg() - s.p.Quote = q.Mul(price) - s.p.AverageCost = price + s.Position.Lock() + defer s.Position.Unlock() + s.Position.Base = q.Neg() + s.Position.Quote = q.Mul(price) + s.Position.AverageCost = price } else { - s.p.Lock() - defer s.p.Unlock() - s.p.Reset() + s.Position.Lock() + defer s.Position.Unlock() + s.Position.Reset() } } - log.Infof("rebalanceafter %v %v %v", baseBalance, quoteBalance, s.p) + log.Infof("rebalanceafter %v %v %v", baseBalance, quoteBalance, s.Position) s.beta = beta } @@ -646,18 +655,20 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter log.Infof("source in long %v %v %f", source, price, s.stdevLow.Last()) - createdOrders, err := s.GeneralOrderExecutor.FastSubmitOrders(ctx, *submitOrder) + o, err := s.SubmitOrder(ctx, *submitOrder) if err != nil { log.WithError(err).Errorf("cannot place buy order") return } - log.Infof("orders %v", createdOrders) - if createdOrders != nil { - for _, o := range createdOrders { - if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled { + log.Infof("order %v", o) + if o != nil { + if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled { + s.pendingLock.Lock() + if _, ok := s.orderPendingCounter[o.OrderID]; !ok { s.orderPendingCounter[o.OrderID] = counter } + s.pendingLock.Unlock() } } return @@ -690,19 +701,19 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter if submitOrder == nil { return } - createdOrders, err := s.GeneralOrderExecutor.FastSubmitOrders(ctx, *submitOrder) + o, err := s.SubmitOrder(ctx, *submitOrder) if err != nil { log.WithError(err).Errorf("cannot place sell order") return } - log.Infof("orders %v", createdOrders) - if createdOrders != nil { - for _, o := range createdOrders { - if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled { - s.pendingLock.Lock() + log.Infof("order %v", o) + if o != nil { + if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled { + s.pendingLock.Lock() + if _, ok := s.orderPendingCounter[o.OrderID]; !ok { s.orderPendingCounter[o.OrderID] = counter - s.pendingLock.Unlock() } + s.pendingLock.Unlock() } } return @@ -717,12 +728,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Will be set by persistence if there's any from DB if s.Position == nil { s.Position = types.NewPositionFromMarket(s.Market) - s.p = types.NewPositionFromMarket(s.Market) - } else { - s.p = types.NewPositionFromMarket(s.Market) - s.p.Base = s.Position.Base - s.p.Quote = s.Position.Quote - s.p.AverageCost = s.Position.AverageCost + } + if s.Session.MakerFeeRate.Sign() > 0 || s.Session.TakerFeeRate.Sign() > 0 { + s.Position.SetExchangeFeeRate(s.Session.ExchangeName, types.ExchangeFee{ + MakerFeeRate: s.Session.MakerFeeRate, + TakerFeeRate: s.Session.TakerFeeRate, + }) } if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.Market) @@ -742,7 +753,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se _ = s.ClosePosition(ctx, fixedpoint.One) }) - profit := floats.Slice{1., 1.} + profitChart := floats.Slice{1., 1.} price, _ := s.Session.LastPrice(s.Symbol) initAsset := s.CalcAssetValue(price).Float64() cumProfit := floats.Slice{initAsset, initAsset} @@ -754,36 +765,100 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return p * (1. - Fee) } } + + s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.GeneralOrderExecutor.DisableNotify() + orderStore := s.GeneralOrderExecutor.OrderStore() + orderStore.AddOrderUpdate = true + orderStore.RemoveCancelled = true + orderStore.RemoveFilled = true + activeOrders := s.GeneralOrderExecutor.ActiveMakerOrders() + tradeCollector := s.GeneralOrderExecutor.TradeCollector() + tradeStore := tradeCollector.TradeStore() + + syscounter := 0 + + // Modify activeOrders to force write order updates + s.Session.UserDataStream.OnOrderUpdate(func(order types.Order) { + hasSymbol := len(activeOrders.Symbol) > 0 + if hasSymbol && order.Symbol != activeOrders.Symbol { + return + } + + switch order.Status { + case types.OrderStatusFilled: + s.pendingLock.Lock() + s.orderPendingCounter = make(map[uint64]int) + s.pendingLock.Unlock() + // make sure we have the order and we remove it + activeOrders.Remove(order) + + case types.OrderStatusPartiallyFilled: + s.pendingLock.Lock() + if _, ok := s.orderPendingCounter[order.OrderID]; !ok { + s.orderPendingCounter[order.OrderID] = syscounter + } + s.pendingLock.Unlock() + activeOrders.Add(order) + + case types.OrderStatusNew: + s.pendingLock.Lock() + if _, ok := s.orderPendingCounter[order.OrderID]; !ok { + s.orderPendingCounter[order.OrderID] = syscounter + } + s.pendingLock.Unlock() + activeOrders.Add(order) + + case types.OrderStatusCanceled, types.OrderStatusRejected: + log.Debugf("[ActiveOrderBook] order status %s, removing order %s", order.Status, order) + s.pendingLock.Lock() + s.orderPendingCounter = make(map[uint64]int) + s.pendingLock.Unlock() + activeOrders.Remove(order) + + default: + log.Errorf("unhandled order status: %s", order.Status) + } + orderStore.HandleOrderUpdate(order) + }) s.Session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { if trade.Symbol != s.Symbol { return } + profit, netProfit, madeProfit := s.Position.AddTrade(trade) + tradeStore.Add(trade) + if madeProfit { + p := s.Position.NewProfit(trade, profit, netProfit) + s.Environment.RecordPosition(s.Position, trade, &p) + s.TradeStats.Add(&p) + s.ProfitStats.AddTrade(trade) + s.ProfitStats.AddProfit(p) + bbgo.Notify(&p) + bbgo.Notify(s.ProfitStats) + } + price := trade.Price.Float64() - s.p.AddTrade(trade) - s.pendingLock.Lock() - delete(s.orderPendingCounter, trade.OrderID) - s.pendingLock.Unlock() if s.buyPrice > 0 { - profit.Update(modify(price / s.buyPrice)) + profitChart.Update(modify(price / s.buyPrice)) cumProfit.Update(s.CalcAssetValue(trade.Price).Float64()) } else if s.sellPrice > 0 { - profit.Update(modify(s.sellPrice / price)) + profitChart.Update(modify(s.sellPrice / price)) cumProfit.Update(s.CalcAssetValue(trade.Price).Float64()) } s.positionLock.Lock() - if s.p.IsDust(trade.Price) { + if s.Position.IsDust(trade.Price) { s.buyPrice = 0 s.sellPrice = 0 s.highestPrice = 0 s.lowestPrice = 0 - } else if s.p.IsLong() { - s.buyPrice = s.p.ApproximateAverageCost.Float64() + } else if s.Position.IsLong() { + s.buyPrice = s.Position.ApproximateAverageCost.Float64() s.sellPrice = 0 s.highestPrice = math.Max(s.buyPrice, s.highestPrice) s.lowestPrice = s.buyPrice - } else if s.p.IsShort() { - s.sellPrice = s.p.ApproximateAverageCost.Float64() + } else if s.Position.IsShort() { + s.sellPrice = s.Position.ApproximateAverageCost.Float64() s.buyPrice = 0 s.highestPrice = s.sellPrice if s.lowestPrice == 0 { @@ -795,12 +870,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.positionLock.Unlock() }) - s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) - s.GeneralOrderExecutor.BindEnvironment(s.Environment) - s.GeneralOrderExecutor.BindProfitStats(s.ProfitStats) - s.GeneralOrderExecutor.BindTradeStats(s.TradeStats) - s.GeneralOrderExecutor.Bind() - s.orderPendingCounter = make(map[uint64]int) // Exit methods from config @@ -818,7 +887,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, s.startTime)) s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, s.startTime)) - s.InitDrawCommands(&profit, &cumProfit) + s.InitDrawCommands(&profitChart, &cumProfit) bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) { var buffer bytes.Buffer @@ -826,10 +895,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se reply.Message(buffer.String()) }) - bbgo.RegisterCommand("/pos", "Show internal position", func(reply interact.Reply) { - reply.Message(s.p.String()) - }) - bbgo.RegisterCommand("/dump", "Dump internal params", func(reply interact.Reply) { reply.Message("Please enter series output length:") }).Next(func(length string, reply interact.Reply) { @@ -855,9 +920,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } - // var lastK types.KLine store.OnKLineClosed(func(kline types.KLine) { counter := int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) / s.MinInterval.Milliseconds() + syscounter = counter if kline.Interval == s.Interval { s.klineHandler(ctx, kline, counter) } else if kline.Interval == s.MinInterval { @@ -886,7 +951,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se os.Stdout.Write(buffer.Bytes()) if s.GenerateGraph { - s.Draw(s.frameKLine.StartTime, &profit, &cumProfit) + s.Draw(s.frameKLine.StartTime, &profitChart, &cumProfit) } wg.Done() }) From a6e0edbb3c83b09ec2548c0683d1e614ecdb9ae8 Mon Sep 17 00:00:00 2001 From: zenix Date: Mon, 21 Nov 2022 12:13:19 +0900 Subject: [PATCH 0047/1392] fix: naming of prepare function of openPosition and add comments --- pkg/bbgo/order_executor_general.go | 20 ++++++++++++++++++-- pkg/strategy/drift/strategy.go | 4 ++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index d23508105b..6051d7d232 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -181,6 +181,12 @@ func (e *GeneralOrderExecutor) CancelOrders(ctx context.Context, orders ...types return err } +// FastSubmitOrders send []types.SubmitOrder directly to the exchange without blocking wait on the status update. +// This is a faster version of SubmitOrders(). Created orders will be consumed in newly created goroutine (in non-backteset session). +// @param ctx: golang context type. +// @param submitOrders: Lists of types.SubmitOrder to be sent to the exchange. +// @return *types.SubmitOrder: SubmitOrder with calculated quantity and price. +// @return error: Error message. func (e *GeneralOrderExecutor) FastSubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { formattedOrders, err := e.session.FormatOrders(submitOrders) if err != nil { @@ -297,7 +303,12 @@ func (e *GeneralOrderExecutor) reduceQuantityAndSubmitOrder(ctx context.Context, return nil, multierr.Append(ErrExceededSubmitOrderRetryLimit, err) } -func (e *GeneralOrderExecutor) OpenPositionPrepare(ctx context.Context, options *OpenPositionOptions) (*types.SubmitOrder, error) { +// Create new submitOrder from OpenPositionOptions. +// @param ctx: golang context type. +// @param options: OpenPositionOptions to control the generated SubmitOrder in a higher level way. Notice that the Price in options will be updated as the submitOrder price. +// @return *types.SubmitOrder: SubmitOrder with calculated quantity and price. +// @return error: Error message. +func (e *GeneralOrderExecutor) NewOrderFromOpenPosition(ctx context.Context, options *OpenPositionOptions) (*types.SubmitOrder, error) { price := options.Price submitOrder := types.SubmitOrder{ Symbol: e.position.Symbol, @@ -386,8 +397,13 @@ func (e *GeneralOrderExecutor) OpenPositionPrepare(ctx context.Context, options return nil, errors.New("options Long or Short must be set") } +// OpenPosition sends the orders generated from OpenPositionOptions to the exchange by calling SubmitOrders or reduceQuantityAndSubmitOrder. +// @param ctx: golang context type. +// @param options: OpenPositionOptions to control the generated SubmitOrder in a higher level way. Notice that the Price in options will be updated as the submitOrder price. +// @return types.OrderSlice: Created orders with information from exchange. +// @return error: Error message. func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) (types.OrderSlice, error) { - submitOrder, err := e.OpenPositionPrepare(ctx, &options) + submitOrder, err := e.NewOrderFromOpenPosition(ctx, &options) if err != nil { return nil, err } diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 2579129d0c..403ee1914e 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -640,7 +640,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter opt.Price = source opt.Tags = []string{"long"} - submitOrder, err := s.GeneralOrderExecutor.OpenPositionPrepare(ctx, &opt) + submitOrder, err := s.GeneralOrderExecutor.NewOrderFromOpenPosition(ctx, &opt) if err != nil { errs := filterErrors(multierr.Errors(err)) if len(errs) > 0 { @@ -690,7 +690,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter } opt.Price = source opt.Tags = []string{"short"} - submitOrder, err := s.GeneralOrderExecutor.OpenPositionPrepare(ctx, &opt) + submitOrder, err := s.GeneralOrderExecutor.NewOrderFromOpenPosition(ctx, &opt) if err != nil { errs := filterErrors(multierr.Errors(err)) if len(errs) > 0 { From f121218ede3feec095b71342b0b5a96152019184 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 21 Nov 2022 13:46:13 +0800 Subject: [PATCH 0048/1392] strategy/linregmaker: prototype --- config/linregmaker.yaml | 143 +++++++++++++++++++++++++++ pkg/indicator/linreg.go | 4 +- pkg/strategy/linregmaker/strategy.go | 5 +- 3 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 config/linregmaker.yaml diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml new file mode 100644 index 0000000000..5a62ed8ce3 --- /dev/null +++ b/config/linregmaker.yaml @@ -0,0 +1,143 @@ +--- +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + envVarPrefix: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: BTCUSDT + +backtest: + sessions: [binance] + # for testing max draw down (MDD) at 03-12 + # see here for more details + # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp + startTime: "2022-01-01" + endTime: "2022-06-30" + symbols: + - BTCUSDT + accounts: + binance: + makerCommission: 10 # 0.15% + takerCommission: 15 # 0.15% + balances: + BTC: 10.0 + USDT: 10000.0 + +exchangeStrategies: +- on: binance + linregmaker: + symbol: BTCUSDT + + # interval is how long do you want to update your order price and quantity + interval: 1m + + # reverseEMA + reverseEMA: + interval: 1d + window: 60 + + # fastLinReg + fastLinReg: + interval: 1m + window: 20 + + # slowLinReg + slowLinReg: + interval: 1m + window: 60 + + # allowOppositePosition + allowOppositePosition: true + + # fasterDecreaseRatio + fasterDecreaseRatio: 2 + + # neutralBollinger + neutralBollinger: + interval: "5m" + window: 21 + bandWidth: 2.0 + + # tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band. + tradeInBand: true + + # spread + spread: 0.1% + + # dynamicSpread + dynamicSpread: + amplitude: # delete other scaling strategy if this is defined + # window is the window of the SMAs of spreads + window: 1 + interval: "1m" + askSpreadScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from down to up + domain: [ 0.0001, 0.005 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 0.001, 0.002 ] + bidSpreadScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from down to up + domain: [ 0.0001, 0.005 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 0.001, 0.002 ] + + maxExposurePosition: 10 + DynamicExposure: + interval: "1h" + window: 21 + bandWidth: 2.0 + dynamicExposurePositionScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ -1, 1 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 10.0, 1.0 ] + + # quantity is the base order quantity for your buy/sell order. + quantity: 0.1 + dynamicQuantityIncrease: + - linRegDynamicQuantity: + quantityLinReg: + interval: 1m + window: 20 + dynamicQuantityLinRegScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ -1, 1 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 0, 0.1 ] + dynamicQuantityDecrease: + - linRegDynamicQuantity: + quantityLinReg: + interval: 1m + window: 20 + dynamicQuantityLinRegScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ 1, -1 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 0, 0.1 ] diff --git a/pkg/indicator/linreg.go b/pkg/indicator/linreg.go index e48c82a2cb..d41fe9856a 100644 --- a/pkg/indicator/linreg.go +++ b/pkg/indicator/linreg.go @@ -8,7 +8,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -var log = logrus.WithField("indicator", "supertrend") +var logLinReg = logrus.WithField("indicator", "LinReg") // LinReg is Linear Regression baseline //go:generate callbackgen -type LinReg @@ -74,7 +74,7 @@ func (lr *LinReg) Update(kline types.KLine) { startPrice := endPrice + slope*(length-1) lr.Values.Push((endPrice - startPrice) / (length - 1)) - log.Debugf("linear regression baseline slope: %f", lr.Last()) + logLinReg.Debugf("linear regression baseline slope: %f", lr.Last()) } func (lr *LinReg) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 1eca593d81..e38f6613b0 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -74,7 +74,7 @@ type Strategy struct { // FasterDecreaseRatio the quantity of decreasing position orders are multiplied by this ratio when both fast and // slow LinReg are in the opposite direction to main trend - FasterDecreaseRatio fixedpoint.Value `json:"FasterDecreaseRatio,omitempty"` + FasterDecreaseRatio fixedpoint.Value `json:"fasterDecreaseRatio,omitempty"` // NeutralBollinger is the smaller range of the bollinger band // If price is in this band, it usually means the price is oscillating. @@ -136,9 +136,6 @@ type Strategy struct { groupID uint32 - // defaultBoll is the BOLLINGER indicator we used for predicting the price. - defaultBoll *indicator.BOLL - // neutralBoll is the neutral price section neutralBoll *indicator.BOLL From dd0f13e742817eae522139767931759707a1375c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 22 Nov 2022 11:35:32 +0800 Subject: [PATCH 0049/1392] strategy/linregmaker: misc --- config/linregmaker.yaml | 4 ++-- pkg/strategy/linregmaker/strategy.go | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 5a62ed8ce3..7cee625042 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -18,8 +18,8 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2022-01-01" - endTime: "2022-06-30" + startTime: "2022-05-01" + endTime: "2022-10-31" symbols: - BTCUSDT accounts: diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index e38f6613b0..17564952d6 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -17,11 +17,6 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// TODO: -// - TradeInBand: no buy order above the band, no sell order below the band -// - DynamicQuantity -// - Validate() - const ID = "linregmaker" var notionModifier = fixedpoint.NewFromFloat(1.1) @@ -115,7 +110,7 @@ type Strategy struct { DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` bbgo.QuantityOrAmount - + // TODO: Should work w/o dynamic qty // DynamicQuantityIncrease calculates the increase position order quantity dynamically DynamicQuantityIncrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityIncrease"` @@ -208,6 +203,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } +// TODO func (s *Strategy) Validate() error { if len(s.Symbol) == 0 { return errors.New("symbol is required") @@ -450,7 +446,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } else if closePrice.Compare(priceReverseEMA) < 0 { s.mainTrendCurrent = types.DirectionDown } - // TODO: everything should works for both direction // Trend reversal if s.mainTrendCurrent != s.mainTrendPrevious { From 6af39e2e4065fd1c66c009e9b5265852efc4ecdd Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 22 Nov 2022 11:39:15 +0800 Subject: [PATCH 0050/1392] strategy/supertrend: update supertrend config --- config/supertrend.yaml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 7045004193..bee067b5d1 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -18,8 +18,8 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2022-01-01" - endTime: "2022-06-30" + startTime: "2022-05-01" + endTime: "2022-10-31" symbols: - BTCUSDT accounts: @@ -27,7 +27,7 @@ backtest: makerCommission: 10 # 0.15% takerCommission: 15 # 0.15% balances: - BTC: 1.0 + BTC: 50.0 USDT: 10000.0 exchangeStrategies: @@ -39,9 +39,9 @@ exchangeStrategies: interval: 1m # ATR window used by Supertrend - window: 34 + window: 220 # ATR Multiplier for calculating super trend prices, the higher, the stronger the trends are - supertrendMultiplier: 4 + supertrendMultiplier: 10 # leverage uses the account net value to calculate the order qty leverage: 1.0 @@ -49,13 +49,13 @@ exchangeStrategies: #quantity: 0.5 # fastDEMAWindow and slowDEMAWindow are for filtering super trend noise - fastDEMAWindow: 40 - slowDEMAWindow: 49 + fastDEMAWindow: 28 + slowDEMAWindow: 170 # Use linear regression as trend confirmation linearRegression: interval: 1m - window: 74 + window: 18 # TP according to ATR multiple, 0 to disable this TakeProfitAtrMultiplier: 0 @@ -80,19 +80,19 @@ exchangeStrategies: exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) - roiStopLoss: - percentage: 4.5% + percentage: 4.6% - protectiveStopLoss: - activationRatio: 3% - stopLossRatio: 2.5% + activationRatio: 3.5% + stopLossRatio: 2.9% placeStopOrder: false - protectiveStopLoss: - activationRatio: 7% - stopLossRatio: 3.5% + activationRatio: 11.1% + stopLossRatio: 9.5% placeStopOrder: false - trailingStop: - callbackRate: 4.5% - #activationRatio: 40% - minProfit: 9.5% + callbackRate: 1.1% + #activationRatio: 20% + minProfit: 19.5% interval: 1m side: both closePosition: 100% From 37a2fedf155e4f484eb4d038e35b25be1edad60e Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 22 Nov 2022 18:24:04 +0800 Subject: [PATCH 0051/1392] strategy/linregmaker: dynamic qty uses linreg slope ratio --- config/linregmaker.yaml | 25 +++++++++++++------------ pkg/bbgo/standard_indicator_set.go | 1 + pkg/dynamicmetric/dynamic_exposure.go | 9 +++++---- pkg/dynamicmetric/dynamic_quantity.go | 2 +- pkg/indicator/linreg.go | 27 +++++++++++++++++++++++++++ pkg/strategy/linregmaker/strategy.go | 2 +- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 7cee625042..63c1a224e9 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -98,18 +98,19 @@ exchangeStrategies: maxExposurePosition: 10 DynamicExposure: - interval: "1h" - window: 21 - bandWidth: 2.0 - dynamicExposurePositionScale: - byPercentage: - # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale - exp: - # from lower band -100% (-1) to upper band 100% (+1) - domain: [ -1, 1 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum - range: [ 10.0, 1.0 ] + bollBandExposure: + interval: "1h" + window: 21 + bandWidth: 2.0 + dynamicExposurePositionScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ -1, 1 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 10.0, 1.0 ] # quantity is the base order quantity for your buy/sell order. quantity: 0.1 diff --git a/pkg/bbgo/standard_indicator_set.go b/pkg/bbgo/standard_indicator_set.go index 52ecdddce1..9228567265 100644 --- a/pkg/bbgo/standard_indicator_set.go +++ b/pkg/bbgo/standard_indicator_set.go @@ -142,6 +142,7 @@ func (s *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) if !ok { inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth} s.initAndBind(inc, iw.Interval) + inc.SMA = &indicator.SMA{IntervalWindow: iw} if debugBOLL { inc.OnUpdate(func(sma float64, upBand float64, downBand float64) { diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go index c2e1802a18..afc442c370 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -16,10 +16,10 @@ type DynamicExposure struct { } // Initialize dynamic exposure -func (d *DynamicExposure) Initialize(symbol string, session *bbgo.ExchangeSession) { +func (d *DynamicExposure) Initialize(symbol string, session *bbgo.ExchangeSession, standardIndicatorSet *bbgo.StandardIndicatorSet) { switch { case d.BollBandExposure != nil: - d.BollBandExposure.initialize(symbol, session) + d.BollBandExposure.initialize(symbol, session, standardIndicatorSet) } } @@ -42,7 +42,7 @@ type DynamicExposureBollBand struct { // DynamicExposureBollBandScale is used to define the exposure range with the given percentage. DynamicExposureBollBandScale *bbgo.PercentageScale `json:"dynamicExposurePositionScale"` - *BollingerSetting + types.IntervalWindowBandWidth StandardIndicatorSet *bbgo.StandardIndicatorSet @@ -50,7 +50,8 @@ type DynamicExposureBollBand struct { } // initialize dynamic exposure with Bollinger Band -func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession) { +func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession, standardIndicatorSet *bbgo.StandardIndicatorSet) { + d.StandardIndicatorSet = standardIndicatorSet d.dynamicExposureBollBand = d.StandardIndicatorSet.BOLL(d.IntervalWindow, d.BandWidth) // Subscribe kline diff --git a/pkg/dynamicmetric/dynamic_quantity.go b/pkg/dynamicmetric/dynamic_quantity.go index ac2e85b387..70e44797d2 100644 --- a/pkg/dynamicmetric/dynamic_quantity.go +++ b/pkg/dynamicmetric/dynamic_quantity.go @@ -85,7 +85,7 @@ func (d *DynamicQuantityLinReg) initialize(symbol string, session *bbgo.Exchange // getQuantity returns quantity func (d *DynamicQuantityLinReg) getQuantity() (fixedpoint.Value, error) { - v, err := d.DynamicQuantityLinRegScale.Scale(d.QuantityLinReg.Last()) + v, err := d.DynamicQuantityLinRegScale.Scale(d.QuantityLinReg.LastRatio()) if err != nil { return fixedpoint.Zero, err } diff --git a/pkg/indicator/linreg.go b/pkg/indicator/linreg.go index d41fe9856a..3bb4606edc 100644 --- a/pkg/indicator/linreg.go +++ b/pkg/indicator/linreg.go @@ -18,6 +18,9 @@ type LinReg struct { // Values are the slopes of linear regression baseline Values floats.Slice + // ValueRatios are the ratio of slope to the price + ValueRatios floats.Slice + klines types.KLineWindow EndTime time.Time @@ -32,6 +35,14 @@ func (lr *LinReg) Last() float64 { return lr.Values.Last() } +// LastRatio of slope to price +func (lr *LinReg) LastRatio() float64 { + if lr.ValueRatios.Length() == 0 { + return 0.0 + } + return lr.ValueRatios.Last() +} + // Index returns the slope of specified index func (lr *LinReg) Index(i int) float64 { if i >= lr.Values.Length() { @@ -41,11 +52,25 @@ func (lr *LinReg) Index(i int) float64 { return lr.Values.Index(i) } +// IndexRatio returns the slope ratio +func (lr *LinReg) IndexRatio(i int) float64 { + if i >= lr.ValueRatios.Length() { + return 0.0 + } + + return lr.ValueRatios.Index(i) +} + // Length of the slope values func (lr *LinReg) Length() int { return lr.Values.Length() } +// LengthRatio of the slope ratio values +func (lr *LinReg) LengthRatio() int { + return lr.ValueRatios.Length() +} + var _ types.SeriesExtend = &LinReg{} // Update Linear Regression baseline slope @@ -54,6 +79,7 @@ func (lr *LinReg) Update(kline types.KLine) { lr.klines.Truncate(lr.Window) if len(lr.klines) < lr.Window { lr.Values.Push(0) + lr.ValueRatios.Push(0) return } @@ -73,6 +99,7 @@ func (lr *LinReg) Update(kline types.KLine) { endPrice := average - slope*sumX/length + slope startPrice := endPrice + slope*(length-1) lr.Values.Push((endPrice - startPrice) / (length - 1)) + lr.ValueRatios.Push(lr.Values.Last() / kline.GetClose().Float64()) logLinReg.Debugf("linear regression baseline slope: %f", lr.Last()) } diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 17564952d6..8b4b7584e5 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -191,7 +191,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Setup dynamic exposure if s.DynamicExposure.IsEnabled() { - s.DynamicExposure.Initialize(s.Symbol, session) + s.DynamicExposure.Initialize(s.Symbol, session, s.StandardIndicatorSet) } // Setup dynamic quantities From e776c9e5eaca7bdd327b27bffcb74ed2bedd269b Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 23 Nov 2022 12:28:38 +0800 Subject: [PATCH 0052/1392] strategy/linregmaker: use session standard indicator set --- pkg/dynamicmetric/dynamic_exposure.go | 11 ++++------- pkg/dynamicmetric/dynamic_spread.go | 6 ++---- pkg/strategy/linregmaker/strategy.go | 9 ++++++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go index afc442c370..dc5f51c8d1 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -16,10 +16,10 @@ type DynamicExposure struct { } // Initialize dynamic exposure -func (d *DynamicExposure) Initialize(symbol string, session *bbgo.ExchangeSession, standardIndicatorSet *bbgo.StandardIndicatorSet) { +func (d *DynamicExposure) Initialize(symbol string, session *bbgo.ExchangeSession) { switch { case d.BollBandExposure != nil: - d.BollBandExposure.initialize(symbol, session, standardIndicatorSet) + d.BollBandExposure.initialize(symbol, session) } } @@ -44,15 +44,12 @@ type DynamicExposureBollBand struct { types.IntervalWindowBandWidth - StandardIndicatorSet *bbgo.StandardIndicatorSet - dynamicExposureBollBand *indicator.BOLL } // initialize dynamic exposure with Bollinger Band -func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession, standardIndicatorSet *bbgo.StandardIndicatorSet) { - d.StandardIndicatorSet = standardIndicatorSet - d.dynamicExposureBollBand = d.StandardIndicatorSet.BOLL(d.IntervalWindow, d.BandWidth) +func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.ExchangeSession) { + d.dynamicExposureBollBand = session.StandardIndicatorSet(symbol).BOLL(d.IntervalWindow, d.BandWidth) // Subscribe kline session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/dynamicmetric/dynamic_spread.go index a1e5e631c8..6c0391c33e 100644 --- a/pkg/dynamicmetric/dynamic_spread.go +++ b/pkg/dynamicmetric/dynamic_spread.go @@ -174,15 +174,13 @@ type DynamicSpreadBollWidthRatio struct { DefaultBollinger *BollingerSetting `json:"defaultBollinger"` NeutralBollinger *BollingerSetting `json:"neutralBollinger"` - StandardIndicatorSet *bbgo.StandardIndicatorSet - neutralBoll *indicator.BOLL defaultBoll *indicator.BOLL } func (ds *DynamicSpreadBollWidthRatio) initialize(symbol string, session *bbgo.ExchangeSession) { - ds.neutralBoll = ds.StandardIndicatorSet.BOLL(ds.NeutralBollinger.IntervalWindow, ds.NeutralBollinger.BandWidth) - ds.defaultBoll = ds.StandardIndicatorSet.BOLL(ds.DefaultBollinger.IntervalWindow, ds.DefaultBollinger.BandWidth) + ds.neutralBoll = session.StandardIndicatorSet(symbol).BOLL(ds.NeutralBollinger.IntervalWindow, ds.NeutralBollinger.BandWidth) + ds.defaultBoll = session.StandardIndicatorSet(symbol).BOLL(ds.DefaultBollinger.IntervalWindow, ds.DefaultBollinger.BandWidth) // Subscribe kline session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{ diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 8b4b7584e5..0d84ba0317 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -24,10 +24,13 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) +//TODO: Logic for backtest + func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +// TODO: Remove BollingerSetting and bollsetting.go type BollingerSetting struct { types.IntervalWindow BandWidth float64 `json:"bandWidth"` @@ -191,7 +194,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Setup dynamic exposure if s.DynamicExposure.IsEnabled() { - s.DynamicExposure.Initialize(s.Symbol, session, s.StandardIndicatorSet) + s.DynamicExposure.Initialize(s.Symbol, session) } // Setup dynamic quantities @@ -203,7 +206,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } -// TODO +// TODO Validate() func (s *Strategy) Validate() error { if len(s.Symbol) == 0 { return errors.New("symbol is required") @@ -541,7 +544,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -// TODO +// TODO adjustOrderQuantity() func adjustOrderQuantity(submitOrder types.SubmitOrder, market types.Market) types.SubmitOrder { if submitOrder.Quantity.Mul(submitOrder.Price).Compare(market.MinNotional) < 0 { submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, market.MinNotional.Mul(notionModifier)) From cc124d42649ec68380ecb4090b66411df16371be Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 23 Nov 2022 16:53:08 +0800 Subject: [PATCH 0053/1392] strategy/linregmaker: works w/o dynamic qty --- config/linregmaker.yaml | 2 +- pkg/dynamicmetric/bollsetting.go | 9 ----- pkg/dynamicmetric/dynamic_spread.go | 26 ++------------ pkg/strategy/linregmaker/strategy.go | 52 ++++++++++++---------------- 4 files changed, 27 insertions(+), 62 deletions(-) delete mode 100644 pkg/dynamicmetric/bollsetting.go diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 63c1a224e9..80d5b1f5bf 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -110,7 +110,7 @@ exchangeStrategies: domain: [ -1, 1 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum - range: [ 10.0, 1.0 ] + range: [ 1.0, 1.0 ] # quantity is the base order quantity for your buy/sell order. quantity: 0.1 diff --git a/pkg/dynamicmetric/bollsetting.go b/pkg/dynamicmetric/bollsetting.go deleted file mode 100644 index 79bff4ef1e..0000000000 --- a/pkg/dynamicmetric/bollsetting.go +++ /dev/null @@ -1,9 +0,0 @@ -package dynamicmetric - -import "github.com/c9s/bbgo/pkg/types" - -// BollingerSetting is for Bollinger Band settings -type BollingerSetting struct { - types.IntervalWindow - BandWidth float64 `json:"bandWidth"` -} diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/dynamicmetric/dynamic_spread.go index 6c0391c33e..905f49bc34 100644 --- a/pkg/dynamicmetric/dynamic_spread.go +++ b/pkg/dynamicmetric/dynamic_spread.go @@ -16,18 +16,6 @@ type DynamicSpread struct { // WeightedBollWidthRatioSpread calculates spreads based on two Bollinger Bands WeightedBollWidthRatioSpread *DynamicSpreadBollWidthRatio `json:"weightedBollWidth"` - - // deprecated - Enabled *bool `json:"enabled"` - - // deprecated - types.IntervalWindow - - // deprecated. AskSpreadScale is used to define the ask spread range with the given percentage. - AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` - - // deprecated. BidSpreadScale is used to define the bid spread range with the given percentage. - BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` } // Initialize dynamic spread @@ -37,14 +25,6 @@ func (ds *DynamicSpread) Initialize(symbol string, session *bbgo.ExchangeSession ds.AmpSpread.initialize(symbol, session) case ds.WeightedBollWidthRatioSpread != nil: ds.WeightedBollWidthRatioSpread.initialize(symbol, session) - case ds.Enabled != nil && *ds.Enabled: - // backward compatibility - ds.AmpSpread = &DynamicSpreadAmp{ - IntervalWindow: ds.IntervalWindow, - AskSpreadScale: ds.AskSpreadScale, - BidSpreadScale: ds.BidSpreadScale, - } - ds.AmpSpread.initialize(symbol, session) } } @@ -171,8 +151,8 @@ type DynamicSpreadBollWidthRatio struct { // A positive number. The greater factor, the sharper weighting function. Default set to 1.0 . Sensitivity float64 `json:"sensitivity"` - DefaultBollinger *BollingerSetting `json:"defaultBollinger"` - NeutralBollinger *BollingerSetting `json:"neutralBollinger"` + DefaultBollinger types.IntervalWindowBandWidth `json:"defaultBollinger"` + NeutralBollinger types.IntervalWindowBandWidth `json:"neutralBollinger"` neutralBoll *indicator.BOLL defaultBoll *indicator.BOLL @@ -232,7 +212,7 @@ func (ds *DynamicSpreadBollWidthRatio) getWeightedBBWidthRatio(positiveSigmoid b // - To bid spread, the weighting density function d_weight(x) is sigmoid((default_BB_mid - x) / (w / alpha)) // - The higher sensitivity factor alpha, the sharper weighting function. // - // Then calculate the weighted band width ratio by taking integral of d_weight(x) from neutral_BB_lower to neutral_BB_upper: + // Then calculate the weighted bandwidth ratio by taking integral of d_weight(x) from neutral_BB_lower to neutral_BB_upper: // infinite integral of ask spread sigmoid weighting density function F(x) = (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha))) // infinite integral of bid spread sigmoid weighting density function F(x) = x - (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha))) // Note that we've rescaled the sigmoid function to fit default BB, diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 0d84ba0317..5e4ba99e65 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -25,17 +25,12 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) //TODO: Logic for backtest +// TODO: Dynamic exposure should work on both side func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } -// TODO: Remove BollingerSetting and bollsetting.go -type BollingerSetting struct { - types.IntervalWindow - BandWidth float64 `json:"bandWidth"` -} - type Strategy struct { Environment *bbgo.Environment StandardIndicatorSet *bbgo.StandardIndicatorSet @@ -77,7 +72,10 @@ type Strategy struct { // NeutralBollinger is the smaller range of the bollinger band // If price is in this band, it usually means the price is oscillating. // If price goes out of this band, we tend to not place sell orders or buy orders - NeutralBollinger *BollingerSetting `json:"neutralBollinger"` + NeutralBollinger types.IntervalWindowBandWidth `json:"neutralBollinger"` + + // neutralBoll is the neutral price section for TradeInBand + neutralBoll *indicator.BOLL // TradeInBand // When this is on, places orders only when the current price is in the bollinger band. @@ -113,7 +111,7 @@ type Strategy struct { DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` bbgo.QuantityOrAmount - // TODO: Should work w/o dynamic qty + // DynamicQuantityIncrease calculates the increase position order quantity dynamically DynamicQuantityIncrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityIncrease"` @@ -134,9 +132,6 @@ type Strategy struct { groupID uint32 - // neutralBoll is the neutral price section - neutralBoll *indicator.BOLL - // StrategyController bbgo.StrategyController } @@ -176,7 +171,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } // Subscribe for BBs - if s.NeutralBollinger != nil && s.NeutralBollinger.Interval != "" { + if s.NeutralBollinger.Interval != "" { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: s.NeutralBollinger.Interval, }) @@ -276,39 +271,38 @@ func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoin func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { // TODO: spot, margin, and futures + // Default + sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + // Dynamic qty switch { case s.mainTrendCurrent == types.DirectionUp: - var err error if len(s.DynamicQuantityIncrease) > 0 { - buyQuantity, err = s.DynamicQuantityIncrease.GetQuantity() - if err != nil { - buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + qty, err := s.DynamicQuantityIncrease.GetQuantity() + if err == nil { + buyQuantity = qty } } if len(s.DynamicQuantityDecrease) > 0 { - sellQuantity, err = s.DynamicQuantityDecrease.GetQuantity() - if err != nil { - sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + qty, err := s.DynamicQuantityDecrease.GetQuantity() + if err == nil { + sellQuantity = qty } } case s.mainTrendCurrent == types.DirectionDown: - var err error if len(s.DynamicQuantityIncrease) > 0 { - sellQuantity, err = s.DynamicQuantityIncrease.GetQuantity() - if err != nil { - sellQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) + qty, err := s.DynamicQuantityIncrease.GetQuantity() + if err == nil { + sellQuantity = qty } } if len(s.DynamicQuantityDecrease) > 0 { - buyQuantity, err = s.DynamicQuantityDecrease.GetQuantity() - if err != nil { - buyQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) + qty, err := s.DynamicQuantityDecrease.GetQuantity() + if err == nil { + buyQuantity = qty } } - default: - sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) - buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) } // Faster position decrease From fbc949a133f695cdb3fbb241b791bbe612c3e96f Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 23 Nov 2022 16:58:24 +0800 Subject: [PATCH 0054/1392] strategy/linregmaker: validate basic config parameters --- pkg/strategy/linregmaker/strategy.go | 48 ++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 5e4ba99e65..cca769c77d 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -54,12 +54,12 @@ type Strategy struct { // FastLinReg is to determine the short-term trend. // Buy/sell orders are placed if the FastLinReg and the ReverseEMA trend are in the same direction, and only orders // that reduce position are placed if the FastLinReg and the ReverseEMA trend are in different directions. - FastLinReg *indicator.LinReg `json:"fastLinReg,omitempty"` + FastLinReg *indicator.LinReg `json:"fastLinReg"` // SlowLinReg is to determine the midterm trend. // When the SlowLinReg and the ReverseEMA trend are in different directions, creation of opposite position is // allowed. - SlowLinReg *indicator.LinReg `json:"slowLinReg,omitempty"` + SlowLinReg *indicator.LinReg `json:"slowLinReg"` // AllowOppositePosition if true, the creation of opposite position is allowed when both fast and slow LinReg are in // the opposite direction to main trend @@ -90,7 +90,8 @@ type Strategy struct { // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) // Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 - Spread fixedpoint.Value `json:"spread"` + // TODO: if nil? + Spread fixedpoint.Value `json:"spread,omitempty"` // BidSpread overrides the spread setting, this spread will be used for the buy order BidSpread fixedpoint.Value `json:"bidSpread,omitempty"` @@ -104,7 +105,8 @@ type Strategy struct { // MaxExposurePosition is the maximum position you can hold // 10 means you can hold 10 ETH long/short position by maximum - MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` + // TODO: if nil? + MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition,omitempty"` // DynamicExposure is used to define the exposure position range with the given percentage. // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically @@ -144,6 +146,35 @@ func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s:%s", ID, s.Symbol) } +// Validate basic config parameters. TODO LATER: Validate more +func (s *Strategy) Validate() error { + if len(s.Symbol) == 0 { + return errors.New("symbol is required") + } + + if len(s.Interval) == 0 { + return errors.New("interval is required") + } + + if s.Window <= 0 { + return errors.New("window must be more than 0") + } + + if s.ReverseEMA == nil { + return errors.New("reverseEMA must be set") + } + + if s.FastLinReg == nil { + return errors.New("fastLinReg must be set") + } + + if s.SlowLinReg == nil { + return errors.New("slowLinReg must be set") + } + + return nil +} + func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Subscribe for ReverseEMA session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ @@ -201,15 +232,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } -// TODO Validate() -func (s *Strategy) Validate() error { - if len(s.Symbol) == 0 { - return errors.New("symbol is required") - } - - return nil -} - func (s *Strategy) CurrentPosition() *types.Position { return s.Position } From 0f0549fa42b56b4bcefe7ffc45d0ddd20563cb94 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 23 Nov 2022 17:23:18 +0800 Subject: [PATCH 0055/1392] strategy/linregmaker: dynamic exposure works on both direction --- config/linregmaker.yaml | 2 +- pkg/dynamicmetric/dynamic_exposure.go | 11 ++++++++--- pkg/strategy/linregmaker/strategy.go | 16 ++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 80d5b1f5bf..a550aaee0a 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -110,7 +110,7 @@ exchangeStrategies: domain: [ -1, 1 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum - range: [ 1.0, 1.0 ] + range: [ 0.1, 10 ] # quantity is the base order quantity for your buy/sell order. quantity: 0.1 diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go index dc5f51c8d1..d150e11d95 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -28,10 +28,10 @@ func (d *DynamicExposure) IsEnabled() bool { } // GetMaxExposure returns the max exposure -func (d *DynamicExposure) GetMaxExposure(price float64) (maxExposure fixedpoint.Value, err error) { +func (d *DynamicExposure) GetMaxExposure(price float64, trend types.Direction) (maxExposure fixedpoint.Value, err error) { switch { case d.BollBandExposure != nil: - return d.BollBandExposure.getMaxExposure(price) + return d.BollBandExposure.getMaxExposure(price, trend) default: return fixedpoint.Zero, errors.New("dynamic exposure is not enabled") } @@ -58,7 +58,7 @@ func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.Exchan } // getMaxExposure returns the max exposure -func (d *DynamicExposureBollBand) getMaxExposure(price float64) (fixedpoint.Value, error) { +func (d *DynamicExposureBollBand) getMaxExposure(price float64, trend types.Direction) (fixedpoint.Value, error) { downBand := d.dynamicExposureBollBand.DownBand.Last() upBand := d.dynamicExposureBollBand.UpBand.Last() sma := d.dynamicExposureBollBand.SMA.Last() @@ -73,6 +73,11 @@ func (d *DynamicExposureBollBand) getMaxExposure(price float64) (fixedpoint.Valu bandPercentage = (price - sma) / math.Abs(upBand-sma) } + // Reverse if downtrend + if trend == types.DirectionDown { + bandPercentage = 0 - bandPercentage + } + v, err := d.DynamicExposureBollBandScale.Scale(bandPercentage) if err != nil { return fixedpoint.Zero, err diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index cca769c77d..1ce8481c9f 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -24,8 +24,7 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) -//TODO: Logic for backtest -// TODO: Dynamic exposure should work on both side +// TODO: Logic for backtest func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -81,7 +80,7 @@ type Strategy struct { // When this is on, places orders only when the current price is in the bollinger band. TradeInBand bool `json:"tradeInBand"` - // useTickerPrice use the ticker api to get the mid price instead of the closed kline price. + // useTickerPrice use the ticker api to get the mid-price instead of the closed kline price. // The back-test engine is kline-based, so the ticker price api is not supported. // Turn this on if you want to do real trading. useTickerPrice bool @@ -146,7 +145,8 @@ func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s:%s", ID, s.Symbol) } -// Validate basic config parameters. TODO LATER: Validate more +// Validate basic config parameters +// TODO LATER: Validate more func (s *Strategy) Validate() error { if len(s.Symbol) == 0 { return errors.New("symbol is required") @@ -156,10 +156,6 @@ func (s *Strategy) Validate() error { return errors.New("interval is required") } - if s.Window <= 0 { - return errors.New("window must be more than 0") - } - if s.ReverseEMA == nil { return errors.New("reverseEMA must be set") } @@ -270,7 +266,7 @@ func (s *Strategy) updateMaxExposure(midPrice fixedpoint.Value) { // Calculate max exposure if s.DynamicExposure.IsEnabled() { var err error - maxExposurePosition, err := s.DynamicExposure.GetMaxExposure(midPrice.Float64()) + maxExposurePosition, err := s.DynamicExposure.GetMaxExposure(midPrice.Float64(), s.mainTrendCurrent) if err != nil { log.WithError(err).Errorf("can not calculate DynamicExposure of %s, use previous MaxExposurePosition instead", s.Symbol) } else { @@ -560,7 +556,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -// TODO adjustOrderQuantity() +// adjustOrderQuantity to meet the min notional and qty requirement func adjustOrderQuantity(submitOrder types.SubmitOrder, market types.Market) types.SubmitOrder { if submitOrder.Quantity.Mul(submitOrder.Price).Compare(market.MinNotional) < 0 { submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, market.MinNotional.Mul(notionModifier)) From 41e27a8e382a6a0c7243e32aca15991772d79618 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 23 Nov 2022 17:44:40 +0800 Subject: [PATCH 0056/1392] strategy/linregmaker: default value of spread --- config/linregmaker.yaml | 10 +++++++--- pkg/strategy/linregmaker/strategy.go | 11 +++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index a550aaee0a..6b7eda4886 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -27,7 +27,7 @@ backtest: makerCommission: 10 # 0.15% takerCommission: 15 # 0.15% balances: - BTC: 10.0 + BTC: 2.0 USDT: 10000.0 exchangeStrategies: @@ -70,7 +70,6 @@ exchangeStrategies: # spread spread: 0.1% - # dynamicSpread dynamicSpread: amplitude: # delete other scaling strategy if this is defined @@ -96,7 +95,7 @@ exchangeStrategies: # when in up band, holds 0.05 by maximum range: [ 0.001, 0.002 ] - maxExposurePosition: 10 + #maxExposurePosition: 10 DynamicExposure: bollBandExposure: interval: "1h" @@ -142,3 +141,8 @@ exchangeStrategies: # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum range: [ 0, 0.1 ] + + exits: + # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) + - roiStopLoss: + percentage: 20% diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 1ce8481c9f..c7492034a0 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -89,8 +89,7 @@ type Strategy struct { // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) // Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 - // TODO: if nil? - Spread fixedpoint.Value `json:"spread,omitempty"` + Spread fixedpoint.Value `json:"spread"` // BidSpread overrides the spread setting, this spread will be used for the buy order BidSpread fixedpoint.Value `json:"bidSpread,omitempty"` @@ -104,8 +103,7 @@ type Strategy struct { // MaxExposurePosition is the maximum position you can hold // 10 means you can hold 10 ETH long/short position by maximum - // TODO: if nil? - MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition,omitempty"` + MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` // DynamicExposure is used to define the exposure position range with the given percentage. // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically @@ -429,6 +427,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.useTickerPrice = true } + // Default spread + if s.Spread == fixedpoint.Zero { + s.Spread = fixedpoint.NewFromFloat(0.001) + } + // StrategyController s.Status = types.StrategyStatusRunning s.OnSuspend(func() { From d14d441ff86754a4712493443d47460750a1d3f7 Mon Sep 17 00:00:00 2001 From: a dwarf <74894662+a-dwarf@users.noreply.github.com> Date: Thu, 24 Nov 2022 16:05:02 +0800 Subject: [PATCH 0057/1392] upgrade cobra bbgo Completion only supports unix-like operating systems --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2531600cef..536111a4d0 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/sajari/regression v1.0.1 github.com/sirupsen/logrus v1.8.1 github.com/slack-go/slack v0.10.1 - github.com/spf13/cobra v1.1.1 + github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.4 From 70e45ccf939b4ab8b9a4ef1ed7af234baa672310 Mon Sep 17 00:00:00 2001 From: a dwarf <74894662+a-dwarf@users.noreply.github.com> Date: Thu, 24 Nov 2022 16:13:16 +0800 Subject: [PATCH 0058/1392] Create bbgo_completion.md bbgo Completion --- doc/commands/bbgo_completion.md | 70 +++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 doc/commands/bbgo_completion.md diff --git a/doc/commands/bbgo_completion.md b/doc/commands/bbgo_completion.md new file mode 100644 index 0000000000..26d5bee774 --- /dev/null +++ b/doc/commands/bbgo_completion.md @@ -0,0 +1,70 @@ +# bbgo Completion + + +## usage + +```shell +(base) ➜ bbgo git:(main) ✗ bbgo completion -h + +./build/bbgo/bbgo completion -h +Generate the autocompletion script for bbgo for the specified shell. +See each sub-command's help for details on how to use the generated script. + +Usage: + bbgo completion [command] + +Available Commands: + bash Generate the autocompletion script for bash + fish Generate the autocompletion script for fish + powershell Generate the autocompletion script for powershell + zsh Generate the autocompletion script for zsh + +``` + +## shell configuration + +```shell +(base) ➜ bbgo git:(main) ✗ ./build/bbgo/bbgo completion zsh -h + + +Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(bbgo completion zsh); compdef _bbgo bbgo + +To load completions for every new session, execute once: + +#### Linux: + + bbgo completion zsh > "${fpath[1]}/_bbgo" + +#### macOS: + + bbgo completion zsh > $(brew --prefix)/share/zsh/site-functions/_bbgo + +You will need to start a new shell for this setup to take effect. + +``` + +## demo effect +Use the `tab` key to bring up the autocomplete prompt4 + + +```shell +(base) ➜ bbgo git:(main) ✗ ./build/bbgo/bbgo account - +--binance-api-key -- binance api key +--binance-api-secret -- binance api secret +--config -- config file +--cpu-profile -- cpu profile +--debug -- debug mode +--dotenv -- the dotenv file you want to load +--ftx-api-key -- ftx api key +--ftx-api-secret -- ftx api secret +--ftx-subaccount -- subaccount name. Specify it if the crede +``` From 8c57dec793fa701be9337eefff8168ca3cb2aecd Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 24 Nov 2022 16:51:37 +0800 Subject: [PATCH 0059/1392] strategy/linregmaker: parameter of check main trend interval --- config/linregmaker.yaml | 19 ++++++++------ pkg/strategy/linregmaker/strategy.go | 38 +++++++++++++++++++++------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 6b7eda4886..2a4f97233f 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -43,15 +43,18 @@ exchangeStrategies: interval: 1d window: 60 + # reverseInterval + reverseInterval: 4h + # fastLinReg fastLinReg: interval: 1m - window: 20 + window: 30 # slowLinReg slowLinReg: interval: 1m - window: 60 + window: 120 # allowOppositePosition allowOppositePosition: true @@ -61,7 +64,7 @@ exchangeStrategies: # neutralBollinger neutralBollinger: - interval: "5m" + interval: "15m" window: 21 bandWidth: 2.0 @@ -120,8 +123,8 @@ exchangeStrategies: window: 20 dynamicQuantityLinRegScale: byPercentage: - # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale - exp: + # log means we want to use log scale, you can replace "log" with "linear" for linear scale + linear: # from lower band -100% (-1) to upper band 100% (+1) domain: [ -1, 1 ] # when in down band, holds 1.0 by maximum @@ -134,8 +137,8 @@ exchangeStrategies: window: 20 dynamicQuantityLinRegScale: byPercentage: - # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale - exp: + # log means we want to use log scale, you can replace "log" with "linear" for linear scale + linear: # from lower band -100% (-1) to upper band 100% (+1) domain: [ 1, -1 ] # when in down band, holds 1.0 by maximum @@ -145,4 +148,4 @@ exchangeStrategies: exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) - roiStopLoss: - percentage: 20% + percentage: 30% diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index c7492034a0..ec11d95b92 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -45,6 +45,10 @@ type Strategy struct { // All the opposite trend position will be closed upon the trend change ReverseEMA *indicator.EWMA `json:"reverseEMA"` + // ReverseInterval is the interval to check trend reverse against ReverseEMA. Close price of this interval crossing + // the ReverseEMA triggers main trend change. + ReverseInterval types.Interval `json:"reverseInterval"` + // mainTrendCurrent is the current long-term trend mainTrendCurrent types.Direction // mainTrendPrevious is the long-term trend of previous kline @@ -158,6 +162,11 @@ func (s *Strategy) Validate() error { return errors.New("reverseEMA must be set") } + // Use interval of ReverseEMA if ReverseInterval is omitted + if s.ReverseInterval == "" { + s.ReverseInterval = s.ReverseEMA.Interval + } + if s.FastLinReg == nil { return errors.New("fastLinReg must be set") } @@ -177,6 +186,11 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Initialize ReverseEMA s.ReverseEMA = s.StandardIndicatorSet.EWMA(s.ReverseEMA.IntervalWindow) + // Subscribe for ReverseInterval + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.ReverseInterval, + }) + // Subscribe for LinRegs session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: s.FastLinReg.Interval, @@ -443,15 +457,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se _ = s.ClosePosition(ctx, fixedpoint.NewFromFloat(1.0)) }) - // Main interval - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - // StrategyController - if s.Status != types.StrategyStatusRunning { - return - } - - _ = s.orderExecutor.GracefulCancel(ctx) - + // Check trend reversal + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.ReverseInterval, func(kline types.KLine) { // closePrice is the close price of current kline closePrice := kline.GetClose() // priceReverseEMA is the current ReverseEMA price @@ -464,6 +471,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } else if closePrice.Compare(priceReverseEMA) < 0 { s.mainTrendCurrent = types.DirectionDown } + })) + + // Main interval + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + _ = s.orderExecutor.GracefulCancel(ctx) + + // closePrice is the close price of current kline + closePrice := kline.GetClose() // Trend reversal if s.mainTrendCurrent != s.mainTrendPrevious { From 170c3b8c41cc908b60136faf8a5cff817208b057 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 24 Nov 2022 16:46:40 +0800 Subject: [PATCH 0060/1392] all: remove ftx --- pkg/cmd/account.go | 2 +- pkg/cmd/balances.go | 2 +- pkg/cmd/kline.go | 2 +- pkg/cmd/market.go | 2 +- pkg/cmd/orderbook.go | 2 +- pkg/cmd/trades.go | 2 +- pkg/cmd/userdatastream.go | 2 +- pkg/cmd/utils.go | 18 - pkg/exchange/factory.go | 4 - pkg/exchange/ftx/convert.go | 249 ------ pkg/exchange/ftx/convert_test.go | 121 --- pkg/exchange/ftx/exchange.go | 637 -------------- pkg/exchange/ftx/exchange_test.go | 612 ------------- pkg/exchange/ftx/ftxapi/account.go | 87 -- .../cancel_all_order_request_requestgen.go | 126 --- ...r_by_client_order_id_request_requestgen.go | 133 --- .../ftxapi/cancel_order_request_requestgen.go | 133 --- pkg/exchange/ftx/ftxapi/client.go | 203 ----- pkg/exchange/ftx/ftxapi/client_test.go | 109 --- pkg/exchange/ftx/ftxapi/coin.go | 42 - .../ftxapi/get_account_request_requestgen.go | 115 --- .../ftxapi/get_balances_request_requestgen.go | 115 --- .../ftxapi/get_coins_request_requestgen.go | 115 --- .../ftxapi/get_fills_request_requestgen.go | 187 ---- .../ftxapi/get_market_request_requestgen.go | 155 ---- .../ftxapi/get_markets_request_requestgen.go | 139 --- .../get_open_orders_request_requestgen.go | 128 --- .../get_order_history_request_requestgen.go | 158 ---- .../get_order_status_request_requestgen.go | 131 --- .../get_positions_request_requestgen.go | 115 --- pkg/exchange/ftx/ftxapi/market.go | 59 -- .../ftxapi/place_order_request_requestgen.go | 219 ----- pkg/exchange/ftx/ftxapi/trade.go | 172 ---- pkg/exchange/ftx/ftxapi/types.go | 35 - pkg/exchange/ftx/generate_symbol_map.go | 65 -- pkg/exchange/ftx/orderbook_snapshot.json | 814 ----------------- pkg/exchange/ftx/orderbook_update.json | 26 - pkg/exchange/ftx/rest.go | 269 ------ pkg/exchange/ftx/rest_market_request.go | 53 -- pkg/exchange/ftx/rest_responses.go | 391 --------- pkg/exchange/ftx/rest_test.go | 34 - pkg/exchange/ftx/rest_wallet_request.go | 44 - pkg/exchange/ftx/stream.go | 259 ------ pkg/exchange/ftx/stream_message_handler.go | 169 ---- .../ftx/stream_message_handler_test.go | 119 --- pkg/exchange/ftx/symbols.go | 819 ------------------ pkg/exchange/ftx/ticker_test.go | 54 -- pkg/exchange/ftx/websocket_messages.go | 468 ---------- pkg/exchange/ftx/websocket_messages_test.go | 249 ------ pkg/types/exchange.go | 8 +- pkg/types/exchange_icon.go | 2 - 51 files changed, 9 insertions(+), 8165 deletions(-) delete mode 100644 pkg/exchange/ftx/convert.go delete mode 100644 pkg/exchange/ftx/convert_test.go delete mode 100644 pkg/exchange/ftx/exchange.go delete mode 100644 pkg/exchange/ftx/exchange_test.go delete mode 100644 pkg/exchange/ftx/ftxapi/account.go delete mode 100644 pkg/exchange/ftx/ftxapi/cancel_all_order_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/cancel_order_by_client_order_id_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/cancel_order_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/client.go delete mode 100644 pkg/exchange/ftx/ftxapi/client_test.go delete mode 100644 pkg/exchange/ftx/ftxapi/coin.go delete mode 100644 pkg/exchange/ftx/ftxapi/get_account_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/get_balances_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/get_coins_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/get_fills_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/get_open_orders_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/get_order_history_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/get_order_status_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/get_positions_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/market.go delete mode 100644 pkg/exchange/ftx/ftxapi/place_order_request_requestgen.go delete mode 100644 pkg/exchange/ftx/ftxapi/trade.go delete mode 100644 pkg/exchange/ftx/ftxapi/types.go delete mode 100644 pkg/exchange/ftx/generate_symbol_map.go delete mode 100644 pkg/exchange/ftx/orderbook_snapshot.json delete mode 100644 pkg/exchange/ftx/orderbook_update.json delete mode 100644 pkg/exchange/ftx/rest.go delete mode 100644 pkg/exchange/ftx/rest_market_request.go delete mode 100644 pkg/exchange/ftx/rest_responses.go delete mode 100644 pkg/exchange/ftx/rest_test.go delete mode 100644 pkg/exchange/ftx/rest_wallet_request.go delete mode 100644 pkg/exchange/ftx/stream.go delete mode 100644 pkg/exchange/ftx/stream_message_handler.go delete mode 100644 pkg/exchange/ftx/stream_message_handler_test.go delete mode 100644 pkg/exchange/ftx/symbols.go delete mode 100644 pkg/exchange/ftx/ticker_test.go delete mode 100644 pkg/exchange/ftx/websocket_messages.go delete mode 100644 pkg/exchange/ftx/websocket_messages_test.go diff --git a/pkg/cmd/account.go b/pkg/cmd/account.go index 41ed9465d3..18d5ff6f2a 100644 --- a/pkg/cmd/account.go +++ b/pkg/cmd/account.go @@ -19,7 +19,7 @@ func init() { RootCmd.AddCommand(accountCmd) } -// go run ./cmd/bbgo account --session=ftx --config=config/bbgo.yaml +// go run ./cmd/bbgo account --session=binance --config=config/bbgo.yaml var accountCmd = &cobra.Command{ Use: "account [--session SESSION]", Short: "show user account details (ex: balance)", diff --git a/pkg/cmd/balances.go b/pkg/cmd/balances.go index 492b49d648..9298b409f9 100644 --- a/pkg/cmd/balances.go +++ b/pkg/cmd/balances.go @@ -15,7 +15,7 @@ func init() { RootCmd.AddCommand(balancesCmd) } -// go run ./cmd/bbgo balances --session=ftx +// go run ./cmd/bbgo balances --session=binance var balancesCmd = &cobra.Command{ Use: "balances [--session SESSION]", Short: "Show user account balances", diff --git a/pkg/cmd/kline.go b/pkg/cmd/kline.go index b35abf05f5..e51b822bb9 100644 --- a/pkg/cmd/kline.go +++ b/pkg/cmd/kline.go @@ -14,7 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// go run ./cmd/bbgo kline --exchange=ftx --symbol=BTCUSDT +// go run ./cmd/bbgo kline --exchange=binance --symbol=BTCUSDT var klineCmd = &cobra.Command{ Use: "kline", Short: "connect to the kline market data streaming service of an exchange", diff --git a/pkg/cmd/market.go b/pkg/cmd/market.go index 794f1bba63..1ff22a25ca 100644 --- a/pkg/cmd/market.go +++ b/pkg/cmd/market.go @@ -17,7 +17,7 @@ func init() { RootCmd.AddCommand(marketCmd) } -// go run ./cmd/bbgo market --session=ftx --config=config/bbgo.yaml +// go run ./cmd/bbgo market --session=binance --config=config/bbgo.yaml var marketCmd = &cobra.Command{ Use: "market", Short: "List the symbols that the are available to be traded in the exchange", diff --git a/pkg/cmd/orderbook.go b/pkg/cmd/orderbook.go index bfe9323441..9eba051d6d 100644 --- a/pkg/cmd/orderbook.go +++ b/pkg/cmd/orderbook.go @@ -14,7 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// go run ./cmd/bbgo orderbook --session=ftx --symbol=BTCUSDT +// go run ./cmd/bbgo orderbook --session=binance --symbol=BTCUSDT var orderbookCmd = &cobra.Command{ Use: "orderbook --session=[exchange_name] --symbol=[pair_name]", Short: "connect to the order book market data streaming service of an exchange", diff --git a/pkg/cmd/trades.go b/pkg/cmd/trades.go index 7b93086050..7472fe2a40 100644 --- a/pkg/cmd/trades.go +++ b/pkg/cmd/trades.go @@ -14,7 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// go run ./cmd/bbgo trades --session=ftx --symbol="BTC/USD" +// go run ./cmd/bbgo trades --session=binance --symbol="BTC/USD" var tradesCmd = &cobra.Command{ Use: "trades --session=[exchange_name] --symbol=[pair_name]", Short: "Query trading history", diff --git a/pkg/cmd/userdatastream.go b/pkg/cmd/userdatastream.go index b53a1a6312..29c5d9148b 100644 --- a/pkg/cmd/userdatastream.go +++ b/pkg/cmd/userdatastream.go @@ -14,7 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// go run ./cmd/bbgo userdatastream --session=ftx +// go run ./cmd/bbgo userdatastream --session=binance var userDataStreamCmd = &cobra.Command{ Use: "userdatastream", Short: "Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)", diff --git a/pkg/cmd/utils.go b/pkg/cmd/utils.go index dda83d2ff9..eac567af2e 100644 --- a/pkg/cmd/utils.go +++ b/pkg/cmd/utils.go @@ -1,14 +1,9 @@ package cmd import ( - "fmt" - - "github.com/spf13/viper" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/c9s/bbgo/pkg/exchange/ftx" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -36,16 +31,3 @@ func inBaseAsset(balances types.BalanceMap, market types.Market, price fixedpoin base := balances[market.BaseCurrency] return quote.Total().Div(price).Add(base.Total()) } - -func newExchange(session string) (types.Exchange, error) { - switch session { - case "ftx": - return ftx.NewExchange( - viper.GetString("ftx-api-key"), - viper.GetString("ftx-api-secret"), - viper.GetString("ftx-subaccount"), - ), nil - - } - return nil, fmt.Errorf("unsupported session %s", session) -} diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index d03f8654e9..436b03b5bb 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/c9s/bbgo/pkg/exchange/binance" - "github.com/c9s/bbgo/pkg/exchange/ftx" "github.com/c9s/bbgo/pkg/exchange/kucoin" "github.com/c9s/bbgo/pkg/exchange/max" "github.com/c9s/bbgo/pkg/exchange/okex" @@ -20,9 +19,6 @@ func NewPublic(exchangeName types.ExchangeName) (types.Exchange, error) { func NewStandard(n types.ExchangeName, key, secret, passphrase, subAccount string) (types.Exchange, error) { switch n { - case types.ExchangeFTX: - return ftx.NewExchange(key, secret, subAccount), nil - case types.ExchangeBinance: return binance.New(key, secret), nil diff --git a/pkg/exchange/ftx/convert.go b/pkg/exchange/ftx/convert.go deleted file mode 100644 index f40bc73bb3..0000000000 --- a/pkg/exchange/ftx/convert.go +++ /dev/null @@ -1,249 +0,0 @@ -package ftx - -import ( - "fmt" - "strings" - "time" - - log "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi" - "github.com/c9s/bbgo/pkg/types" -) - -func toGlobalCurrency(original string) string { - return TrimUpperString(original) -} - -func toGlobalSymbol(original string) string { - return strings.ReplaceAll(TrimUpperString(original), "/", "") -} - -func toLocalSymbol(original string) string { - if symbolMap[original] == "" { - return original - } - - return symbolMap[original] -} - -func TrimUpperString(original string) string { - return strings.ToUpper(strings.TrimSpace(original)) -} - -func TrimLowerString(original string) string { - return strings.ToLower(strings.TrimSpace(original)) -} - -var errUnsupportedOrderStatus = fmt.Errorf("unsupported order status") - -func toGlobalOrderNew(r ftxapi.Order) (types.Order, error) { - // In exchange/max/convert.go, it only parses these fields. - timeInForce := types.TimeInForceGTC - if r.Ioc { - timeInForce = types.TimeInForceIOC - } - - // order type definition: https://github.com/ftexchange/ftx/blob/master/rest/client.py#L122 - orderType := types.OrderType(TrimUpperString(string(r.Type))) - if orderType == types.OrderTypeLimit && r.PostOnly { - orderType = types.OrderTypeLimitMaker - } - - o := types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: r.ClientId, - Symbol: toGlobalSymbol(r.Market), - Side: types.SideType(TrimUpperString(string(r.Side))), - Type: orderType, - Quantity: r.Size, - Price: r.Price, - TimeInForce: timeInForce, - }, - Exchange: types.ExchangeFTX, - IsWorking: r.Status == ftxapi.OrderStatusOpen || r.Status == ftxapi.OrderStatusNew, - OrderID: uint64(r.Id), - Status: "", - ExecutedQuantity: r.FilledSize, - CreationTime: types.Time(r.CreatedAt), - UpdateTime: types.Time(r.CreatedAt), - } - - s, err := toGlobalOrderStatus(r, r.Status) - o.Status = s - return o, err -} - -func toGlobalOrderStatus(o ftxapi.Order, s ftxapi.OrderStatus) (types.OrderStatus, error) { - switch s { - case ftxapi.OrderStatusNew: - return types.OrderStatusNew, nil - - case ftxapi.OrderStatusOpen: - if !o.FilledSize.IsZero() { - return types.OrderStatusPartiallyFilled, nil - } else { - return types.OrderStatusNew, nil - } - case ftxapi.OrderStatusClosed: - // filled or canceled - if o.FilledSize == o.Size { - return types.OrderStatusFilled, nil - } else { - // can't distinguish it's canceled or rejected from order response, so always set to canceled - return types.OrderStatusCanceled, nil - } - } - - return "", fmt.Errorf("unsupported ftx order status %s: %w", s, errUnsupportedOrderStatus) -} - -func toGlobalOrder(r order) (types.Order, error) { - // In exchange/max/convert.go, it only parses these fields. - timeInForce := types.TimeInForceGTC - if r.Ioc { - timeInForce = types.TimeInForceIOC - } - - // order type definition: https://github.com/ftexchange/ftx/blob/master/rest/client.py#L122 - orderType := types.OrderType(TrimUpperString(r.Type)) - if orderType == types.OrderTypeLimit && r.PostOnly { - orderType = types.OrderTypeLimitMaker - } - - o := types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: r.ClientId, - Symbol: toGlobalSymbol(r.Market), - Side: types.SideType(TrimUpperString(r.Side)), - Type: orderType, - Quantity: r.Size, - Price: r.Price, - TimeInForce: timeInForce, - }, - Exchange: types.ExchangeFTX, - IsWorking: r.Status == "open", - OrderID: uint64(r.ID), - Status: "", - ExecutedQuantity: r.FilledSize, - CreationTime: types.Time(r.CreatedAt.Time), - UpdateTime: types.Time(r.CreatedAt.Time), - } - - // `new` (accepted but not processed yet), `open`, or `closed` (filled or cancelled) - switch r.Status { - case "new": - o.Status = types.OrderStatusNew - case "open": - if !o.ExecutedQuantity.IsZero() { - o.Status = types.OrderStatusPartiallyFilled - } else { - o.Status = types.OrderStatusNew - } - case "closed": - // filled or canceled - if o.Quantity == o.ExecutedQuantity { - o.Status = types.OrderStatusFilled - } else { - // can't distinguish it's canceled or rejected from order response, so always set to canceled - o.Status = types.OrderStatusCanceled - } - default: - return types.Order{}, fmt.Errorf("unsupported status %s: %w", r.Status, errUnsupportedOrderStatus) - } - - return o, nil -} - -func toGlobalDeposit(input depositHistory) (types.Deposit, error) { - s, err := toGlobalDepositStatus(input.Status) - if err != nil { - log.WithError(err).Warnf("assign empty string to the deposit status") - } - t := input.Time - if input.ConfirmedTime.Time != (time.Time{}) { - t = input.ConfirmedTime - } - d := types.Deposit{ - GID: 0, - Exchange: types.ExchangeFTX, - Time: types.Time(t.Time), - Amount: input.Size, - Asset: toGlobalCurrency(input.Coin), - TransactionID: input.TxID, - Status: s, - Address: input.Address.Address, - AddressTag: input.Address.Tag, - } - return d, nil -} - -func toGlobalDepositStatus(input string) (types.DepositStatus, error) { - // The document only list `confirmed` status - switch input { - case "confirmed", "complete": - return types.DepositSuccess, nil - } - return "", fmt.Errorf("unsupported status %s", input) -} - -func toGlobalTrade(f ftxapi.Fill) (types.Trade, error) { - return types.Trade{ - ID: f.TradeId, - OrderID: f.OrderId, - Exchange: types.ExchangeFTX, - Price: f.Price, - Quantity: f.Size, - QuoteQuantity: f.Price.Mul(f.Size), - Symbol: toGlobalSymbol(f.Market), - Side: types.SideType(strings.ToUpper(string(f.Side))), - IsBuyer: f.Side == ftxapi.SideBuy, - IsMaker: f.Liquidity == ftxapi.LiquidityMaker, - Time: types.Time(f.Time), - Fee: f.Fee, - FeeCurrency: f.FeeCurrency, - IsMargin: false, - IsIsolated: false, - IsFutures: f.Future != "", - }, nil -} - -func toGlobalKLine(symbol string, interval types.Interval, h Candle) (types.KLine, error) { - return types.KLine{ - Exchange: types.ExchangeFTX, - Symbol: toGlobalSymbol(symbol), - StartTime: types.Time(h.StartTime.Time), - EndTime: types.Time(h.StartTime.Add(interval.Duration())), - Interval: interval, - Open: h.Open, - Close: h.Close, - High: h.High, - Low: h.Low, - Volume: h.Volume, - Closed: true, - }, nil -} - -type OrderType string - -const ( - OrderTypeLimit OrderType = "limit" - OrderTypeMarket OrderType = "market" -) - -func toLocalOrderType(orderType types.OrderType) (ftxapi.OrderType, error) { - switch orderType { - - case types.OrderTypeLimitMaker: - return ftxapi.OrderTypeLimit, nil - - case types.OrderTypeLimit: - return ftxapi.OrderTypeLimit, nil - - case types.OrderTypeMarket: - return ftxapi.OrderTypeMarket, nil - - } - - return "", fmt.Errorf("order type %s not supported", orderType) -} diff --git a/pkg/exchange/ftx/convert_test.go b/pkg/exchange/ftx/convert_test.go deleted file mode 100644 index 3a1ea7f1e7..0000000000 --- a/pkg/exchange/ftx/convert_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package ftx - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi" - "github.com/c9s/bbgo/pkg/types" -) - -func Test_toGlobalOrderFromOpenOrder(t *testing.T) { - input := ` -{ - "createdAt": "2019-03-05T09:56:55.728933+00:00", - "filledSize": 10, - "future": "XRP-PERP", - "id": 9596912, - "market": "XRP-PERP", - "price": 0.306525, - "avgFillPrice": 0.306526, - "remainingSize": 31421, - "side": "sell", - "size": 31431, - "status": "open", - "type": "limit", - "reduceOnly": false, - "ioc": false, - "postOnly": false, - "clientId": "client-id-123" -} -` - - var r order - assert.NoError(t, json.Unmarshal([]byte(input), &r)) - - o, err := toGlobalOrder(r) - assert.NoError(t, err) - assert.Equal(t, "client-id-123", o.ClientOrderID) - assert.Equal(t, "XRP-PERP", o.Symbol) - assert.Equal(t, types.SideTypeSell, o.Side) - assert.Equal(t, types.OrderTypeLimit, o.Type) - assert.Equal(t, "31431", o.Quantity.String()) - assert.Equal(t, "0.306525", o.Price.String()) - assert.Equal(t, types.TimeInForceGTC, o.TimeInForce) - assert.Equal(t, types.ExchangeFTX, o.Exchange) - assert.True(t, o.IsWorking) - assert.Equal(t, uint64(9596912), o.OrderID) - assert.Equal(t, types.OrderStatusPartiallyFilled, o.Status) - assert.Equal(t, "10", o.ExecutedQuantity.String()) -} - -func TestTrimLowerString(t *testing.T) { - type args struct { - original string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "spaces", - args: args{ - original: " ", - }, - want: "", - }, - { - name: "uppercase", - args: args{ - original: " HELLO ", - }, - want: "hello", - }, - { - name: "lowercase", - args: args{ - original: " hello", - }, - want: "hello", - }, - { - name: "upper/lower cases", - args: args{ - original: " heLLo ", - }, - want: "hello", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := TrimLowerString(tt.args.original); got != tt.want { - t.Errorf("TrimLowerString() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_toGlobalSymbol(t *testing.T) { - assert.Equal(t, "BTCUSDT", toGlobalSymbol("BTC/USDT")) -} - -func Test_toLocalOrderTypeWithLimitMaker(t *testing.T) { - orderType, err := toLocalOrderType(types.OrderTypeLimitMaker) - assert.NoError(t, err) - assert.Equal(t, ftxapi.OrderTypeLimit, orderType) -} - -func Test_toLocalOrderTypeWithLimit(t *testing.T) { - orderType, err := toLocalOrderType(types.OrderTypeLimit) - assert.NoError(t, err) - assert.Equal(t, ftxapi.OrderTypeLimit, orderType) -} - -func Test_toLocalOrderTypeWithMarket(t *testing.T) { - orderType, err := toLocalOrderType(types.OrderTypeMarket) - assert.NoError(t, err) - assert.Equal(t, ftxapi.OrderTypeMarket, orderType) -} diff --git a/pkg/exchange/ftx/exchange.go b/pkg/exchange/ftx/exchange.go deleted file mode 100644 index d5b3294918..0000000000 --- a/pkg/exchange/ftx/exchange.go +++ /dev/null @@ -1,637 +0,0 @@ -package ftx - -import ( - "context" - "fmt" - "net/http" - "net/url" - "sort" - "strconv" - "strings" - "time" - - "golang.org/x/time/rate" - - "github.com/google/uuid" - "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi" - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -const ( - restEndpoint = "https://ftx.com" - defaultHTTPTimeout = 15 * time.Second -) - -var logger = logrus.WithField("exchange", "ftx") - -// POST https://ftx.com/api/orders 429, Success: false, err: Do not send more than 2 orders on this market per 200ms -var requestLimit = rate.NewLimiter(rate.Every(220*time.Millisecond), 2) - -var marketDataLimiter = rate.NewLimiter(rate.Every(500*time.Millisecond), 2) - -//go:generate go run generate_symbol_map.go - -type Exchange struct { - client *ftxapi.RestClient - - key, secret string - subAccount string - restEndpoint *url.URL - orderAmountReduceFactor fixedpoint.Value -} - -type MarketTicker struct { - Market types.Market - Price fixedpoint.Value - Ask fixedpoint.Value - Bid fixedpoint.Value - Last fixedpoint.Value -} - -type MarketMap map[string]MarketTicker - -// FTX does not have broker ID -const spotBrokerID = "BBGO" - -func newSpotClientOrderID(originalID string) (clientOrderID string) { - prefix := "x-" + spotBrokerID - prefixLen := len(prefix) - - if originalID != "" { - // try to keep the whole original client order ID if user specifies it. - if prefixLen+len(originalID) > 32 { - return originalID - } - - clientOrderID = prefix + originalID - return clientOrderID - } - - clientOrderID = uuid.New().String() - clientOrderID = prefix + clientOrderID - if len(clientOrderID) > 32 { - return clientOrderID[0:32] - } - - return clientOrderID -} - -func NewExchange(key, secret string, subAccount string) *Exchange { - u, err := url.Parse(restEndpoint) - if err != nil { - panic(err) - } - - client := ftxapi.NewClient() - client.Auth(key, secret, subAccount) - return &Exchange{ - client: client, - restEndpoint: u, - key: key, - // pragma: allowlist nextline secret - secret: secret, - subAccount: subAccount, - orderAmountReduceFactor: fixedpoint.One, - } -} - -func (e *Exchange) newRest() *restRequest { - r := newRestRequest(&http.Client{Timeout: defaultHTTPTimeout}, e.restEndpoint).Auth(e.key, e.secret) - if len(e.subAccount) > 0 { - r.SubAccount(e.subAccount) - } - return r -} - -func (e *Exchange) Name() types.ExchangeName { - return types.ExchangeFTX -} - -func (e *Exchange) PlatformFeeCurrency() string { - return toGlobalCurrency("FTT") -} - -func (e *Exchange) NewStream() types.Stream { - return NewStream(e.key, e.secret, e.subAccount, e) -} - -func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { - markets, err := e._queryMarkets(ctx) - if err != nil { - return nil, err - } - marketMap := types.MarketMap{} - for k, v := range markets { - marketMap[k] = v.Market - } - return marketMap, nil -} - -func (e *Exchange) _queryMarkets(ctx context.Context) (MarketMap, error) { - req := e.client.NewGetMarketsRequest() - ftxMarkets, err := req.Do(ctx) - if err != nil { - return nil, err - } - - markets := MarketMap{} - for _, m := range ftxMarkets { - symbol := toGlobalSymbol(m.Name) - symbolMap[symbol] = m.Name - - mkt2 := MarketTicker{ - Market: types.Market{ - Symbol: symbol, - LocalSymbol: m.Name, - // The max precision is length(DefaultPow). For example, currently fixedpoint.DefaultPow - // is 1e8, so the max precision will be 8. - PricePrecision: m.PriceIncrement.NumFractionalDigits(), - VolumePrecision: m.SizeIncrement.NumFractionalDigits(), - QuoteCurrency: toGlobalCurrency(m.QuoteCurrency), - BaseCurrency: toGlobalCurrency(m.BaseCurrency), - // FTX only limit your order by `MinProvideSize`, so I assign zero value to unsupported fields: - // MinNotional, MinAmount, MaxQuantity, MinPrice and MaxPrice. - MinNotional: fixedpoint.Zero, - MinAmount: fixedpoint.Zero, - MinQuantity: m.MinProvideSize, - MaxQuantity: fixedpoint.Zero, - StepSize: m.SizeIncrement, - MinPrice: fixedpoint.Zero, - MaxPrice: fixedpoint.Zero, - TickSize: m.PriceIncrement, - }, - Price: m.Price, - Bid: m.Bid, - Ask: m.Ask, - Last: m.Last, - } - markets[symbol] = mkt2 - } - return markets, nil -} - -func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { - - req := e.client.NewGetAccountRequest() - ftxAccount, err := req.Do(ctx) - if err != nil { - return nil, err - } - - a := &types.Account{ - TotalAccountValue: ftxAccount.TotalAccountValue, - } - - balances, err := e.QueryAccountBalances(ctx) - if err != nil { - return nil, err - } - - a.UpdateBalances(balances) - return a, nil -} - -func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { - balanceReq := e.client.NewGetBalancesRequest() - ftxBalances, err := balanceReq.Do(ctx) - if err != nil { - return nil, err - } - - var balances = make(types.BalanceMap) - for _, r := range ftxBalances { - currency := toGlobalCurrency(r.Coin) - balances[currency] = types.Balance{ - Currency: currency, - Available: r.Free, - Locked: r.Total.Sub(r.Free), - } - } - - return balances, nil -} - -// DefaultFeeRates returns the FTX Tier 1 fee -// See also https://help.ftx.com/hc/en-us/articles/360024479432-Fees -func (e *Exchange) DefaultFeeRates() types.ExchangeFee { - return types.ExchangeFee{ - MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.020), // 0.020% - TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.070), // 0.070% - } -} - -// SetModifyOrderAmountForFee protects the limit buy orders by reducing amount with taker fee. -// The amount is recalculated before submit: submit_amount = original_amount / (1 + taker_fee_rate) . -// This prevents balance exceeding error while closing position without spot margin enabled. -func (e *Exchange) SetModifyOrderAmountForFee(feeRate types.ExchangeFee) { - e.orderAmountReduceFactor = fixedpoint.One.Add(feeRate.TakerFeeRate) -} - -// resolution field in api -// window length in seconds. options: 15, 60, 300, 900, 3600, 14400, 86400, or any multiple of 86400 up to 30*86400 -var supportedIntervals = map[types.Interval]int{ - types.Interval1m: 1 * 60, - types.Interval5m: 5 * 60, - types.Interval15m: 15 * 60, - types.Interval1h: 60 * 60, - types.Interval4h: 60 * 60 * 4, - types.Interval1d: 60 * 60 * 24, - types.Interval3d: 60 * 60 * 24 * 3, -} - -func (e *Exchange) SupportedInterval() map[types.Interval]int { - return supportedIntervals -} - -func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { - return isIntervalSupportedInKLine(interval) -} - -func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - var klines []types.KLine - - // the fetch result is from newest to oldest - // currentEnd = until - // endTime := currentEnd.Add(interval.Duration()) - klines, err := e._queryKLines(ctx, symbol, interval, options) - if err != nil { - return nil, err - } - - klines = types.SortKLinesAscending(klines) - return klines, nil -} - -func (e *Exchange) _queryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - if !isIntervalSupportedInKLine(interval) { - return nil, fmt.Errorf("interval %s is not supported", interval.String()) - } - - if err := marketDataLimiter.Wait(ctx); err != nil { - return nil, err - } - - // assign limit to a default value since ftx has the limit - if options.Limit == 0 { - options.Limit = 500 - } - - // if the time range exceed the ftx valid time range, we need to adjust the endTime - if options.StartTime != nil && options.EndTime != nil { - rangeDuration := options.EndTime.Sub(*options.StartTime) - estimatedCount := rangeDuration / interval.Duration() - - if options.Limit != 0 && uint64(estimatedCount) > uint64(options.Limit) { - endTime := options.StartTime.Add(interval.Duration() * time.Duration(options.Limit)) - options.EndTime = &endTime - } - } - - resp, err := e.newRest().marketRequest.HistoricalPrices(ctx, toLocalSymbol(symbol), interval, int64(options.Limit), options.StartTime, options.EndTime) - if err != nil { - return nil, err - } - if !resp.Success { - return nil, fmt.Errorf("ftx returns failure") - } - - var klines []types.KLine - for _, r := range resp.Result { - globalKline, err := toGlobalKLine(symbol, interval, r) - if err != nil { - return nil, err - } - klines = append(klines, globalKline) - } - - return klines, nil -} - -func isIntervalSupportedInKLine(interval types.Interval) bool { - _, ok := supportedIntervals[interval] - return ok -} - -func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { - tradeIDs := make(map[uint64]struct{}) - lastTradeID := options.LastTradeID - - req := e.client.NewGetFillsRequest() - req.Market(toLocalSymbol(symbol)) - - if options.StartTime != nil { - req.StartTime(*options.StartTime) - } else if options.EndTime != nil { - req.EndTime(*options.EndTime) - } - - req.Order("asc") - fills, err := req.Do(ctx) - if err != nil { - return nil, err - } - - sort.Slice(fills, func(i, j int) bool { - return fills[i].Time.Before(fills[j].Time) - }) - - var trades []types.Trade - symbol = strings.ToUpper(symbol) - for _, fill := range fills { - if _, ok := tradeIDs[fill.TradeId]; ok { - continue - } - - if options.StartTime != nil && fill.Time.Before(*options.StartTime) { - continue - } - - if options.EndTime != nil && fill.Time.After(*options.EndTime) { - continue - } - - if fill.TradeId <= lastTradeID { - continue - } - - tradeIDs[fill.TradeId] = struct{}{} - lastTradeID = fill.TradeId - - t, err := toGlobalTrade(fill) - if err != nil { - return nil, err - } - trades = append(trades, t) - } - - return trades, nil -} - -func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []types.Deposit, err error) { - if until == (time.Time{}) { - until = time.Now() - } - if since.After(until) { - return nil, fmt.Errorf("invalid query deposit history time range, since: %+v, until: %+v", since, until) - } - asset = TrimUpperString(asset) - - resp, err := e.newRest().DepositHistory(ctx, since, until, 0) - if err != nil { - return nil, err - } - if !resp.Success { - return nil, fmt.Errorf("ftx returns failure") - } - sort.Slice(resp.Result, func(i, j int) bool { - return resp.Result[i].Time.Before(resp.Result[j].Time.Time) - }) - for _, r := range resp.Result { - d, err := toGlobalDeposit(r) - if err != nil { - return nil, err - } - if d.Asset == asset && !since.After(d.Time.Time()) && !until.Before(d.Time.Time()) { - allDeposits = append(allDeposits, d) - } - } - return -} - -func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { - // TODO: currently only support limit and market order - // TODO: support time in force - so := order - if err := requestLimit.Wait(ctx); err != nil { - logrus.WithError(err).Error("rate limit error") - } - - orderType, err := toLocalOrderType(so.Type) - if err != nil { - logrus.WithError(err).Error("type error") - } - - submitQuantity := so.Quantity - switch orderType { - case ftxapi.OrderTypeLimit, ftxapi.OrderTypeStopLimit: - submitQuantity = so.Quantity.Div(e.orderAmountReduceFactor) - } - - req := e.client.NewPlaceOrderRequest() - req.Market(toLocalSymbol(TrimUpperString(so.Symbol))) - req.OrderType(orderType) - req.Side(ftxapi.Side(TrimLowerString(string(so.Side)))) - req.Size(submitQuantity) - - switch so.Type { - case types.OrderTypeLimit, types.OrderTypeLimitMaker: - req.Price(so.Price) - - } - - if so.Type == types.OrderTypeLimitMaker { - req.PostOnly(true) - } - - if so.TimeInForce == types.TimeInForceIOC { - req.Ioc(true) - } - - req.ClientID(newSpotClientOrderID(so.ClientOrderID)) - - or, err := req.Do(ctx) - if err != nil { - return nil, fmt.Errorf("failed to place order %+v: %w", so, err) - } - - globalOrder, err := toGlobalOrderNew(*or) - return &globalOrder, err -} - -func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { - orderID, err := strconv.ParseInt(q.OrderID, 10, 64) - if err != nil { - return nil, err - } - - req := e.client.NewGetOrderStatusRequest(uint64(orderID)) - ftxOrder, err := req.Do(ctx) - if err != nil { - return nil, err - } - - o, err := toGlobalOrderNew(*ftxOrder) - if err != nil { - return nil, err - } - - return &o, err -} - -func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { - // TODO: invoke open trigger orders - - req := e.client.NewGetOpenOrdersRequest(toLocalSymbol(symbol)) - ftxOrders, err := req.Do(ctx) - if err != nil { - return nil, err - } - - for _, ftxOrder := range ftxOrders { - o, err := toGlobalOrderNew(ftxOrder) - if err != nil { - return orders, err - } - - orders = append(orders, o) - } - return orders, nil -} - -// symbol, since and until are all optional. FTX can only query by order created time, not updated time. -// FTX doesn't support lastOrderID, so we will query by the time range first, and filter by the lastOrderID. -func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) { - symbol = TrimUpperString(symbol) - - req := e.client.NewGetOrderHistoryRequest(toLocalSymbol(symbol)) - - if since != (time.Time{}) { - req.StartTime(since) - } else if until != (time.Time{}) { - req.EndTime(until) - } - - ftxOrders, err := req.Do(ctx) - if err != nil { - return nil, err - } - - sort.Slice(ftxOrders, func(i, j int) bool { - return ftxOrders[i].CreatedAt.Before(ftxOrders[j].CreatedAt) - }) - - for _, ftxOrder := range ftxOrders { - switch ftxOrder.Status { - case ftxapi.OrderStatusOpen, ftxapi.OrderStatusNew: - continue - } - - o, err := toGlobalOrderNew(ftxOrder) - if err != nil { - return orders, err - } - - orders = append(orders, o) - } - return orders, nil -} - -func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error { - for _, o := range orders { - if err := requestLimit.Wait(ctx); err != nil { - logrus.WithError(err).Error("rate limit error") - } - - var resp *ftxapi.APIResponse - var err error - if len(o.ClientOrderID) > 0 { - req := e.client.NewCancelOrderByClientOrderIdRequest(o.ClientOrderID) - resp, err = req.Do(ctx) - } else { - req := e.client.NewCancelOrderRequest(strconv.FormatUint(o.OrderID, 10)) - resp, err = req.Do(ctx) - } - - if err != nil { - return err - } - - if !resp.Success { - return fmt.Errorf("cancel order failed: %s", resp.Result) - } - } - return nil -} - -func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { - ticketMap, err := e.QueryTickers(ctx, symbol) - if err != nil { - return nil, err - } - - if ticker, ok := ticketMap[symbol]; ok { - return &ticker, nil - } - return nil, fmt.Errorf("ticker %s not found", symbol) -} - -func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { - var tickers = make(map[string]types.Ticker) - - markets, err := e._queryMarkets(ctx) - if err != nil { - return nil, err - } - - m := make(map[string]struct{}) - for _, s := range symbol { - m[toGlobalSymbol(s)] = struct{}{} - } - - rest := e.newRest() - for k, v := range markets { - - // if we provide symbol as condition then we only query the gieven symbol , - // or we should query "ALL" symbol in the market. - if _, ok := m[toGlobalSymbol(k)]; len(symbol) != 0 && !ok { - continue - } - - if err := requestLimit.Wait(ctx); err != nil { - logrus.WithError(err).Errorf("order rate limiter wait error") - } - - // ctx context.Context, market string, interval types.Interval, limit int64, start, end time.Time - now := time.Now() - since := now.Add(time.Duration(-1) * time.Hour) - until := now - prices, err := rest.marketRequest.HistoricalPrices(ctx, v.Market.LocalSymbol, types.Interval1h, 1, &since, &until) - if err != nil || !prices.Success || len(prices.Result) == 0 { - continue - } - - lastCandle := prices.Result[0] - tickers[toGlobalSymbol(k)] = types.Ticker{ - Time: lastCandle.StartTime.Time, - Volume: lastCandle.Volume, - Last: v.Last, - Open: lastCandle.Open, - High: lastCandle.High, - Low: lastCandle.Low, - Buy: v.Bid, - Sell: v.Ask, - } - } - - return tickers, nil -} - -func (e *Exchange) Transfer(ctx context.Context, coin string, size float64, destination string) (string, error) { - payload := TransferPayload{ - Coin: coin, - Size: size, - Source: e.subAccount, - Destination: destination, - } - resp, err := e.newRest().Transfer(ctx, payload) - if err != nil { - return "", err - } - if !resp.Success { - return "", fmt.Errorf("ftx returns transfer failure") - } - return resp.Result.String(), nil -} diff --git a/pkg/exchange/ftx/exchange_test.go b/pkg/exchange/ftx/exchange_test.go deleted file mode 100644 index 1f4f46ebd2..0000000000 --- a/pkg/exchange/ftx/exchange_test.go +++ /dev/null @@ -1,612 +0,0 @@ -package ftx - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -func integrationTestConfigured() (key, secret string, ok bool) { - var hasKey, hasSecret bool - key, hasKey = os.LookupEnv("FTX_API_KEY") - secret, hasSecret = os.LookupEnv("FTX_API_SECRET") - ok = hasKey && hasSecret && os.Getenv("TEST_FTX") == "1" - return key, secret, ok -} - -func TestExchange_IOCOrder(t *testing.T) { - key, secret, ok := integrationTestConfigured() - if !ok { - t.SkipNow() - return - } - - ex := NewExchange(key, secret, "") - createdOrder, err := ex.SubmitOrder(context.Background(), types.SubmitOrder{ - Symbol: "LTCUSDT", - Side: types.SideTypeBuy, - Type: types.OrderTypeLimitMaker, - Quantity: fixedpoint.NewFromFloat(1.0), - Price: fixedpoint.NewFromFloat(50.0), - Market: types.Market{ - Symbol: "LTCUSDT", - LocalSymbol: "LTC/USDT", - PricePrecision: 3, - VolumePrecision: 2, - QuoteCurrency: "USDT", - BaseCurrency: "LTC", - MinQuantity: fixedpoint.NewFromFloat(0.01), - StepSize: fixedpoint.NewFromFloat(0.01), - TickSize: fixedpoint.NewFromFloat(0.01), - }, - TimeInForce: "IOC", - }) - assert.NoError(t, err) - assert.NotEmpty(t, createdOrder) - t.Logf("created orders: %+v", createdOrder) -} - -func TestExchange_QueryAccountBalances(t *testing.T) { - successResp := ` -{ - "result": [ - { - "availableWithoutBorrow": 19.47458865, - "coin": "USD", - "free": 19.48085209, - "spotBorrow": 0.0, - "total": 1094.66405065, - "usdValue": 1094.664050651561 - } - ], - "success": true -} -` - failureResp := `{"result":[],"success":false}` - i := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if i == 0 { - fmt.Fprintln(w, successResp) - i++ - return - } - fmt.Fprintln(w, failureResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryAccountBalances(context.Background()) - assert.NoError(t, err) - - assert.Len(t, resp, 1) - b, ok := resp["USD"] - assert.True(t, ok) - expectedAvailable := fixedpoint.Must(fixedpoint.NewFromString("19.48085209")) - assert.Equal(t, expectedAvailable, b.Available) - assert.Equal(t, fixedpoint.Must(fixedpoint.NewFromString("1094.66405065")).Sub(expectedAvailable), b.Locked) -} - -func TestExchange_QueryOpenOrders(t *testing.T) { - successResp := ` -{ - "success": true, - "result": [ - { - "createdAt": "2019-03-05T09:56:55.728933+00:00", - "filledSize": 10, - "future": "XRP-PERP", - "id": 9596912, - "market": "XRP-PERP", - "price": 0.306525, - "avgFillPrice": 0.306526, - "remainingSize": 31421, - "side": "sell", - "size": 31431, - "status": "open", - "type": "limit", - "reduceOnly": false, - "ioc": false, - "postOnly": false, - "clientId": null - } - ] -} -` - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, successResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryOpenOrders(context.Background(), "XRP-PREP") - assert.NoError(t, err) - assert.Len(t, resp, 1) - assert.Equal(t, "XRP-PERP", resp[0].Symbol) -} - -func TestExchange_QueryClosedOrders(t *testing.T) { - t.Run("no closed orders", func(t *testing.T) { - successResp := `{"success": true, "result": []}` - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, successResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) - assert.NoError(t, err) - - assert.Len(t, resp, 0) - }) - t.Run("one closed order", func(t *testing.T) { - successResp := ` -{ - "success": true, - "result": [ - { - "avgFillPrice": 10135.25, - "clientId": null, - "createdAt": "2019-06-27T15:24:03.101197+00:00", - "filledSize": 0.001, - "future": "BTC-PERP", - "id": 257132591, - "ioc": false, - "market": "BTC-PERP", - "postOnly": false, - "price": 10135.25, - "reduceOnly": false, - "remainingSize": 0.0, - "side": "buy", - "size": 0.001, - "status": "closed", - "type": "limit" - } - ], - "hasMoreData": false -} -` - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, successResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) - assert.NoError(t, err) - assert.Len(t, resp, 1) - assert.Equal(t, "BTC-PERP", resp[0].Symbol) - }) - - t.Run("sort the order", func(t *testing.T) { - successResp := ` -{ - "success": true, - "result": [ - { - "status": "closed", - "createdAt": "2020-09-01T15:24:03.101197+00:00", - "id": 789 - }, - { - "status": "closed", - "createdAt": "2019-03-27T15:24:03.101197+00:00", - "id": 123 - }, - { - "status": "closed", - "createdAt": "2019-06-27T15:24:03.101197+00:00", - "id": 456 - }, - { - "status": "new", - "createdAt": "2019-06-27T15:24:03.101197+00:00", - "id": 999 - } - ], - "hasMoreData": false -} -` - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, successResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) - assert.NoError(t, err) - assert.Len(t, resp, 3) - - expectedOrderID := []uint64{123, 456, 789} - for i, o := range resp { - assert.Equal(t, expectedOrderID[i], o.OrderID) - } - }) -} - -func TestExchange_QueryAccount(t *testing.T) { - balanceResp := ` -{ - "result": [ - { - "availableWithoutBorrow": 19.47458865, - "coin": "USD", - "free": 19.48085209, - "spotBorrow": 0.0, - "total": 1094.66405065, - "usdValue": 1094.664050651561 - } - ], - "success": true -} -` - - accountInfoResp := ` -{ - "success": true, - "result": { - "backstopProvider": true, - "collateral": 3568181.02691129, - "freeCollateral": 1786071.456884368, - "initialMarginRequirement": 0.12222384240257728, - "leverage": 10, - "liquidating": false, - "maintenanceMarginRequirement": 0.07177992558058484, - "makerFee": 0.0002, - "marginFraction": 0.5588433331419503, - "openMarginFraction": 0.2447194090423075, - "takerFee": 0.0005, - "totalAccountValue": 3568180.98341129, - "totalPositionSize": 6384939.6992, - "username": "user@domain.com", - "positions": [ - { - "cost": -31.7906, - "entryPrice": 138.22, - "future": "ETH-PERP", - "initialMarginRequirement": 0.1, - "longOrderSize": 1744.55, - "maintenanceMarginRequirement": 0.04, - "netSize": -0.23, - "openSize": 1744.32, - "realizedPnl": 3.39441714, - "shortOrderSize": 1732.09, - "side": "sell", - "size": 0.23, - "unrealizedPnl": 0 - } - ] - } -} -` - returnBalance := false - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if returnBalance { - fmt.Fprintln(w, balanceResp) - return - } - returnBalance = true - fmt.Fprintln(w, accountInfoResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryAccount(context.Background()) - assert.NoError(t, err) - - b, ok := resp.Balance("USD") - assert.True(t, ok) - expected := types.Balance{ - Currency: "USD", - Available: fixedpoint.MustNewFromString("19.48085209"), - Locked: fixedpoint.MustNewFromString("1094.66405065"), - } - expected.Locked = expected.Locked.Sub(expected.Available) - assert.Equal(t, expected, b) -} - -func TestExchange_QueryMarkets(t *testing.T) { - respJSON := `{ -"success": true, -"result": [ - { - "name": "BTC/USD", - "enabled": true, - "postOnly": false, - "priceIncrement": 1.0, - "sizeIncrement": 0.0001, - "minProvideSize": 0.001, - "last": 59039.0, - "bid": 59038.0, - "ask": 59040.0, - "price": 59039.0, - "type": "spot", - "baseCurrency": "BTC", - "quoteCurrency": "USD", - "underlying": null, - "restricted": false, - "highLeverageFeeExempt": true, - "change1h": 0.0015777151969599294, - "change24h": 0.05475756601279165, - "changeBod": -0.0035107262814994852, - "quoteVolume24h": 316493675.5463, - "volumeUsd24h": 316493675.5463 - } -] -}` - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, respJSON) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - ex.restEndpoint = serverURL - - resp, err := ex.QueryMarkets(context.Background()) - assert.NoError(t, err) - - assert.Len(t, resp, 1) - assert.Equal(t, types.Market{ - Symbol: "BTCUSD", - LocalSymbol: "BTC/USD", - PricePrecision: 0, - VolumePrecision: 4, - QuoteCurrency: "USD", - BaseCurrency: "BTC", - MinQuantity: fixedpoint.NewFromFloat(0.001), - StepSize: fixedpoint.NewFromFloat(0.0001), - TickSize: fixedpoint.NewFromInt(1), - }, resp["BTCUSD"]) -} - -func TestExchange_QueryDepositHistory(t *testing.T) { - respJSON := ` -{ - "success": true, - "result": [ - { - "coin": "TUSD", - "confirmations": 64, - "confirmedTime": "2019-03-05T09:56:55.728933+00:00", - "fee": 0, - "id": 1, - "sentTime": "2019-03-05T09:56:55.735929+00:00", - "size": 99.0, - "status": "confirmed", - "time": "2019-03-05T09:56:55.728933+00:00", - "txid": "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1", - "address": {"address": "test-addr", "tag": "test-tag"} - } - ] -} -` - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, respJSON) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - ex.restEndpoint = serverURL - - ctx := context.Background() - layout := "2006-01-02T15:04:05.999999Z07:00" - actualConfirmedTime, err := time.Parse(layout, "2019-03-05T09:56:55.728933+00:00") - assert.NoError(t, err) - dh, err := ex.QueryDepositHistory(ctx, "TUSD", actualConfirmedTime.Add(-1*time.Hour), actualConfirmedTime.Add(1*time.Hour)) - assert.NoError(t, err) - assert.Len(t, dh, 1) - assert.Equal(t, types.Deposit{ - Exchange: types.ExchangeFTX, - Time: types.Time(actualConfirmedTime), - Amount: fixedpoint.NewFromInt(99), - Asset: "TUSD", - TransactionID: "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1", - Status: types.DepositSuccess, - Address: "test-addr", - AddressTag: "test-tag", - }, dh[0]) - - // not in the time range - dh, err = ex.QueryDepositHistory(ctx, "TUSD", actualConfirmedTime.Add(1*time.Hour), actualConfirmedTime.Add(2*time.Hour)) - assert.NoError(t, err) - assert.Len(t, dh, 0) - - // exclude by asset - dh, err = ex.QueryDepositHistory(ctx, "BTC", actualConfirmedTime.Add(-1*time.Hour), actualConfirmedTime.Add(1*time.Hour)) - assert.NoError(t, err) - assert.Len(t, dh, 0) -} - -func TestExchange_QueryTrades(t *testing.T) { - t.Run("empty response", func(t *testing.T) { - respJSON := ` -{ - "success": true, - "result": [] -} -` - var f fillsResponse - assert.NoError(t, json.Unmarshal([]byte(respJSON), &f)) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, respJSON) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - ctx := context.Background() - actualConfirmedTime, err := parseDatetime("2021-02-23T09:29:08.534000+00:00") - assert.NoError(t, err) - - since := actualConfirmedTime.Add(-1 * time.Hour) - until := actualConfirmedTime.Add(1 * time.Hour) - - // ignore unavailable market - trades, err := ex.QueryTrades(ctx, "TSLA/USD", &types.TradeQueryOptions{ - StartTime: &since, - EndTime: &until, - Limit: 0, - LastTradeID: 0, - }) - assert.NoError(t, err) - assert.Len(t, trades, 0) - }) - - t.Run("duplicated response", func(t *testing.T) { - respJSON := ` -{ - "success": true, - "result": [{ - "id": 123, - "market": "TSLA/USD", - "future": null, - "baseCurrency": "TSLA", - "quoteCurrency": "USD", - "type": "order", - "side": "sell", - "price": 672.5, - "size": 1.0, - "orderId": 456, - "time": "2021-02-23T09:29:08.534000+00:00", - "tradeId": 789, - "feeRate": -5e-6, - "fee": -0.0033625, - "feeCurrency": "USD", - "liquidity": "maker" -}, { - "id": 123, - "market": "TSLA/USD", - "future": null, - "baseCurrency": "TSLA", - "quoteCurrency": "USD", - "type": "order", - "side": "sell", - "price": 672.5, - "size": 1.0, - "orderId": 456, - "time": "2021-02-23T09:29:08.534000+00:00", - "tradeId": 789, - "feeRate": -5e-6, - "fee": -0.0033625, - "feeCurrency": "USD", - "liquidity": "maker" -}] -} -` - var f fillsResponse - assert.NoError(t, json.Unmarshal([]byte(respJSON), &f)) - i := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if i == 0 { - fmt.Fprintln(w, respJSON) - return - } - fmt.Fprintln(w, `{"success":true, "result":[]}`) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - ctx := context.Background() - actualConfirmedTime, err := parseDatetime("2021-02-23T09:29:08.534000+00:00") - assert.NoError(t, err) - - since := actualConfirmedTime.Add(-1 * time.Hour) - until := actualConfirmedTime.Add(1 * time.Hour) - - // ignore unavailable market - trades, err := ex.QueryTrades(ctx, "TSLA/USD", &types.TradeQueryOptions{ - StartTime: &since, - EndTime: &until, - Limit: 0, - LastTradeID: 0, - }) - assert.NoError(t, err) - assert.Len(t, trades, 1) - assert.Equal(t, types.Trade{ - ID: 789, - OrderID: 456, - Exchange: types.ExchangeFTX, - Price: fixedpoint.NewFromFloat(672.5), - Quantity: fixedpoint.One, - QuoteQuantity: fixedpoint.NewFromFloat(672.5 * 1.0), - Symbol: "TSLAUSD", - Side: types.SideTypeSell, - IsBuyer: false, - IsMaker: true, - Time: types.Time(actualConfirmedTime), - Fee: fixedpoint.NewFromFloat(-0.0033625), - FeeCurrency: "USD", - IsMargin: false, - IsIsolated: false, - StrategyID: sql.NullString{}, - PnL: sql.NullFloat64{}, - }, trades[0]) - }) -} - -func Test_isIntervalSupportedInKLine(t *testing.T) { - supportedIntervals := []types.Interval{ - types.Interval1m, - types.Interval5m, - types.Interval15m, - types.Interval1h, - types.Interval1d, - } - for _, i := range supportedIntervals { - assert.True(t, isIntervalSupportedInKLine(i)) - } - assert.False(t, isIntervalSupportedInKLine(types.Interval30m)) - assert.False(t, isIntervalSupportedInKLine(types.Interval2h)) - assert.True(t, isIntervalSupportedInKLine(types.Interval3d)) -} diff --git a/pkg/exchange/ftx/ftxapi/account.go b/pkg/exchange/ftx/ftxapi/account.go deleted file mode 100644 index f6309f272c..0000000000 --- a/pkg/exchange/ftx/ftxapi/account.go +++ /dev/null @@ -1,87 +0,0 @@ -package ftxapi - -//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result -//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result - -import ( - "github.com/c9s/requestgen" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Position struct { - Cost fixedpoint.Value `json:"cost"` - EntryPrice fixedpoint.Value `json:"entryPrice"` - Future string `json:"future"` - InitialMarginRequirement fixedpoint.Value `json:"initialMarginRequirement"` - LongOrderSize fixedpoint.Value `json:"longOrderSize"` - MaintenanceMarginRequirement fixedpoint.Value `json:"maintenanceMarginRequirement"` - NetSize fixedpoint.Value `json:"netSize"` - OpenSize fixedpoint.Value `json:"openSize"` - ShortOrderSize fixedpoint.Value `json:"shortOrderSize"` - Side string `json:"side"` - Size fixedpoint.Value `json:"size"` - RealizedPnl fixedpoint.Value `json:"realizedPnl"` - UnrealizedPnl fixedpoint.Value `json:"unrealizedPnl"` -} - -type Account struct { - BackstopProvider bool `json:"backstopProvider"` - Collateral fixedpoint.Value `json:"collateral"` - FreeCollateral fixedpoint.Value `json:"freeCollateral"` - Leverage fixedpoint.Value `json:"leverage"` - InitialMarginRequirement fixedpoint.Value `json:"initialMarginRequirement"` - MaintenanceMarginRequirement fixedpoint.Value `json:"maintenanceMarginRequirement"` - Liquidating bool `json:"liquidating"` - MakerFee fixedpoint.Value `json:"makerFee"` - MarginFraction fixedpoint.Value `json:"marginFraction"` - OpenMarginFraction fixedpoint.Value `json:"openMarginFraction"` - TakerFee fixedpoint.Value `json:"takerFee"` - TotalAccountValue fixedpoint.Value `json:"totalAccountValue"` - TotalPositionSize fixedpoint.Value `json:"totalPositionSize"` - Username string `json:"username"` - Positions []Position `json:"positions"` -} - -//go:generate GetRequest -url "/api/account" -type GetAccountRequest -responseDataType .Account -type GetAccountRequest struct { - client requestgen.AuthenticatedAPIClient -} - -func (c *RestClient) NewGetAccountRequest() *GetAccountRequest { - return &GetAccountRequest{ - client: c, - } -} - -//go:generate GetRequest -url "/api/positions" -type GetPositionsRequest -responseDataType []Position -type GetPositionsRequest struct { - client requestgen.AuthenticatedAPIClient -} - -func (c *RestClient) NewGetPositionsRequest() *GetPositionsRequest { - return &GetPositionsRequest{ - client: c, - } -} - -type Balance struct { - Coin string `json:"coin"` - Free fixedpoint.Value `json:"free"` - SpotBorrow fixedpoint.Value `json:"spotBorrow"` - Total fixedpoint.Value `json:"total"` - UsdValue fixedpoint.Value `json:"usdValue"` - AvailableWithoutBorrow fixedpoint.Value `json:"availableWithoutBorrow"` -} - -//go:generate GetRequest -url "/api/wallet/balances" -type GetBalancesRequest -responseDataType []Balance -type GetBalancesRequest struct { - client requestgen.AuthenticatedAPIClient -} - -func (c *RestClient) NewGetBalancesRequest() *GetBalancesRequest { - return &GetBalancesRequest{ - client: c, - } -} diff --git a/pkg/exchange/ftx/ftxapi/cancel_all_order_request_requestgen.go b/pkg/exchange/ftx/ftxapi/cancel_all_order_request_requestgen.go deleted file mode 100644 index f47ea614cf..0000000000 --- a/pkg/exchange/ftx/ftxapi/cancel_all_order_request_requestgen.go +++ /dev/null @@ -1,126 +0,0 @@ -// Code generated by "requestgen -method DELETE -url /api/orders -type CancelAllOrderRequest -responseType .APIResponse"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (c *CancelAllOrderRequest) Market(market string) *CancelAllOrderRequest { - c.market = &market - return c -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (c *CancelAllOrderRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (c *CancelAllOrderRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check market field -> json key market - if c.market != nil { - market := *c.market - - // assign parameter of market - params["market"] = market - } else { - } - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (c *CancelAllOrderRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := c.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (c *CancelAllOrderRequest) GetParametersJSON() ([]byte, error) { - params, err := c.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (c *CancelAllOrderRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (c *CancelAllOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (c *CancelAllOrderRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := c.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (c *CancelAllOrderRequest) Do(ctx context.Context) (*APIResponse, error) { - - params, err := c.GetParameters() - if err != nil { - return nil, err - } - query := url.Values{} - - apiURL := "/api/orders" - - req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := c.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return &apiResponse, nil -} diff --git a/pkg/exchange/ftx/ftxapi/cancel_order_by_client_order_id_request_requestgen.go b/pkg/exchange/ftx/ftxapi/cancel_order_by_client_order_id_request_requestgen.go deleted file mode 100644 index 23cb4bab39..0000000000 --- a/pkg/exchange/ftx/ftxapi/cancel_order_by_client_order_id_request_requestgen.go +++ /dev/null @@ -1,133 +0,0 @@ -// Code generated by "requestgen -method DELETE -url /api/orders/by_client_id/:clientOrderId -type CancelOrderByClientOrderIdRequest -responseType .APIResponse"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (c *CancelOrderByClientOrderIdRequest) ClientOrderId(clientOrderId string) *CancelOrderByClientOrderIdRequest { - c.clientOrderId = clientOrderId - return c -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (c *CancelOrderByClientOrderIdRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (c *CancelOrderByClientOrderIdRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (c *CancelOrderByClientOrderIdRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := c.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (c *CancelOrderByClientOrderIdRequest) GetParametersJSON() ([]byte, error) { - params, err := c.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (c *CancelOrderByClientOrderIdRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check clientOrderId field -> json key clientOrderId - clientOrderId := c.clientOrderId - - // TEMPLATE check-required - if len(clientOrderId) == 0 { - return params, fmt.Errorf("clientOrderId is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of clientOrderId - params["clientOrderId"] = clientOrderId - - return params, nil -} - -func (c *CancelOrderByClientOrderIdRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (c *CancelOrderByClientOrderIdRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := c.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (c *CancelOrderByClientOrderIdRequest) Do(ctx context.Context) (*APIResponse, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/orders/by_client_id/:clientOrderId" - slugs, err := c.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = c.applySlugsToUrl(apiURL, slugs) - - req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := c.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return &apiResponse, nil -} diff --git a/pkg/exchange/ftx/ftxapi/cancel_order_request_requestgen.go b/pkg/exchange/ftx/ftxapi/cancel_order_request_requestgen.go deleted file mode 100644 index 70684c1df8..0000000000 --- a/pkg/exchange/ftx/ftxapi/cancel_order_request_requestgen.go +++ /dev/null @@ -1,133 +0,0 @@ -// Code generated by "requestgen -method DELETE -url /api/orders/:orderID -type CancelOrderRequest -responseType .APIResponse"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (c *CancelOrderRequest) OrderID(orderID string) *CancelOrderRequest { - c.orderID = orderID - return c -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (c *CancelOrderRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (c *CancelOrderRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (c *CancelOrderRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := c.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (c *CancelOrderRequest) GetParametersJSON() ([]byte, error) { - params, err := c.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (c *CancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check orderID field -> json key orderID - orderID := c.orderID - - // TEMPLATE check-required - if len(orderID) == 0 { - return params, fmt.Errorf("orderID is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of orderID - params["orderID"] = orderID - - return params, nil -} - -func (c *CancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (c *CancelOrderRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := c.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (c *CancelOrderRequest) Do(ctx context.Context) (*APIResponse, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/orders/:orderID" - slugs, err := c.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = c.applySlugsToUrl(apiURL, slugs) - - req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := c.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return &apiResponse, nil -} diff --git a/pkg/exchange/ftx/ftxapi/client.go b/pkg/exchange/ftx/ftxapi/client.go deleted file mode 100644 index 2437bd48f9..0000000000 --- a/pkg/exchange/ftx/ftxapi/client.go +++ /dev/null @@ -1,203 +0,0 @@ -package ftxapi - -//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result -//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result - -import ( - "bytes" - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/c9s/requestgen" - "github.com/pkg/errors" -) - -const defaultHTTPTimeout = time.Second * 15 -const RestBaseURL = "https://ftx.com/api" - -type APIResponse struct { - Success bool `json:"success"` - Result json.RawMessage `json:"result,omitempty"` - HasMoreData bool `json:"hasMoreData,omitempty"` -} - -type RestClient struct { - BaseURL *url.URL - - client *http.Client - - Key, Secret, subAccount string - - /* - AccountService *AccountService - MarketDataService *MarketDataService - TradeService *TradeService - BulletService *BulletService - */ -} - -func NewClient() *RestClient { - u, err := url.Parse(RestBaseURL) - if err != nil { - panic(err) - } - - client := &RestClient{ - BaseURL: u, - client: &http.Client{ - Timeout: defaultHTTPTimeout, - }, - } - - /* - client.AccountService = &AccountService{client: client} - client.MarketDataService = &MarketDataService{client: client} - client.TradeService = &TradeService{client: client} - client.BulletService = &BulletService{client: client} - */ - return client -} - -func (c *RestClient) Auth(key, secret, subAccount string) { - c.Key = key - // pragma: allowlist nextline secret - c.Secret = secret - c.subAccount = subAccount -} - -// NewRequest create new API request. Relative url can be provided in refURL. -func (c *RestClient) NewRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { - rel, err := url.Parse(refURL) - if err != nil { - return nil, err - } - - if params != nil { - rel.RawQuery = params.Encode() - } - - body, err := castPayload(payload) - if err != nil { - return nil, err - } - - pathURL := c.BaseURL.ResolveReference(rel) - return http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) -} - -// sendRequest sends the request to the API server and handle the response -func (c *RestClient) SendRequest(req *http.Request) (*requestgen.Response, error) { - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - // newResponse reads the response body and return a new Response object - response, err := requestgen.NewResponse(resp) - if err != nil { - return response, err - } - - // Check error, if there is an error, return the ErrorResponse struct type - if response.IsError() { - return response, errors.New(string(response.Body)) - } - - return response, nil -} - -// newAuthenticatedRequest creates new http request for authenticated routes. -func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { - if len(c.Key) == 0 { - return nil, errors.New("empty api key") - } - - if len(c.Secret) == 0 { - return nil, errors.New("empty api secret") - } - - rel, err := url.Parse(refURL) - if err != nil { - return nil, err - } - - if params != nil { - rel.RawQuery = params.Encode() - } - - // pathURL is for sending request - pathURL := c.BaseURL.ResolveReference(rel) - - // path here is used for auth header - path := pathURL.Path - if rel.RawQuery != "" { - path += "?" + rel.RawQuery - } - - body, err := castPayload(payload) - if err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Accept", "application/json") - - // Build authentication headers - c.attachAuthHeaders(req, method, path, body) - return req, nil -} - -func (c *RestClient) attachAuthHeaders(req *http.Request, method string, path string, body []byte) { - millisecondTs := time.Now().UnixNano() / int64(time.Millisecond) - ts := strconv.FormatInt(millisecondTs, 10) - p := ts + method + path + string(body) - signature := sign(c.Secret, p) - req.Header.Set("FTX-KEY", c.Key) - req.Header.Set("FTX-SIGN", signature) - req.Header.Set("FTX-TS", ts) - if c.subAccount != "" { - req.Header.Set("FTX-SUBACCOUNT", c.subAccount) - } -} - -// sign uses sha256 to sign the payload with the given secret -func sign(secret, payload string) string { - var sig = hmac.New(sha256.New, []byte(secret)) - _, err := sig.Write([]byte(payload)) - if err != nil { - return "" - } - - return hex.EncodeToString(sig.Sum(nil)) -} - -func castPayload(payload interface{}) ([]byte, error) { - if payload != nil { - switch v := payload.(type) { - case string: - return []byte(v), nil - - case []byte: - return v, nil - - default: - body, err := json.Marshal(v) - return body, err - } - } - - return nil, nil -} diff --git a/pkg/exchange/ftx/ftxapi/client_test.go b/pkg/exchange/ftx/ftxapi/client_test.go deleted file mode 100644 index a73f595663..0000000000 --- a/pkg/exchange/ftx/ftxapi/client_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package ftxapi - -import ( - "context" - "os" - "regexp" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -func maskSecret(s string) string { - re := regexp.MustCompile(`\b(\w{4})\w+\b`) - s = re.ReplaceAllString(s, "$1******") - return s -} - -func integrationTestConfigured(t *testing.T) (key, secret string, ok bool) { - var hasKey, hasSecret bool - key, hasKey = os.LookupEnv("FTX_API_KEY") - secret, hasSecret = os.LookupEnv("FTX_API_SECRET") - ok = hasKey && hasSecret && os.Getenv("TEST_FTX") == "1" - if ok { - t.Logf("ftx api integration test enabled, key = %s, secret = %s", maskSecret(key), maskSecret(secret)) - } - return key, secret, ok -} - -func TestClient_Requests(t *testing.T) { - key, secret, ok := integrationTestConfigured(t) - if !ok { - t.SkipNow() - return - } - - ctx, cancel := context.WithTimeout(context.TODO(), 15*time.Second) - defer cancel() - - client := NewClient() - client.Auth(key, secret, "") - - testCases := []struct { - name string - tt func(t *testing.T) - }{ - { - name: "GetMarketsRequest", - tt: func(t *testing.T) { - req := client.NewGetMarketsRequest() - markets, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, markets) - t.Logf("markets: %+v", markets) - }, - }, - { - name: "GetAccountRequest", - tt: func(t *testing.T) { - req := client.NewGetAccountRequest() - account, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, account) - t.Logf("account: %+v", account) - }, - }, - { - name: "PlaceOrderRequest", - tt: func(t *testing.T) { - req := client.NewPlaceOrderRequest() - req.PostOnly(true). - Size(fixedpoint.MustNewFromString("1.0")). - Price(fixedpoint.MustNewFromString("10.0")). - OrderType(OrderTypeLimit). - Side(SideBuy). - Market("LTC/USDT") - - createdOrder, err := req.Do(ctx) - if assert.NoError(t, err) { - assert.NotNil(t, createdOrder) - t.Logf("createdOrder: %+v", createdOrder) - - req2 := client.NewCancelOrderRequest(strconv.FormatInt(createdOrder.Id, 10)) - ret, err := req2.Do(ctx) - assert.NoError(t, err) - t.Logf("cancelOrder: %+v", ret) - assert.True(t, ret.Success) - } - }, - }, - { - name: "GetFillsRequest", - tt: func(t *testing.T) { - req := client.NewGetFillsRequest() - req.Market("CRO/USD") - fills, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, fills) - t.Logf("fills: %+v", fills) - }, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, testCase.tt) - } -} diff --git a/pkg/exchange/ftx/ftxapi/coin.go b/pkg/exchange/ftx/ftxapi/coin.go deleted file mode 100644 index ca8d81a55c..0000000000 --- a/pkg/exchange/ftx/ftxapi/coin.go +++ /dev/null @@ -1,42 +0,0 @@ -package ftxapi - -//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result -//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result - -import ( - "github.com/c9s/requestgen" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Coin struct { - Bep2Asset *string `json:"bep2Asset"` - CanConvert bool `json:"canConvert"` - CanDeposit bool `json:"canDeposit"` - CanWithdraw bool `json:"canWithdraw"` - Collateral bool `json:"collateral"` - CollateralWeight fixedpoint.Value `json:"collateralWeight"` - CreditTo *string `json:"creditTo"` - Erc20Contract string `json:"erc20Contract"` - Fiat bool `json:"fiat"` - HasTag bool `json:"hasTag"` - Id string `json:"id"` - IsToken bool `json:"isToken"` - Methods []string `json:"methods"` - Name string `json:"name"` - SplMint string `json:"splMint"` - Trc20Contract string `json:"trc20Contract"` - UsdFungible bool `json:"usdFungible"` -} - -//go:generate GetRequest -url "api/coins" -type GetCoinsRequest -responseDataType []Coin -type GetCoinsRequest struct { - client requestgen.AuthenticatedAPIClient -} - -func (c *RestClient) NewGetCoinsRequest() *GetCoinsRequest { - return &GetCoinsRequest{ - client: c, - } -} diff --git a/pkg/exchange/ftx/ftxapi/get_account_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_account_request_requestgen.go deleted file mode 100644 index ef153bc72a..0000000000 --- a/pkg/exchange/ftx/ftxapi/get_account_request_requestgen.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/account -type GetAccountRequest -responseDataType .Account"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetAccountRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetAccountRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetAccountRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetAccountRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetAccountRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetAccountRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetAccountRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetAccountRequest) Do(ctx context.Context) (*Account, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/account" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data Account - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return &data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_balances_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_balances_request_requestgen.go deleted file mode 100644 index e67a36299d..0000000000 --- a/pkg/exchange/ftx/ftxapi/get_balances_request_requestgen.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/wallet/balances -type GetBalancesRequest -responseDataType []Balance"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetBalancesRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetBalancesRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetBalancesRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetBalancesRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetBalancesRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetBalancesRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetBalancesRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetBalancesRequest) Do(ctx context.Context) ([]Balance, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/wallet/balances" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Balance - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_coins_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_coins_request_requestgen.go deleted file mode 100644 index 3e5547c795..0000000000 --- a/pkg/exchange/ftx/ftxapi/get_coins_request_requestgen.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url api/coins -type GetCoinsRequest -responseDataType []Coin"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetCoinsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetCoinsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetCoinsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetCoinsRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetCoinsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetCoinsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetCoinsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetCoinsRequest) Do(ctx context.Context) ([]Coin, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "api/coins" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Coin - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_fills_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_fills_request_requestgen.go deleted file mode 100644 index 714817710e..0000000000 --- a/pkg/exchange/ftx/ftxapi/get_fills_request_requestgen.go +++ /dev/null @@ -1,187 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/fills -type GetFillsRequest -responseDataType []Fill"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" - "strconv" - "time" -) - -func (g *GetFillsRequest) Market(market string) *GetFillsRequest { - g.market = &market - return g -} - -func (g *GetFillsRequest) StartTime(startTime time.Time) *GetFillsRequest { - g.startTime = &startTime - return g -} - -func (g *GetFillsRequest) EndTime(endTime time.Time) *GetFillsRequest { - g.endTime = &endTime - return g -} - -func (g *GetFillsRequest) OrderID(orderID int) *GetFillsRequest { - g.orderID = &orderID - return g -} - -func (g *GetFillsRequest) Order(order string) *GetFillsRequest { - g.order = &order - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetFillsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check market field -> json key market - if g.market != nil { - market := *g.market - - // assign parameter of market - params["market"] = market - } else { - } - // check startTime field -> json key start_time - if g.startTime != nil { - startTime := *g.startTime - - // assign parameter of startTime - // convert time.Time to seconds time stamp - params["start_time"] = strconv.FormatInt(startTime.Unix(), 10) - } else { - } - // check endTime field -> json key end_time - if g.endTime != nil { - endTime := *g.endTime - - // assign parameter of endTime - // convert time.Time to seconds time stamp - params["end_time"] = strconv.FormatInt(endTime.Unix(), 10) - } else { - } - // check orderID field -> json key orderId - if g.orderID != nil { - orderID := *g.orderID - - // assign parameter of orderID - params["orderId"] = orderID - } else { - } - // check order field -> json key order - if g.order != nil { - order := *g.order - - // assign parameter of order - params["order"] = order - } else { - } - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetFillsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetFillsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetFillsRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetFillsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetFillsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetFillsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetFillsRequest) Do(ctx context.Context) ([]Fill, error) { - - // no body params - var params interface{} - query, err := g.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/api/fills" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Fill - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go deleted file mode 100644 index 72825a4c29..0000000000 --- a/pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go +++ /dev/null @@ -1,155 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url api/markets/:market -type GetMarketRequest -responseDataType .Market"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "reflect" - "regexp" -) - -func (g *GetMarketRequest) Market(market string) *GetMarketRequest { - g.market = market - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetMarketRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for _k, _v := range params { - query.Add(_k, fmt.Sprintf("%v", _v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetMarketRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetMarketRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for _k, _v := range params { - if g.isVarSlice(_v) { - g.iterateSlice(_v, func(it interface{}) { - query.Add(_k+"[]", fmt.Sprintf("%v", it)) - }) - } else { - query.Add(_k, fmt.Sprintf("%v", _v)) - } - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetMarketRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetMarketRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := g.market - - // assign parameter of market - params["market"] = market - - return params, nil -} - -func (g *GetMarketRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for _k, _v := range slugs { - needleRE := regexp.MustCompile(":" + _k + "\\b") - url = needleRE.ReplaceAllString(url, _v) - } - - return url -} - -func (g *GetMarketRequest) iterateSlice(slice interface{}, _f func(it interface{})) { - sliceValue := reflect.ValueOf(slice) - for _i := 0; _i < sliceValue.Len(); _i++ { - it := sliceValue.Index(_i).Interface() - _f(it) - } -} - -func (g *GetMarketRequest) isVarSlice(_v interface{}) bool { - rt := reflect.TypeOf(_v) - switch rt.Kind() { - case reflect.Slice: - return true - } - return false -} - -func (g *GetMarketRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for _k, _v := range params { - slugs[_k] = fmt.Sprintf("%v", _v) - } - - return slugs, nil -} - -func (g *GetMarketRequest) Do(ctx context.Context) (*Market, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "api/markets/:market" - slugs, err := g.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = g.applySlugsToUrl(apiURL, slugs) - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data Market - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return &data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go deleted file mode 100644 index db8e591bc8..0000000000 --- a/pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go +++ /dev/null @@ -1,139 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url api/markets -type GetMarketsRequest -responseDataType []Market"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "reflect" - "regexp" -) - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetMarketsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for _k, _v := range params { - query.Add(_k, fmt.Sprintf("%v", _v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetMarketsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetMarketsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for _k, _v := range params { - if g.isVarSlice(_v) { - g.iterateSlice(_v, func(it interface{}) { - query.Add(_k+"[]", fmt.Sprintf("%v", it)) - }) - } else { - query.Add(_k, fmt.Sprintf("%v", _v)) - } - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetMarketsRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetMarketsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetMarketsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for _k, _v := range slugs { - needleRE := regexp.MustCompile(":" + _k + "\\b") - url = needleRE.ReplaceAllString(url, _v) - } - - return url -} - -func (g *GetMarketsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { - sliceValue := reflect.ValueOf(slice) - for _i := 0; _i < sliceValue.Len(); _i++ { - it := sliceValue.Index(_i).Interface() - _f(it) - } -} - -func (g *GetMarketsRequest) isVarSlice(_v interface{}) bool { - rt := reflect.TypeOf(_v) - switch rt.Kind() { - case reflect.Slice: - return true - } - return false -} - -func (g *GetMarketsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for _k, _v := range params { - slugs[_k] = fmt.Sprintf("%v", _v) - } - - return slugs, nil -} - -func (g *GetMarketsRequest) Do(ctx context.Context) ([]Market, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "api/markets" - - req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Market - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_open_orders_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_open_orders_request_requestgen.go deleted file mode 100644 index b36d0fede8..0000000000 --- a/pkg/exchange/ftx/ftxapi/get_open_orders_request_requestgen.go +++ /dev/null @@ -1,128 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/orders -type GetOpenOrdersRequest -responseDataType []Order"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (g *GetOpenOrdersRequest) Market(market string) *GetOpenOrdersRequest { - g.market = market - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetOpenOrdersRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := g.market - - // assign parameter of market - params["market"] = market - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetOpenOrdersRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetOpenOrdersRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetOpenOrdersRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetOpenOrdersRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetOpenOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetOpenOrdersRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetOpenOrdersRequest) Do(ctx context.Context) ([]Order, error) { - - // no body params - var params interface{} - query, err := g.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/api/orders" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Order - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_order_history_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_order_history_request_requestgen.go deleted file mode 100644 index e10a4da17b..0000000000 --- a/pkg/exchange/ftx/ftxapi/get_order_history_request_requestgen.go +++ /dev/null @@ -1,158 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/orders/history -type GetOrderHistoryRequest -responseDataType []Order"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" - "strconv" - "time" -) - -func (g *GetOrderHistoryRequest) Market(market string) *GetOrderHistoryRequest { - g.market = market - return g -} - -func (g *GetOrderHistoryRequest) StartTime(startTime time.Time) *GetOrderHistoryRequest { - g.startTime = &startTime - return g -} - -func (g *GetOrderHistoryRequest) EndTime(endTime time.Time) *GetOrderHistoryRequest { - g.endTime = &endTime - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetOrderHistoryRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := g.market - - // assign parameter of market - params["market"] = market - // check startTime field -> json key start_time - if g.startTime != nil { - startTime := *g.startTime - - // assign parameter of startTime - // convert time.Time to seconds time stamp - params["start_time"] = strconv.FormatInt(startTime.Unix(), 10) - } else { - } - // check endTime field -> json key end_time - if g.endTime != nil { - endTime := *g.endTime - - // assign parameter of endTime - // convert time.Time to seconds time stamp - params["end_time"] = strconv.FormatInt(endTime.Unix(), 10) - } else { - } - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetOrderHistoryRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetOrderHistoryRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetOrderHistoryRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetOrderHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetOrderHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetOrderHistoryRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetOrderHistoryRequest) Do(ctx context.Context) ([]Order, error) { - - // no body params - var params interface{} - query, err := g.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/api/orders/history" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Order - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_order_status_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_order_status_request_requestgen.go deleted file mode 100644 index 6b613611a7..0000000000 --- a/pkg/exchange/ftx/ftxapi/get_order_status_request_requestgen.go +++ /dev/null @@ -1,131 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/orders/:orderId -type GetOrderStatusRequest -responseDataType .Order"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (g *GetOrderStatusRequest) OrderID(orderID uint64) *GetOrderStatusRequest { - g.orderID = orderID - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetOrderStatusRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetOrderStatusRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetOrderStatusRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetOrderStatusRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetOrderStatusRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check orderID field -> json key orderId - orderID := g.orderID - - // assign parameter of orderID - params["orderId"] = orderID - - return params, nil -} - -func (g *GetOrderStatusRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetOrderStatusRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetOrderStatusRequest) Do(ctx context.Context) (*Order, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/orders/:orderId" - slugs, err := g.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = g.applySlugsToUrl(apiURL, slugs) - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data Order - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return &data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_positions_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_positions_request_requestgen.go deleted file mode 100644 index d77811f6fe..0000000000 --- a/pkg/exchange/ftx/ftxapi/get_positions_request_requestgen.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/positions -type GetPositionsRequest -responseDataType []Position"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetPositionsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetPositionsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetPositionsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetPositionsRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetPositionsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetPositionsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetPositionsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetPositionsRequest) Do(ctx context.Context) ([]Position, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/positions" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Position - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/market.go b/pkg/exchange/ftx/ftxapi/market.go deleted file mode 100644 index 4cac2fee9e..0000000000 --- a/pkg/exchange/ftx/ftxapi/market.go +++ /dev/null @@ -1,59 +0,0 @@ -package ftxapi - -//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result -//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result - -import ( - "github.com/c9s/requestgen" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Market struct { - Name string `json:"name"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - QuoteVolume24H fixedpoint.Value `json:"quoteVolume24h"` - Change1H fixedpoint.Value `json:"change1h"` - Change24H fixedpoint.Value `json:"change24h"` - ChangeBod fixedpoint.Value `json:"changeBod"` - VolumeUsd24H fixedpoint.Value `json:"volumeUsd24h"` - HighLeverageFeeExempt bool `json:"highLeverageFeeExempt"` - MinProvideSize fixedpoint.Value `json:"minProvideSize"` - Type string `json:"type"` - Underlying string `json:"underlying"` - Enabled bool `json:"enabled"` - Ask fixedpoint.Value `json:"ask"` - Bid fixedpoint.Value `json:"bid"` - Last fixedpoint.Value `json:"last"` - PostOnly bool `json:"postOnly"` - Price fixedpoint.Value `json:"price"` - PriceIncrement fixedpoint.Value `json:"priceIncrement"` - SizeIncrement fixedpoint.Value `json:"sizeIncrement"` - Restricted bool `json:"restricted"` -} - -//go:generate GetRequest -url "api/markets" -type GetMarketsRequest -responseDataType []Market -type GetMarketsRequest struct { - client requestgen.APIClient -} - -func (c *RestClient) NewGetMarketsRequest() *GetMarketsRequest { - return &GetMarketsRequest{ - client: c, - } -} - -//go:generate GetRequest -url "api/markets/:market" -type GetMarketRequest -responseDataType .Market -type GetMarketRequest struct { - client requestgen.AuthenticatedAPIClient - market string `param:"market,slug"` -} - -func (c *RestClient) NewGetMarketRequest(market string) *GetMarketRequest { - return &GetMarketRequest{ - client: c, - market: market, - } -} diff --git a/pkg/exchange/ftx/ftxapi/place_order_request_requestgen.go b/pkg/exchange/ftx/ftxapi/place_order_request_requestgen.go deleted file mode 100644 index 994011ca96..0000000000 --- a/pkg/exchange/ftx/ftxapi/place_order_request_requestgen.go +++ /dev/null @@ -1,219 +0,0 @@ -// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Result -url /api/orders -type PlaceOrderRequest -responseDataType .Order"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "github.com/c9s/bbgo/pkg/fixedpoint" - "net/url" - "regexp" -) - -func (p *PlaceOrderRequest) Market(market string) *PlaceOrderRequest { - p.market = market - return p -} - -func (p *PlaceOrderRequest) Side(side Side) *PlaceOrderRequest { - p.side = side - return p -} - -func (p *PlaceOrderRequest) Price(price fixedpoint.Value) *PlaceOrderRequest { - p.price = price - return p -} - -func (p *PlaceOrderRequest) Size(size fixedpoint.Value) *PlaceOrderRequest { - p.size = size - return p -} - -func (p *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest { - p.orderType = orderType - return p -} - -func (p *PlaceOrderRequest) Ioc(ioc bool) *PlaceOrderRequest { - p.ioc = &ioc - return p -} - -func (p *PlaceOrderRequest) PostOnly(postOnly bool) *PlaceOrderRequest { - p.postOnly = &postOnly - return p -} - -func (p *PlaceOrderRequest) ClientID(clientID string) *PlaceOrderRequest { - p.clientID = &clientID - return p -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (p *PlaceOrderRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (p *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := p.market - - // TEMPLATE check-required - if len(market) == 0 { - return params, fmt.Errorf("market is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of market - params["market"] = market - // check side field -> json key side - side := p.side - - // TEMPLATE check-required - if len(side) == 0 { - return params, fmt.Errorf("side is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of side - params["side"] = side - // check price field -> json key price - price := p.price - - // assign parameter of price - params["price"] = price - // check size field -> json key size - size := p.size - - // assign parameter of size - params["size"] = size - // check orderType field -> json key type - orderType := p.orderType - - // assign parameter of orderType - params["type"] = orderType - // check ioc field -> json key ioc - if p.ioc != nil { - ioc := *p.ioc - - // assign parameter of ioc - params["ioc"] = ioc - } else { - } - // check postOnly field -> json key postOnly - if p.postOnly != nil { - postOnly := *p.postOnly - - // assign parameter of postOnly - params["postOnly"] = postOnly - } else { - } - // check clientID field -> json key clientId - if p.clientID != nil { - clientID := *p.clientID - - // assign parameter of clientID - params["clientId"] = clientID - } else { - } - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (p *PlaceOrderRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := p.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (p *PlaceOrderRequest) GetParametersJSON() ([]byte, error) { - params, err := p.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (p *PlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (p *PlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (p *PlaceOrderRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := p.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (p *PlaceOrderRequest) Do(ctx context.Context) (*Order, error) { - - params, err := p.GetParameters() - if err != nil { - return nil, err - } - query := url.Values{} - - apiURL := "/api/orders" - - req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := p.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data Order - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return &data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/trade.go b/pkg/exchange/ftx/ftxapi/trade.go deleted file mode 100644 index 323481d20d..0000000000 --- a/pkg/exchange/ftx/ftxapi/trade.go +++ /dev/null @@ -1,172 +0,0 @@ -package ftxapi - -//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result -//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result - -import ( - "time" - - "github.com/c9s/requestgen" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Order struct { - CreatedAt time.Time `json:"createdAt"` - Future string `json:"future"` - Id int64 `json:"id"` - Market string `json:"market"` - Price fixedpoint.Value `json:"price"` - AvgFillPrice fixedpoint.Value `json:"avgFillPrice"` - Size fixedpoint.Value `json:"size"` - RemainingSize fixedpoint.Value `json:"remainingSize"` - FilledSize fixedpoint.Value `json:"filledSize"` - Side Side `json:"side"` - Status OrderStatus `json:"status"` - Type OrderType `json:"type"` - ReduceOnly bool `json:"reduceOnly"` - Ioc bool `json:"ioc"` - PostOnly bool `json:"postOnly"` - ClientId string `json:"clientId"` -} - -//go:generate GetRequest -url "/api/orders" -type GetOpenOrdersRequest -responseDataType []Order -type GetOpenOrdersRequest struct { - client requestgen.AuthenticatedAPIClient - market string `param:"market,query"` -} - -func (c *RestClient) NewGetOpenOrdersRequest(market string) *GetOpenOrdersRequest { - return &GetOpenOrdersRequest{ - client: c, - market: market, - } -} - -//go:generate GetRequest -url "/api/orders/history" -type GetOrderHistoryRequest -responseDataType []Order -type GetOrderHistoryRequest struct { - client requestgen.AuthenticatedAPIClient - - market string `param:"market,query"` - - startTime *time.Time `param:"start_time,seconds,query"` - endTime *time.Time `param:"end_time,seconds,query"` -} - -func (c *RestClient) NewGetOrderHistoryRequest(market string) *GetOrderHistoryRequest { - return &GetOrderHistoryRequest{ - client: c, - market: market, - } -} - -//go:generate PostRequest -url "/api/orders" -type PlaceOrderRequest -responseDataType .Order -type PlaceOrderRequest struct { - client requestgen.AuthenticatedAPIClient - - market string `param:"market,required"` - side Side `param:"side,required"` - price fixedpoint.Value `param:"price"` - size fixedpoint.Value `param:"size"` - orderType OrderType `param:"type"` - ioc *bool `param:"ioc"` - postOnly *bool `param:"postOnly"` - clientID *string `param:"clientId,optional"` -} - -func (c *RestClient) NewPlaceOrderRequest() *PlaceOrderRequest { - return &PlaceOrderRequest{ - client: c, - } -} - -//go:generate requestgen -method DELETE -url "/api/orders/:orderID" -type CancelOrderRequest -responseType .APIResponse -type CancelOrderRequest struct { - client requestgen.AuthenticatedAPIClient - orderID string `param:"orderID,required,slug"` -} - -func (c *RestClient) NewCancelOrderRequest(orderID string) *CancelOrderRequest { - return &CancelOrderRequest{ - client: c, - orderID: orderID, - } -} - -//go:generate requestgen -method DELETE -url "/api/orders" -type CancelAllOrderRequest -responseType .APIResponse -type CancelAllOrderRequest struct { - client requestgen.AuthenticatedAPIClient - market *string `param:"market"` -} - -func (c *RestClient) NewCancelAllOrderRequest() *CancelAllOrderRequest { - return &CancelAllOrderRequest{ - client: c, - } -} - -//go:generate requestgen -method DELETE -url "/api/orders/by_client_id/:clientOrderId" -type CancelOrderByClientOrderIdRequest -responseType .APIResponse -type CancelOrderByClientOrderIdRequest struct { - client requestgen.AuthenticatedAPIClient - clientOrderId string `param:"clientOrderId,required,slug"` -} - -func (c *RestClient) NewCancelOrderByClientOrderIdRequest(clientOrderId string) *CancelOrderByClientOrderIdRequest { - return &CancelOrderByClientOrderIdRequest{ - client: c, - clientOrderId: clientOrderId, - } -} - -type Fill struct { - // Id is fill ID - Id uint64 `json:"id"` - Future string `json:"future"` - Liquidity Liquidity `json:"liquidity"` - Market string `json:"market"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - OrderId uint64 `json:"orderId"` - TradeId uint64 `json:"tradeId"` - Price fixedpoint.Value `json:"price"` - Side Side `json:"side"` - Size fixedpoint.Value `json:"size"` - Time time.Time `json:"time"` - Type string `json:"type"` // always = "order" - Fee fixedpoint.Value `json:"fee"` - FeeCurrency string `json:"feeCurrency"` - FeeRate fixedpoint.Value `json:"feeRate"` -} - -//go:generate GetRequest -url "/api/fills" -type GetFillsRequest -responseDataType []Fill -type GetFillsRequest struct { - client requestgen.AuthenticatedAPIClient - - market *string `param:"market,query"` - startTime *time.Time `param:"start_time,seconds,query"` - endTime *time.Time `param:"end_time,seconds,query"` - orderID *int `param:"orderId,query"` - - // order is the order of the returned records, asc or null - order *string `param:"order,query"` -} - -func (c *RestClient) NewGetFillsRequest() *GetFillsRequest { - return &GetFillsRequest{ - client: c, - } -} - -//go:generate GetRequest -url "/api/orders/:orderId" -type GetOrderStatusRequest -responseDataType .Order -type GetOrderStatusRequest struct { - client requestgen.AuthenticatedAPIClient - orderID uint64 `param:"orderId,slug"` -} - -func (c *RestClient) NewGetOrderStatusRequest(orderID uint64) *GetOrderStatusRequest { - return &GetOrderStatusRequest{ - client: c, - orderID: orderID, - } -} diff --git a/pkg/exchange/ftx/ftxapi/types.go b/pkg/exchange/ftx/ftxapi/types.go deleted file mode 100644 index fdfe6cd784..0000000000 --- a/pkg/exchange/ftx/ftxapi/types.go +++ /dev/null @@ -1,35 +0,0 @@ -package ftxapi - -type Liquidity string - -const ( - LiquidityTaker Liquidity = "taker" - LiquidityMaker Liquidity = "maker" -) - -type Side string - -const ( - SideBuy Side = "buy" - SideSell Side = "sell" -) - -type OrderType string - -const ( - OrderTypeLimit OrderType = "limit" - OrderTypeMarket OrderType = "market" - - // trigger order types - OrderTypeStopLimit OrderType = "stop" - OrderTypeTrailingStop OrderType = "trailingStop" - OrderTypeTakeProfit OrderType = "takeProfit" -) - -type OrderStatus string - -const ( - OrderStatusNew OrderStatus = "new" - OrderStatusOpen OrderStatus = "open" - OrderStatusClosed OrderStatus = "closed" -) diff --git a/pkg/exchange/ftx/generate_symbol_map.go b/pkg/exchange/ftx/generate_symbol_map.go deleted file mode 100644 index b2c68072ea..0000000000 --- a/pkg/exchange/ftx/generate_symbol_map.go +++ /dev/null @@ -1,65 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import ( - "encoding/json" - "log" - "net/http" - "os" - "strings" - "text/template" -) - -var packageTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT. -package ftx -var symbolMap = map[string]string{ -{{- range $k, $v := . }} - {{ printf "%q" $k }}: {{ printf "%q" $v }}, -{{- end }} -} -`)) - -type Market struct { - Name string `json:"name"` -} - -type ApiResponse struct { - Success bool `json:"success"` - - Result []Market `json:"result"` -} - -func main() { - var data = map[string]string{} - - const url = "https://ftx.com/api/markets" - - resp, err := http.Get(url) - if err != nil { - log.Fatal(err) - return - } - defer resp.Body.Close() - - r := &ApiResponse{} - json.NewDecoder(resp.Body).Decode(r) - - for _, m := range r.Result { - key := strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(m.Name)), "/", "") - data[key] = m.Name - } - - f, err := os.Create("symbols.go") - if err != nil { - log.Fatal(err) - } - - defer f.Close() - - err = packageTemplate.Execute(f, data) - if err != nil { - log.Fatal(err) - } -} diff --git a/pkg/exchange/ftx/orderbook_snapshot.json b/pkg/exchange/ftx/orderbook_snapshot.json deleted file mode 100644 index ca912fa493..0000000000 --- a/pkg/exchange/ftx/orderbook_snapshot.json +++ /dev/null @@ -1,814 +0,0 @@ -{ - "channel": "orderbook", - "market": "BTC/USDT", - "type": "partial", - "data": { - "time": 1614520368.9313016, - "checksum": 2150525410, - "bids": [ - [ - 44555.0, - 3.3968 - ], - [ - 44554.0, - 0.0561 - ], - [ - 44548.0, - 0.1683 - ], - [ - 44542.0, - 0.1762 - ], - [ - 44540.0, - 0.0433 - ], - [ - 44539.0, - 4.1616 - ], - [ - 44534.0, - 0.0234 - ], - [ - 44533.0, - 33.1201 - ], - [ - 44532.0, - 8.2272 - ], - [ - 44531.0, - 0.3364 - ], - [ - 44530.0, - 0.0011 - ], - [ - 44527.0, - 0.0074 - ], - [ - 44526.0, - 0.0117 - ], - [ - 44525.0, - 0.4514 - ], - [ - 44520.0, - 0.001 - ], - [ - 44518.0, - 0.1054 - ], - [ - 44517.0, - 0.0077 - ], - [ - 44512.0, - 0.8512 - ], - [ - 44511.0, - 31.8569 - ], - [ - 44510.0, - 0.001 - ], - [ - 44507.0, - 0.0234 - ], - [ - 44506.0, - 0.382 - ], - [ - 44505.0, - 0.0468 - ], - [ - 44501.0, - 0.0082 - ], - [ - 44500.0, - 0.501 - ], - [ - 44498.0, - 0.001 - ], - [ - 44496.0, - 0.0269 - ], - [ - 44490.0, - 0.001 - ], - [ - 44480.0, - 0.001 - ], - [ - 44479.0, - 0.0306 - ], - [ - 44478.0, - 0.01 - ], - [ - 44477.0, - 0.302 - ], - [ - 44470.0, - 0.001 - ], - [ - 44469.0, - 0.0001 - ], - [ - 44460.0, - 0.001 - ], - [ - 44454.0, - 0.001 - ], - [ - 44450.0, - 0.0019 - ], - [ - 44448.0, - 0.0005 - ], - [ - 44440.0, - 0.001 - ], - [ - 44439.0, - 28.9321 - ], - [ - 44430.0, - 0.001 - ], - [ - 44420.0, - 0.001 - ], - [ - 44416.0, - 0.0001 - ], - [ - 44411.0, - 0.0984 - ], - [ - 44410.0, - 0.001 - ], - [ - 44409.0, - 0.001 - ], - [ - 44408.0, - 0.0004 - ], - [ - 44407.0, - 0.0002 - ], - [ - 44400.0, - 0.001 - ], - [ - 44397.0, - 0.0002 - ], - [ - 44391.0, - 0.0004 - ], - [ - 44390.0, - 0.001 - ], - [ - 44389.0, - 43.3904 - ], - [ - 44380.0, - 0.001 - ], - [ - 44376.0, - 0.0001 - ], - [ - 44375.0, - 0.0001 - ], - [ - 44372.0, - 0.0002 - ], - [ - 44370.0, - 0.0012 - ], - [ - 44365.0, - 0.001 - ], - [ - 44363.0, - 0.0004 - ], - [ - 44360.0, - 0.001 - ], - [ - 44354.0, - 54.0385 - ], - [ - 44350.0, - 0.0028 - ], - [ - 44346.0, - 0.0001 - ], - [ - 44340.0, - 0.0013 - ], - [ - 44338.0, - 0.0002 - ], - [ - 44336.0, - 39.6518 - ], - [ - 44333.0, - 0.0001 - ], - [ - 44330.0, - 0.001 - ], - [ - 44329.0, - 0.5014 - ], - [ - 44326.0, - 0.0002 - ], - [ - 44322.0, - 0.001 - ], - [ - 44321.0, - 0.001 - ], - [ - 44320.0, - 0.001 - ], - [ - 44314.0, - 0.0007 - ], - [ - 44310.0, - 0.001 - ], - [ - 44306.0, - 0.0001 - ], - [ - 44300.0, - 33.2836 - ], - [ - 44292.0, - 0.0035 - ], - [ - 44291.0, - 0.0004 - ], - [ - 44290.0, - 0.001 - ], - [ - 44287.0, - 39.717 - ], - [ - 44285.0, - 0.0439 - ], - [ - 44281.0, - 1.0294 - ], - [ - 44280.0, - 0.001 - ], - [ - 44277.0, - 0.001 - ], - [ - 44275.0, - 0.0165 - ], - [ - 44270.0, - 0.001 - ], - [ - 44268.0, - 48.31 - ], - [ - 44260.0, - 0.0011 - ], - [ - 44254.0, - 0.0003 - ], - [ - 44250.0, - 0.0031 - ], - [ - 44246.0, - 0.0002 - ], - [ - 44244.0, - 0.0001 - ], - [ - 44241.0, - 0.0009 - ], - [ - 44240.0, - 0.001 - ], - [ - 44233.0, - 0.001 - ], - [ - 44230.0, - 0.001 - ], - [ - 44224.0, - 0.0001 - ], - [ - 44222.0, - 0.0002 - ] - ], - "asks": [ - [ - 44574.0, - 0.4591 - ], - [ - 44579.0, - 0.15 - ], - [ - 44582.0, - 2.9122 - ], - [ - 44583.0, - 0.1683 - ], - [ - 44584.0, - 0.5 - ], - [ - 44588.0, - 0.0433 - ], - [ - 44590.0, - 8.6379 - ], - [ - 44593.0, - 0.405 - ], - [ - 44595.0, - 0.5988 - ], - [ - 44596.0, - 0.06 - ], - [ - 44605.0, - 0.6927 - ], - [ - 44606.0, - 0.3365 - ], - [ - 44616.0, - 0.1752 - ], - [ - 44617.0, - 0.0215 - ], - [ - 44620.0, - 0.008 - ], - [ - 44629.0, - 0.0078 - ], - [ - 44630.0, - 0.101 - ], - [ - 44631.0, - 0.246 - ], - [ - 44632.0, - 0.01 - ], - [ - 44635.0, - 0.2997 - ], - [ - 44636.0, - 26.777 - ], - [ - 44639.0, - 0.662 - ], - [ - 44642.0, - 0.0078 - ], - [ - 44650.0, - 0.0009 - ], - [ - 44651.0, - 0.0001 - ], - [ - 44652.0, - 0.0079 - ], - [ - 44653.0, - 0.0003 - ], - [ - 44654.0, - 0.354 - ], - [ - 44661.0, - 0.0306 - ], - [ - 44666.0, - 0.0002 - ], - [ - 44667.0, - 0.0009 - ], - [ - 44668.0, - 0.0234 - ], - [ - 44672.0, - 25.923 - ], - [ - 44673.0, - 0.1 - ], - [ - 44674.0, - 0.001 - ], - [ - 44675.0, - 0.0467 - ], - [ - 44678.0, - 0.1286 - ], - [ - 44680.0, - 0.0467 - ], - [ - 44684.0, - 0.0117 - ], - [ - 44687.0, - 0.0351 - ], - [ - 44689.0, - 0.1052 - ], - [ - 44693.0, - 0.0132 - ], - [ - 44699.0, - 0.0984 - ], - [ - 44700.0, - 0.671 - ], - [ - 44709.0, - 0.0007 - ], - [ - 44713.0, - 45.9031 - ], - [ - 44714.0, - 0.0001 - ], - [ - 44719.0, - 0.001 - ], - [ - 44727.0, - 0.0004 - ], - [ - 44728.0, - 0.0002 - ], - [ - 44735.0, - 0.0003 - ], - [ - 44744.0, - 64.7511 - ], - [ - 44750.0, - 0.0018 - ], - [ - 44763.0, - 0.001 - ], - [ - 44775.0, - 0.0006 - ], - [ - 44781.0, - 0.0001 - ], - [ - 44782.0, - 34.2206 - ], - [ - 44784.0, - 0.0001 - ], - [ - 44790.0, - 0.0002 - ], - [ - 44796.0, - 0.001 - ], - [ - 44799.0, - 0.0002 - ], - [ - 44800.0, - 0.0011 - ], - [ - 44806.0, - 0.0165 - ], - [ - 44807.0, - 0.001 - ], - [ - 44813.0, - 0.0001 - ], - [ - 44814.0, - 0.0003 - ], - [ - 44816.0, - 0.0002 - ], - [ - 44820.0, - 38.3495 - ], - [ - 44822.0, - 0.0026 - ], - [ - 44836.0, - 0.0001 - ], - [ - 44846.0, - 50.1127 - ], - [ - 44850.0, - 0.0018 - ], - [ - 44851.0, - 0.001 - ], - [ - 44859.0, - 0.0003 - ], - [ - 44867.0, - 66.5987 - ], - [ - 44876.0, - 1.0294 - ], - [ - 44885.0, - 0.0005 - ], - [ - 44888.0, - 0.0002 - ], - [ - 44889.0, - 0.0003 - ], - [ - 44895.0, - 0.001 - ], - [ - 44897.0, - 0.0443 - ], - [ - 44900.0, - 40.9965 - ], - [ - 44909.0, - 0.0008 - ], - [ - 44913.0, - 0.0001 - ], - [ - 44926.0, - 45.4838 - ], - [ - 44928.0, - 70.5138 - ], - [ - 44938.0, - 0.0005 - ], - [ - 44939.0, - 0.001 - ], - [ - 44949.0, - 0.0004 - ], - [ - 44950.0, - 0.0019 - ], - [ - 44959.0, - 0.0002 - ], - [ - 44962.0, - 0.0002 - ], - [ - 44979.0, - 0.0002 - ], - [ - 44982.0, - 68.1033 - ], - [ - 44983.0, - 0.001 - ], - [ - 44999.0, - 0.0003 - ], - [ - 45000.0, - 0.0273 - ], - [ - 45002.0, - 0.0002 - ], - [ - 45009.0, - 0.0003 - ], - [ - 45010.0, - 0.0003 - ] - ], - "action": "partial" - } -} diff --git a/pkg/exchange/ftx/orderbook_update.json b/pkg/exchange/ftx/orderbook_update.json deleted file mode 100644 index 51931ed803..0000000000 --- a/pkg/exchange/ftx/orderbook_update.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "channel": "orderbook", - "market": "BTC/USDT", - "type": "update", - "data": { - "time": 1614737706.650016, - "checksum": 3976343467, - "bids": [ - [ - 48763.0, - 0.5001 - ] - ], - "asks": [ - [ - 48826.0, - 0.3385 - ], - [ - 48929.0, - 26.8713 - ] - ], - "action": "update" - } -} \ No newline at end of file diff --git a/pkg/exchange/ftx/rest.go b/pkg/exchange/ftx/rest.go deleted file mode 100644 index 18282551ca..0000000000 --- a/pkg/exchange/ftx/rest.go +++ /dev/null @@ -1,269 +0,0 @@ -package ftx - -import ( - "bytes" - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/pkg/errors" - - "github.com/c9s/bbgo/pkg/util" -) - -type transferRequest struct { - *restRequest -} - -type TransferPayload struct { - Coin string - Size float64 - Source string - Destination string -} - -func (r *restRequest) Transfer(ctx context.Context, p TransferPayload) (transferResponse, error) { - resp, err := r. - Method("POST"). - ReferenceURL("api/subaccounts/transfer"). - Payloads(map[string]interface{}{ - "coin": p.Coin, - "size": p.Size, - "source": p.Source, - "destination": p.Destination, - }). - DoAuthenticatedRequest(ctx) - if err != nil { - return transferResponse{}, err - } - - var t transferResponse - if err := json.Unmarshal(resp.Body, &t); err != nil { - return transferResponse{}, fmt.Errorf("failed to unmarshal transfer response body to json: %w", err) - } - - return t, nil -} - -type restRequest struct { - *walletRequest - *marketRequest - *transferRequest - - key, secret string - // Optional sub-account name - sub string - - c *http.Client - baseURL *url.URL - refURL string - // http method, e.g., GET or POST - m string - - // query string - q map[string]string - - // payload - p map[string]interface{} - - // object id - id string -} - -func newRestRequest(c *http.Client, baseURL *url.URL) *restRequest { - r := &restRequest{ - c: c, - baseURL: baseURL, - q: make(map[string]string), - p: make(map[string]interface{}), - } - - r.marketRequest = &marketRequest{restRequest: r} - r.walletRequest = &walletRequest{restRequest: r} - return r -} - -func (r *restRequest) Auth(key, secret string) *restRequest { - r.key = key - // pragma: allowlist nextline secret - r.secret = secret - return r -} - -func (r *restRequest) SubAccount(subAccount string) *restRequest { - r.sub = subAccount - return r -} - -func (r *restRequest) Method(method string) *restRequest { - r.m = method - return r -} - -func (r *restRequest) ReferenceURL(refURL string) *restRequest { - r.refURL = refURL - return r -} - -func (r *restRequest) buildURL() (*url.URL, error) { - u := r.refURL - if len(r.id) > 0 { - u = u + "/" + r.id - } - refURL, err := url.Parse(u) - if err != nil { - return nil, err - } - - return r.baseURL.ResolveReference(refURL), nil -} - -func (r *restRequest) ID(id string) *restRequest { - r.id = id - return r -} - -func (r *restRequest) Payloads(payloads map[string]interface{}) *restRequest { - for k, v := range payloads { - r.p[k] = v - } - return r -} - -func (r *restRequest) Query(query map[string]string) *restRequest { - for k, v := range query { - r.q[k] = v - } - return r -} - -func (r *restRequest) DoAuthenticatedRequest(ctx context.Context) (*util.Response, error) { - req, err := r.newAuthenticatedRequest(ctx) - if err != nil { - return nil, err - } - - return r.sendRequest(req) -} - -func (r *restRequest) newAuthenticatedRequest(ctx context.Context) (*http.Request, error) { - u, err := r.buildURL() - if err != nil { - return nil, err - } - - var jsonPayload []byte - if len(r.p) > 0 { - var err2 error - jsonPayload, err2 = json.Marshal(r.p) - if err2 != nil { - return nil, fmt.Errorf("can't marshal payload map to json: %w", err2) - } - } - - req, err := http.NewRequestWithContext(ctx, r.m, u.String(), bytes.NewBuffer(jsonPayload)) - if err != nil { - return nil, err - } - - ts := strconv.FormatInt(timestamp(), 10) - p := fmt.Sprintf("%s%s%s", ts, r.m, u.Path) - if len(r.q) > 0 { - rq := u.Query() - for k, v := range r.q { - rq.Add(k, v) - } - req.URL.RawQuery = rq.Encode() - p += "?" + req.URL.RawQuery - } - if len(jsonPayload) > 0 { - p += string(jsonPayload) - } - signature := sign(r.secret, p) - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("FTX-KEY", r.key) - req.Header.Set("FTX-SIGN", signature) - req.Header.Set("FTX-TS", ts) - if r.sub != "" { - req.Header.Set("FTX-SUBACCOUNT", r.sub) - } - - return req, nil -} - -func sign(secret, body string) string { - mac := hmac.New(sha256.New, []byte(secret)) - mac.Write([]byte(body)) - return hex.EncodeToString(mac.Sum(nil)) -} - -func timestamp() int64 { - return time.Now().UnixNano() / int64(time.Millisecond) -} - -func (r *restRequest) sendRequest(req *http.Request) (*util.Response, error) { - resp, err := r.c.Do(req) - if err != nil { - return nil, err - } - - // newResponse reads the response body and return a new Response object - response, err := util.NewResponse(resp) - if err != nil { - return response, err - } - - // Check error, if there is an error, return the ErrorResponse struct type - if response.IsError() { - errorResponse, err := toErrorResponse(response) - if err != nil { - return response, err - } - return response, errorResponse - } - - return response, nil -} - -type ErrorResponse struct { - *util.Response - - IsSuccess bool `json:"success"` - ErrorString string `json:"error,omitempty"` -} - -func (r *ErrorResponse) Error() string { - return fmt.Sprintf("%s %s %d, success: %t, err: %s", - r.Response.Request.Method, - r.Response.Request.URL.String(), - r.Response.StatusCode, - r.IsSuccess, - r.ErrorString, - ) -} - -func toErrorResponse(response *util.Response) (*ErrorResponse, error) { - errorResponse := &ErrorResponse{Response: response} - - if response.IsJSON() { - var err = response.DecodeJSON(errorResponse) - if err != nil { - return nil, errors.Wrapf(err, "failed to decode json for response: %d %s", response.StatusCode, string(response.Body)) - } - - if errorResponse.IsSuccess { - return nil, fmt.Errorf("response.Success should be false") - } - return errorResponse, nil - } - - return errorResponse, fmt.Errorf("unexpected response content type %s", response.Header.Get("content-type")) -} diff --git a/pkg/exchange/ftx/rest_market_request.go b/pkg/exchange/ftx/rest_market_request.go deleted file mode 100644 index aeb41e17a5..0000000000 --- a/pkg/exchange/ftx/rest_market_request.go +++ /dev/null @@ -1,53 +0,0 @@ -package ftx - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "time" - - "github.com/c9s/bbgo/pkg/types" -) - -type marketRequest struct { - *restRequest -} - -/* -supported resolutions: window length in seconds. options: 15, 60, 300, 900, 3600, 14400, 86400 -doc: https://docs.ftx.com/?javascript#get-historical-prices -*/ -func (r *marketRequest) HistoricalPrices(ctx context.Context, market string, interval types.Interval, limit int64, start, end *time.Time) (HistoricalPricesResponse, error) { - q := map[string]string{ - "resolution": strconv.FormatInt(int64(interval.Minutes())*60, 10), - } - - if limit > 0 { - q["limit"] = strconv.FormatInt(limit, 10) - } - - if start != nil { - q["start_time"] = strconv.FormatInt(start.Unix(), 10) - } - - if end != nil { - q["end_time"] = strconv.FormatInt(end.Unix(), 10) - } - - resp, err := r. - Method("GET"). - Query(q). - ReferenceURL(fmt.Sprintf("api/markets/%s/candles", market)). - DoAuthenticatedRequest(ctx) - - if err != nil { - return HistoricalPricesResponse{}, err - } - - var h HistoricalPricesResponse - if err := json.Unmarshal(resp.Body, &h); err != nil { - return HistoricalPricesResponse{}, fmt.Errorf("failed to unmarshal historical prices response body to json: %w", err) - } - return h, nil -} diff --git a/pkg/exchange/ftx/rest_responses.go b/pkg/exchange/ftx/rest_responses.go deleted file mode 100644 index 15da5e606d..0000000000 --- a/pkg/exchange/ftx/rest_responses.go +++ /dev/null @@ -1,391 +0,0 @@ -package ftx - -import ( - "fmt" - "strings" - "time" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -// ex: 2019-03-05T09:56:55.728933+00:00 -const timeLayout = "2006-01-02T15:04:05.999999Z07:00" - -type datetime struct { - time.Time -} - -func parseDatetime(s string) (time.Time, error) { - return time.Parse(timeLayout, s) -} - -// used in unit test -func mustParseDatetime(s string) time.Time { - t, err := parseDatetime(s) - if err != nil { - panic(err) - } - return t -} - -func (d *datetime) UnmarshalJSON(b []byte) error { - // remove double quote from json string - s := strings.Trim(string(b), "\"") - if len(s) == 0 { - d.Time = time.Time{} - return nil - } - t, err := parseDatetime(s) - if err != nil { - return err - } - d.Time = t - return nil -} - -/* -{ - "success": true, - "result": { - "backstopProvider": true, - "collateral": 3568181.02691129, - "freeCollateral": 1786071.456884368, - "initialMarginRequirement": 0.12222384240257728, - "leverage": 10, - "liquidating": false, - "maintenanceMarginRequirement": 0.07177992558058484, - "makerFee": 0.0002, - "marginFraction": 0.5588433331419503, - "openMarginFraction": 0.2447194090423075, - "takerFee": 0.0005, - "totalAccountValue": 3568180.98341129, - "totalPositionSize": 6384939.6992, - "username": "user@domain.com", - "positions": [ - { - "cost": -31.7906, - "entryPrice": 138.22, - "future": "ETH-PERP", - "initialMarginRequirement": 0.1, - "longOrderSize": 1744.55, - "maintenanceMarginRequirement": 0.04, - "netSize": -0.23, - "openSize": 1744.32, - "realizedPnl": 3.39441714, - "shortOrderSize": 1732.09, - "side": "sell", - "size": 0.23, - "unrealizedPnl": 0 - } - ] - } -} -*/ -type accountResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - Result account `json:"result"` -} - -type account struct { - MakerFee fixedpoint.Value `json:"makerFee"` - TakerFee fixedpoint.Value `json:"takerFee"` - TotalAccountValue fixedpoint.Value `json:"totalAccountValue"` -} - -type positionsResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - Result []position `json:"result"` -} - -/* -{ - "cost": -31.7906, - "entryPrice": 138.22, - "estimatedLiquidationPrice": 152.1, - "future": "ETH-PERP", - "initialMarginRequirement": 0.1, - "longOrderSize": 1744.55, - "maintenanceMarginRequirement": 0.04, - "netSize": -0.23, - "openSize": 1744.32, - "realizedPnl": 3.39441714, - "shortOrderSize": 1732.09, - "side": "sell", - "size": 0.23, - "unrealizedPnl": 0, - "collateralUsed": 3.17906 -} -*/ -type position struct { - Cost fixedpoint.Value `json:"cost"` - EntryPrice fixedpoint.Value `json:"entryPrice"` - EstimatedLiquidationPrice fixedpoint.Value `json:"estimatedLiquidationPrice"` - Future string `json:"future"` - InitialMarginRequirement fixedpoint.Value `json:"initialMarginRequirement"` - LongOrderSize fixedpoint.Value `json:"longOrderSize"` - MaintenanceMarginRequirement fixedpoint.Value `json:"maintenanceMarginRequirement"` - NetSize fixedpoint.Value `json:"netSize"` - OpenSize fixedpoint.Value `json:"openSize"` - RealizedPnl fixedpoint.Value `json:"realizedPnl"` - ShortOrderSize fixedpoint.Value `json:"shortOrderSize"` - Side string `json:"Side"` - Size fixedpoint.Value `json:"size"` - UnrealizedPnl fixedpoint.Value `json:"unrealizedPnl"` - CollateralUsed fixedpoint.Value `json:"collateralUsed"` -} - -type balances struct { // nolint:golint,deadcode - Success bool `json:"success"` - - Result []struct { - Coin string `json:"coin"` - Free fixedpoint.Value `json:"free"` - Total fixedpoint.Value `json:"total"` - } `json:"result"` -} - -/* -[ - { - "name": "BTC/USD", - "enabled": true, - "postOnly": false, - "priceIncrement": 1.0, - "sizeIncrement": 0.0001, - "minProvideSize": 0.0001, - "last": 59039.0, - "bid": 59038.0, - "ask": 59040.0, - "price": 59039.0, - "type": "spot", - "baseCurrency": "BTC", - "quoteCurrency": "USD", - "underlying": null, - "restricted": false, - "highLeverageFeeExempt": true, - "change1h": 0.0015777151969599294, - "change24h": 0.05475756601279165, - "changeBod": -0.0035107262814994852, - "quoteVolume24h": 316493675.5463, - "volumeUsd24h": 316493675.5463 - } -] -*/ -type marketsResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - Result []market `json:"result"` -} - -type market struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - PostOnly bool `json:"postOnly"` - PriceIncrement fixedpoint.Value `json:"priceIncrement"` - SizeIncrement fixedpoint.Value `json:"sizeIncrement"` - MinProvideSize fixedpoint.Value `json:"minProvideSize"` - Last fixedpoint.Value `json:"last"` - Bid fixedpoint.Value `json:"bid"` - Ask fixedpoint.Value `json:"ask"` - Price fixedpoint.Value `json:"price"` - Type string `json:"type"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - Underlying string `json:"underlying"` - Restricted bool `json:"restricted"` - HighLeverageFeeExempt bool `json:"highLeverageFeeExempt"` - Change1h fixedpoint.Value `json:"change1h"` - Change24h fixedpoint.Value `json:"change24h"` - ChangeBod fixedpoint.Value `json:"changeBod"` - QuoteVolume24h fixedpoint.Value `json:"quoteVolume24h"` - VolumeUsd24h fixedpoint.Value `json:"volumeUsd24h"` -} - -/* -{ - "success": true, - "result": [ - { - "close": 11055.25, - "high": 11089.0, - "low": 11043.5, - "open": 11059.25, - "startTime": "2019-06-24T17:15:00+00:00", - "volume": 464193.95725 - } - ] -} -*/ -type HistoricalPricesResponse struct { - Success bool `json:"success"` - Result []Candle `json:"result"` -} - -type Candle struct { - Close fixedpoint.Value `json:"close"` - High fixedpoint.Value `json:"high"` - Low fixedpoint.Value `json:"low"` - Open fixedpoint.Value `json:"open"` - StartTime datetime `json:"startTime"` - Volume fixedpoint.Value `json:"volume"` -} - -type ordersHistoryResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - Result []order `json:"result"` - HasMoreData bool `json:"hasMoreData"` -} - -type ordersResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - - Result []order `json:"result"` -} - -type cancelOrderResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - Result string `json:"result"` -} - -type order struct { - CreatedAt datetime `json:"createdAt"` - FilledSize fixedpoint.Value `json:"filledSize"` - // Future field is not defined in the response format table but in the response example. - Future string `json:"future"` - ID int64 `json:"id"` - Market string `json:"market"` - Price fixedpoint.Value `json:"price"` - AvgFillPrice fixedpoint.Value `json:"avgFillPrice"` - RemainingSize fixedpoint.Value `json:"remainingSize"` - Side string `json:"side"` - Size fixedpoint.Value `json:"size"` - Status string `json:"status"` - Type string `json:"type"` - ReduceOnly bool `json:"reduceOnly"` - Ioc bool `json:"ioc"` - PostOnly bool `json:"postOnly"` - ClientId string `json:"clientId"` - Liquidation bool `json:"liquidation"` -} - -type orderResponse struct { - Success bool `json:"success"` - - Result order `json:"result"` -} - -/* -{ - "success": true, - "result": [ - { - "coin": "TUSD", - "confirmations": 64, - "confirmedTime": "2019-03-05T09:56:55.728933+00:00", - "fee": 0, - "id": 1, - "sentTime": "2019-03-05T09:56:55.735929+00:00", - "size": 99.0, - "status": "confirmed", - "time": "2019-03-05T09:56:55.728933+00:00", - "txid": "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1" - } - ] -} -*/ -type depositHistoryResponse struct { - Success bool `json:"success"` - Result []depositHistory `json:"result"` -} - -type depositHistory struct { - ID int64 `json:"id"` - Coin string `json:"coin"` - TxID string `json:"txid"` - Address address `json:"address"` - Confirmations int64 `json:"confirmations"` - ConfirmedTime datetime `json:"confirmedTime"` - Fee fixedpoint.Value `json:"fee"` - SentTime datetime `json:"sentTime"` - Size fixedpoint.Value `json:"size"` - Status string `json:"status"` - Time datetime `json:"time"` - Notes string `json:"notes"` -} - -/** -{ - "address": "test123", - "tag": null, - "method": "ltc", - "coin": null -} -*/ -type address struct { - Address string `json:"address"` - Tag string `json:"tag"` - Method string `json:"method"` - Coin string `json:"coin"` -} - -type fillsResponse struct { - Success bool `json:"success"` - Result []fill `json:"result"` -} - -/* -{ - "id": 123, - "market": "TSLA/USD", - "future": null, - "baseCurrency": "TSLA", - "quoteCurrency": "USD", - "type": "order", - "side": "sell", - "price": 672.5, - "size": 1.0, - "orderId": 456, - "time": "2021-02-23T09:29:08.534000+00:00", - "tradeId": 789, - "feeRate": -5e-6, - "fee": -0.0033625, - "feeCurrency": "USD", - "liquidity": "maker" -} -*/ -type fill struct { - ID int64 `json:"id"` - Market string `json:"market"` - Future string `json:"future"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - Type string `json:"type"` - Side types.SideType `json:"side"` - Price fixedpoint.Value `json:"price"` - Size fixedpoint.Value `json:"size"` - OrderId uint64 `json:"orderId"` - Time datetime `json:"time"` - TradeId uint64 `json:"tradeId"` - FeeRate fixedpoint.Value `json:"feeRate"` - Fee fixedpoint.Value `json:"fee"` - FeeCurrency string `json:"feeCurrency"` - Liquidity string `json:"liquidity"` -} - -type transferResponse struct { - Success bool `json:"success"` - Result transfer `json:"result"` -} - -type transfer struct { - Id uint `json:"id"` - Coin string `json:"coin"` - Size fixedpoint.Value `json:"size"` - Time string `json:"time"` - Notes string `json:"notes"` - Status string `json:"status"` -} - -func (t *transfer) String() string { - return fmt.Sprintf("%+v", *t) -} diff --git a/pkg/exchange/ftx/rest_test.go b/pkg/exchange/ftx/rest_test.go deleted file mode 100644 index ca1adaf3ab..0000000000 --- a/pkg/exchange/ftx/rest_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package ftx - -import ( - "bytes" - "io/ioutil" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/util" -) - -func Test_toErrorResponse(t *testing.T) { - r, err := util.NewResponse(&http.Response{ - Header: http.Header{}, - StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"Success": true}`))), - }) - assert.NoError(t, err) - - _, err = toErrorResponse(r) - assert.EqualError(t, err, "unexpected response content type ") - r.Header.Set("content-type", "text/json") - - _, err = toErrorResponse(r) - assert.EqualError(t, err, "response.Success should be false") - - r.Body = []byte(`{"error":"Not logged in","Success":false}`) - errResp, err := toErrorResponse(r) - assert.NoError(t, err) - assert.False(t, errResp.IsSuccess) - assert.Equal(t, "Not logged in", errResp.ErrorString) -} diff --git a/pkg/exchange/ftx/rest_wallet_request.go b/pkg/exchange/ftx/rest_wallet_request.go deleted file mode 100644 index 039a325530..0000000000 --- a/pkg/exchange/ftx/rest_wallet_request.go +++ /dev/null @@ -1,44 +0,0 @@ -package ftx - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "time" -) - -type walletRequest struct { - *restRequest -} - -func (r *walletRequest) DepositHistory(ctx context.Context, since time.Time, until time.Time, limit int) (depositHistoryResponse, error) { - q := make(map[string]string) - if limit > 0 { - q["limit"] = strconv.Itoa(limit) - } - - if since != (time.Time{}) { - q["start_time"] = strconv.FormatInt(since.Unix(), 10) - } - if until != (time.Time{}) { - q["end_time"] = strconv.FormatInt(until.Unix(), 10) - } - - resp, err := r. - Method("GET"). - ReferenceURL("api/wallet/deposits"). - Query(q). - DoAuthenticatedRequest(ctx) - - if err != nil { - return depositHistoryResponse{}, err - } - - var d depositHistoryResponse - if err := json.Unmarshal(resp.Body, &d); err != nil { - return depositHistoryResponse{}, fmt.Errorf("failed to unmarshal deposit history response body to json: %w", err) - } - - return d, nil -} diff --git a/pkg/exchange/ftx/stream.go b/pkg/exchange/ftx/stream.go deleted file mode 100644 index 6a70a249db..0000000000 --- a/pkg/exchange/ftx/stream.go +++ /dev/null @@ -1,259 +0,0 @@ -package ftx - -import ( - "context" - "fmt" - "time" - - "github.com/gorilla/websocket" - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/net/websocketbase" - "github.com/c9s/bbgo/pkg/types" -) - -const endpoint = "wss://ftx.com/ws/" - -type Stream struct { - *types.StandardStream - - ws *websocketbase.WebsocketClientBase - exchange *Exchange - - key string - secret string - subAccount string - - // subscriptions are only accessed in single goroutine environment, so I don't use mutex to protect them - subscriptions []websocketRequest - klineSubscriptions []klineSubscription -} - -type klineSubscription struct { - symbol string - interval types.Interval -} - -func NewStream(key, secret string, subAccount string, e *Exchange) *Stream { - s := &Stream{ - exchange: e, - key: key, - // pragma: allowlist nextline secret - secret: secret, - subAccount: subAccount, - StandardStream: &types.StandardStream{}, - ws: websocketbase.NewWebsocketClientBase(endpoint, 3*time.Second), - } - - s.ws.OnMessage((&messageHandler{StandardStream: s.StandardStream}).handleMessage) - s.ws.OnConnected(func(conn *websocket.Conn) { - subs := []websocketRequest{newLoginRequest(s.key, s.secret, time.Now(), s.subAccount)} - subs = append(subs, s.subscriptions...) - for _, sub := range subs { - if err := conn.WriteJSON(sub); err != nil { - s.ws.EmitError(fmt.Errorf("failed to send subscription: %+v", sub)) - } - } - - s.EmitConnect() - }) - - return s -} - -func (s *Stream) Connect(ctx context.Context) error { - // If it's not public only, let's do the authentication. - if !s.PublicOnly { - s.subscribePrivateEvents() - } - - if err := s.ws.Connect(ctx); err != nil { - return err - } - s.EmitStart() - - go s.pollKLines(ctx) - go s.pollBalances(ctx) - - go func() { - // https://docs.ftx.com/?javascript#request-process - tk := time.NewTicker(15 * time.Second) - defer tk.Stop() - for { - select { - case <-ctx.Done(): - if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) { - logger.WithError(err).Errorf("context returned error") - } - - case <-tk.C: - if err := s.ws.Conn().WriteJSON(websocketRequest{ - Operation: ping, - }); err != nil { - logger.WithError(err).Warnf("failed to ping, try in next tick") - } - } - } - }() - return nil -} - -func (s *Stream) subscribePrivateEvents() { - s.addSubscription(websocketRequest{ - Operation: subscribe, - Channel: privateOrdersChannel, - }) - s.addSubscription(websocketRequest{ - Operation: subscribe, - Channel: privateTradesChannel, - }) -} - -func (s *Stream) addSubscription(request websocketRequest) { - s.subscriptions = append(s.subscriptions, request) -} - -func (s *Stream) Subscribe(channel types.Channel, symbol string, option types.SubscribeOptions) { - switch channel { - case types.BookChannel: - s.addSubscription(websocketRequest{ - Operation: subscribe, - Channel: orderBookChannel, - Market: toLocalSymbol(TrimUpperString(symbol)), - }) - return - case types.BookTickerChannel: - s.addSubscription(websocketRequest{ - Operation: subscribe, - Channel: bookTickerChannel, - Market: toLocalSymbol(TrimUpperString(symbol)), - }) - return - case types.KLineChannel: - // FTX does not support kline channel, do polling - interval := types.Interval(option.Interval) - ks := klineSubscription{symbol: symbol, interval: interval} - s.klineSubscriptions = append(s.klineSubscriptions, ks) - return - case types.MarketTradeChannel: - s.addSubscription(websocketRequest{ - Operation: subscribe, - Channel: marketTradeChannel, - Market: toLocalSymbol(TrimUpperString(symbol)), - }) - return - default: - panic("only support book/kline/trade channel now") - } -} - -func (s *Stream) pollBalances(ctx context.Context) { - ticker := time.NewTicker(15 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - - case <-ticker.C: - balances, err := s.exchange.QueryAccountBalances(ctx) - if err != nil { - log.WithError(err).Errorf("query balance error") - continue - } - s.EmitBalanceSnapshot(balances) - } - } -} - -func (s *Stream) pollKLines(ctx context.Context) { - lastClosed := make(map[string]map[types.Interval]time.Time, 0) - // get current kline candle - for _, sub := range s.klineSubscriptions { - klines := getLast2KLine(s.exchange, ctx, sub.symbol, sub.interval) - lastClosed[sub.symbol] = make(map[types.Interval]time.Time, 0) - if len(klines) > 0 { - // handle mutiple klines, get the latest one - if lastClosed[sub.symbol][sub.interval].Unix() < klines[0].StartTime.Unix() { - s.EmitKLine(klines[0]) - s.EmitKLineClosed(klines[0]) - lastClosed[sub.symbol][sub.interval] = klines[0].StartTime.Time() - } - - if len(klines) > 1 { - s.EmitKLine(klines[1]) - } - } - } - - // the highest resolution of kline is 1min - ticker := time.NewTicker(time.Second * 30) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) { - logger.WithError(err).Errorf("context returned error") - } - return - case <-ticker.C: - now := time.Now().Truncate(time.Minute) - for _, sub := range s.klineSubscriptions { - subTime := now.Truncate(sub.interval.Duration()) - if now != subTime { - // not in the checking time slot, check next subscription - continue - } - klines := getLast2KLine(s.exchange, ctx, sub.symbol, sub.interval) - - if len(klines) > 0 { - // handle mutiple klines, get the latest one - if lastClosed[sub.symbol][sub.interval].Unix() < klines[0].StartTime.Unix() { - s.EmitKLine(klines[0]) - s.EmitKLineClosed(klines[0]) - lastClosed[sub.symbol][sub.interval] = klines[0].StartTime.Time() - } - - if len(klines) > 1 { - s.EmitKLine(klines[1]) - } - } - } - } - } -} - -func getLast2KLine(e *Exchange, ctx context.Context, symbol string, interval types.Interval) []types.KLine { - // set since to more 30s ago to avoid getting no kline candle - since := time.Now().Add(time.Duration(interval.Minutes()*-3) * time.Minute) - klines, err := e.QueryKLines(ctx, symbol, interval, types.KLineQueryOptions{ - StartTime: &since, - Limit: 2, - }) - if err != nil { - logger.WithError(err).Errorf("failed to get kline data") - return klines - } - - return klines -} - -func getLastClosedKLine(e *Exchange, ctx context.Context, symbol string, interval types.Interval) []types.KLine { - // set since to more 30s ago to avoid getting no kline candle - klines := getLast2KLine(e, ctx, symbol, interval) - if len(klines) == 0 { - return []types.KLine{} - } - return []types.KLine{klines[0]} -} - -func (s *Stream) Close() error { - s.subscriptions = nil - if s.ws != nil { - return s.ws.Conn().Close() - } - return nil -} diff --git a/pkg/exchange/ftx/stream_message_handler.go b/pkg/exchange/ftx/stream_message_handler.go deleted file mode 100644 index 98744622c1..0000000000 --- a/pkg/exchange/ftx/stream_message_handler.go +++ /dev/null @@ -1,169 +0,0 @@ -package ftx - -import ( - "encoding/json" - - "github.com/c9s/bbgo/pkg/types" -) - -type messageHandler struct { - *types.StandardStream -} - -func (h *messageHandler) handleMessage(message []byte) { - var r websocketResponse - if err := json.Unmarshal(message, &r); err != nil { - logger.WithError(err).Errorf("failed to unmarshal resp: %s", string(message)) - return - } - - if r.Type == errRespType { - logger.Errorf("receives err: %+v", r) - return - } - - if r.Type == pongRespType { - return - } - - switch r.Channel { - case orderBookChannel: - h.handleOrderBook(r) - case bookTickerChannel: - h.handleBookTicker(r) - case marketTradeChannel: - h.handleMarketTrade(r) - case privateOrdersChannel: - h.handlePrivateOrders(r) - case privateTradesChannel: - h.handleTrades(r) - default: - logger.Warnf("unsupported message type: %+v", r.Type) - } -} - -// {"type": "subscribed", "channel": "orderbook", "market": "BTC/USDT"} -func (h messageHandler) handleSubscribedMessage(response websocketResponse) { - r, err := response.toSubscribedResponse() - if err != nil { - logger.WithError(err).Errorf("failed to convert the subscribed message") - return - } - logger.Info(r) -} - -func (h *messageHandler) handleOrderBook(response websocketResponse) { - if response.Type == subscribedRespType { - h.handleSubscribedMessage(response) - return - } - r, err := response.toPublicOrderBookResponse() - if err != nil { - logger.WithError(err).Errorf("failed to convert the public orderbook") - return - } - - globalOrderBook, err := toGlobalOrderBook(r) - if err != nil { - logger.WithError(err).Errorf("failed to generate orderbook snapshot") - return - } - - switch r.Type { - case partialRespType: - if err := r.verifyChecksum(); err != nil { - logger.WithError(err).Errorf("invalid orderbook snapshot") - return - } - h.EmitBookSnapshot(globalOrderBook) - case updateRespType: - // emit updates, not the whole orderbook - h.EmitBookUpdate(globalOrderBook) - default: - logger.Errorf("unsupported order book data type %s", r.Type) - return - } -} - -func (h *messageHandler) handleMarketTrade(response websocketResponse) { - if response.Type == subscribedRespType { - h.handleSubscribedMessage(response) - return - } - trades, err := response.toMarketTradeResponse() - if err != nil { - logger.WithError(err).Errorf("failed to generate market trade %v", response) - return - } - for _, trade := range trades { - h.EmitMarketTrade(trade) - } -} - -func (h *messageHandler) handleBookTicker(response websocketResponse) { - if response.Type == subscribedRespType { - h.handleSubscribedMessage(response) - return - } - - r, err := response.toBookTickerResponse() - if err != nil { - logger.WithError(err).Errorf("failed to convert the book ticker") - return - } - - globalBookTicker, err := toGlobalBookTicker(r) - if err != nil { - logger.WithError(err).Errorf("failed to generate book ticker") - return - } - - switch r.Type { - case updateRespType: - // emit updates, not the whole orderbook - h.EmitBookTickerUpdate(globalBookTicker) - default: - logger.Errorf("unsupported book ticker data type %s", r.Type) - return - } -} - -func (h *messageHandler) handlePrivateOrders(response websocketResponse) { - if response.Type == subscribedRespType { - h.handleSubscribedMessage(response) - return - } - - r, err := response.toOrderUpdateResponse() - if err != nil { - logger.WithError(err).Errorf("failed to convert the order update response") - return - } - - globalOrder, err := toGlobalOrderNew(r.Data) - if err != nil { - logger.WithError(err).Errorf("failed to convert order update to global order") - return - } - h.EmitOrderUpdate(globalOrder) -} - -func (h *messageHandler) handleTrades(response websocketResponse) { - if response.Type == subscribedRespType { - h.handleSubscribedMessage(response) - return - } - - r, err := response.toTradeUpdateResponse() - if err != nil { - logger.WithError(err).Errorf("failed to convert the trade update response") - return - } - - t, err := toGlobalTrade(r.Data) - if err != nil { - logger.WithError(err).Errorf("failed to convert trade update to global trade ") - return - } - h.EmitTradeUpdate(t) -} diff --git a/pkg/exchange/ftx/stream_message_handler_test.go b/pkg/exchange/ftx/stream_message_handler_test.go deleted file mode 100644 index 1f640211fd..0000000000 --- a/pkg/exchange/ftx/stream_message_handler_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package ftx - -import ( - "database/sql" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -func Test_messageHandler_handleMessage(t *testing.T) { - t.Run("handle order update", func(t *testing.T) { - input := []byte(` -{ - "channel": "orders", - "type": "update", - "data": { - "id": 36379, - "clientId": null, - "market": "OXY-PERP", - "type": "limit", - "side": "sell", - "price": 2.7185, - "size": 1.0, - "status": "closed", - "filledSize": 1.0, - "remainingSize": 0.0, - "reduceOnly": false, - "liquidation": false, - "avgFillPrice": 2.7185, - "postOnly": false, - "ioc": false, - "createdAt": "2021-03-28T06:12:50.991447+00:00" - } -} -`) - - h := &messageHandler{StandardStream: &types.StandardStream{}} - i := 0 - h.OnOrderUpdate(func(order types.Order) { - i++ - assert.Equal(t, types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: "", - Symbol: "OXY-PERP", - Side: types.SideTypeSell, - Type: types.OrderTypeLimit, - Quantity: fixedpoint.One, - Price: fixedpoint.NewFromFloat(2.7185), - TimeInForce: "GTC", - }, - Exchange: types.ExchangeFTX, - OrderID: 36379, - Status: types.OrderStatusFilled, - ExecutedQuantity: fixedpoint.One, - CreationTime: types.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")), - UpdateTime: types.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")), - }, order) - }) - h.handleMessage(input) - assert.Equal(t, 1, i) - }) - - t.Run("handle trade update", func(t *testing.T) { - input := []byte(` -{ - "channel": "fills", - "type": "update", - "data": { - "id": 23427, - "market": "OXY-PERP", - "future": "OXY-PERP", - "baseCurrency": null, - "quoteCurrency": null, - "type": "order", - "side": "buy", - "price": 2.723, - "size": 1.0, - "orderId": 323789, - "time": "2021-03-28T06:12:34.702926+00:00", - "tradeId": 6276431, - "feeRate": 0.00056525, - "fee": 0.00153917575, - "feeCurrency": "USD", - "liquidity": "taker" - } -} -`) - h := &messageHandler{StandardStream: &types.StandardStream{}} - i := 0 - h.OnTradeUpdate(func(trade types.Trade) { - i++ - assert.Equal(t, types.Trade{ - ID: uint64(6276431), - OrderID: uint64(323789), - Exchange: types.ExchangeFTX, - Price: fixedpoint.NewFromFloat(2.723), - Quantity: fixedpoint.One, - QuoteQuantity: fixedpoint.NewFromFloat(2.723 * 1.0), - Symbol: "OXY-PERP", - Side: types.SideTypeBuy, - IsBuyer: true, - IsMaker: false, - Time: types.Time(mustParseDatetime("2021-03-28T06:12:34.702926+00:00")), - Fee: fixedpoint.NewFromFloat(0.00153917575), - FeeCurrency: "USD", - IsMargin: false, - IsIsolated: false, - IsFutures: true, - StrategyID: sql.NullString{}, - PnL: sql.NullFloat64{}, - }, trade) - }) - h.handleMessage(input) - assert.Equal(t, 1, i) - }) -} diff --git a/pkg/exchange/ftx/symbols.go b/pkg/exchange/ftx/symbols.go deleted file mode 100644 index 33cb022965..0000000000 --- a/pkg/exchange/ftx/symbols.go +++ /dev/null @@ -1,819 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -package ftx - -var symbolMap = map[string]string{ - "1INCH-0325": "1INCH-0325", - "1INCH-PERP": "1INCH-PERP", - "1INCHUSD": "1INCH/USD", - "AAPL-0325": "AAPL-0325", - "AAPLUSD": "AAPL/USD", - "AAVE-0325": "AAVE-0325", - "AAVE-PERP": "AAVE-PERP", - "AAVEUSD": "AAVE/USD", - "AAVEUSDT": "AAVE/USDT", - "ABNB-0325": "ABNB-0325", - "ABNBUSD": "ABNB/USD", - "ACB-0325": "ACB-0325", - "ACBUSD": "ACB/USD", - "ADA-0325": "ADA-0325", - "ADA-PERP": "ADA-PERP", - "ADABEARUSD": "ADABEAR/USD", - "ADABULLUSD": "ADABULL/USD", - "ADAHALFUSD": "ADAHALF/USD", - "ADAHEDGEUSD": "ADAHEDGE/USD", - "AGLD-PERP": "AGLD-PERP", - "AGLDUSD": "AGLD/USD", - "AKROUSD": "AKRO/USD", - "AKROUSDT": "AKRO/USDT", - "ALCX-PERP": "ALCX-PERP", - "ALCXUSD": "ALCX/USD", - "ALEPHUSD": "ALEPH/USD", - "ALGO-0325": "ALGO-0325", - "ALGO-PERP": "ALGO-PERP", - "ALGOBEARUSD": "ALGOBEAR/USD", - "ALGOBULLUSD": "ALGOBULL/USD", - "ALGOHALFUSD": "ALGOHALF/USD", - "ALGOHEDGEUSD": "ALGOHEDGE/USD", - "ALICE-PERP": "ALICE-PERP", - "ALICEUSD": "ALICE/USD", - "ALPHA-PERP": "ALPHA-PERP", - "ALPHAUSD": "ALPHA/USD", - "ALT-0325": "ALT-0325", - "ALT-PERP": "ALT-PERP", - "ALTBEARUSD": "ALTBEAR/USD", - "ALTBULLUSD": "ALTBULL/USD", - "ALTHALFUSD": "ALTHALF/USD", - "ALTHEDGEUSD": "ALTHEDGE/USD", - "AMC-0325": "AMC-0325", - "AMCUSD": "AMC/USD", - "AMD-0325": "AMD-0325", - "AMDUSD": "AMD/USD", - "AMPL-PERP": "AMPL-PERP", - "AMPLUSD": "AMPL/USD", - "AMPLUSDT": "AMPL/USDT", - "AMZN-0325": "AMZN-0325", - "AMZNUSD": "AMZN/USD", - "APHAUSD": "APHA/USD", - "AR-PERP": "AR-PERP", - "ARKK-0325": "ARKK-0325", - "ARKKUSD": "ARKK/USD", - "ASD-PERP": "ASD-PERP", - "ASDBEARUSD": "ASDBEAR/USD", - "ASDBEARUSDT": "ASDBEAR/USDT", - "ASDBULLUSD": "ASDBULL/USD", - "ASDBULLUSDT": "ASDBULL/USDT", - "ASDHALFUSD": "ASDHALF/USD", - "ASDHEDGEUSD": "ASDHEDGE/USD", - "ASDUSD": "ASD/USD", - "ATLAS-PERP": "ATLAS-PERP", - "ATLASUSD": "ATLAS/USD", - "ATOM-0325": "ATOM-0325", - "ATOM-PERP": "ATOM-PERP", - "ATOMBEARUSD": "ATOMBEAR/USD", - "ATOMBULLUSD": "ATOMBULL/USD", - "ATOMHALFUSD": "ATOMHALF/USD", - "ATOMHEDGEUSD": "ATOMHEDGE/USD", - "ATOMUSD": "ATOM/USD", - "ATOMUSDT": "ATOM/USDT", - "AUDIO-PERP": "AUDIO-PERP", - "AUDIOUSD": "AUDIO/USD", - "AUDIOUSDT": "AUDIO/USDT", - "AURYUSD": "AURY/USD", - "AVAX-0325": "AVAX-0325", - "AVAX-PERP": "AVAX-PERP", - "AVAXBTC": "AVAX/BTC", - "AVAXUSD": "AVAX/USD", - "AVAXUSDT": "AVAX/USDT", - "AXS-PERP": "AXS-PERP", - "AXSUSD": "AXS/USD", - "BABA-0325": "BABA-0325", - "BABAUSD": "BABA/USD", - "BADGER-PERP": "BADGER-PERP", - "BADGERUSD": "BADGER/USD", - "BAL-0325": "BAL-0325", - "BAL-PERP": "BAL-PERP", - "BALBEARUSD": "BALBEAR/USD", - "BALBEARUSDT": "BALBEAR/USDT", - "BALBULLUSD": "BALBULL/USD", - "BALBULLUSDT": "BALBULL/USDT", - "BALHALFUSD": "BALHALF/USD", - "BALHEDGEUSD": "BALHEDGE/USD", - "BALUSD": "BAL/USD", - "BALUSDT": "BAL/USDT", - "BAND-PERP": "BAND-PERP", - "BANDUSD": "BAND/USD", - "BAO-PERP": "BAO-PERP", - "BAOUSD": "BAO/USD", - "BARUSD": "BAR/USD", - "BAT-PERP": "BAT-PERP", - "BATUSD": "BAT/USD", - "BB-0325": "BB-0325", - "BBUSD": "BB/USD", - "BCH-0325": "BCH-0325", - "BCH-PERP": "BCH-PERP", - "BCHBEARUSD": "BCHBEAR/USD", - "BCHBEARUSDT": "BCHBEAR/USDT", - "BCHBTC": "BCH/BTC", - "BCHBULLUSD": "BCHBULL/USD", - "BCHBULLUSDT": "BCHBULL/USDT", - "BCHHALFUSD": "BCHHALF/USD", - "BCHHEDGEUSD": "BCHHEDGE/USD", - "BCHUSD": "BCH/USD", - "BCHUSDT": "BCH/USDT", - "BEARSHITUSD": "BEARSHIT/USD", - "BEARUSD": "BEAR/USD", - "BEARUSDT": "BEAR/USDT", - "BICOUSD": "BICO/USD", - "BILI-0325": "BILI-0325", - "BILIUSD": "BILI/USD", - "BIT-PERP": "BIT-PERP", - "BITO-0325": "BITO-0325", - "BITOUSD": "BITO/USD", - "BITUSD": "BIT/USD", - "BITW-0325": "BITW-0325", - "BITWUSD": "BITW/USD", - "BLTUSD": "BLT/USD", - "BNB-0325": "BNB-0325", - "BNB-PERP": "BNB-PERP", - "BNBBEARUSD": "BNBBEAR/USD", - "BNBBEARUSDT": "BNBBEAR/USDT", - "BNBBTC": "BNB/BTC", - "BNBBULLUSD": "BNBBULL/USD", - "BNBBULLUSDT": "BNBBULL/USDT", - "BNBHALFUSD": "BNBHALF/USD", - "BNBHEDGEUSD": "BNBHEDGE/USD", - "BNBUSD": "BNB/USD", - "BNBUSDT": "BNB/USDT", - "BNT-PERP": "BNT-PERP", - "BNTUSD": "BNT/USD", - "BNTX-0325": "BNTX-0325", - "BNTXUSD": "BNTX/USD", - "BOBA-PERP": "BOBA-PERP", - "BOBAUSD": "BOBA/USD", - "BOLSONARO2022": "BOLSONARO2022", - "BRZ-PERP": "BRZ-PERP", - "BRZUSD": "BRZ/USD", - "BRZUSDT": "BRZ/USDT", - "BSV-0325": "BSV-0325", - "BSV-PERP": "BSV-PERP", - "BSVBEARUSD": "BSVBEAR/USD", - "BSVBEARUSDT": "BSVBEAR/USDT", - "BSVBULLUSD": "BSVBULL/USD", - "BSVBULLUSDT": "BSVBULL/USDT", - "BSVHALFUSD": "BSVHALF/USD", - "BSVHEDGEUSD": "BSVHEDGE/USD", - "BTC-0325": "BTC-0325", - "BTC-0624": "BTC-0624", - "BTC-MOVE-0303": "BTC-MOVE-0303", - "BTC-MOVE-0304": "BTC-MOVE-0304", - "BTC-MOVE-2022Q1": "BTC-MOVE-2022Q1", - "BTC-MOVE-2022Q2": "BTC-MOVE-2022Q2", - "BTC-MOVE-2022Q3": "BTC-MOVE-2022Q3", - "BTC-MOVE-WK-0304": "BTC-MOVE-WK-0304", - "BTC-MOVE-WK-0311": "BTC-MOVE-WK-0311", - "BTC-MOVE-WK-0318": "BTC-MOVE-WK-0318", - "BTC-MOVE-WK-0325": "BTC-MOVE-WK-0325", - "BTC-PERP": "BTC-PERP", - "BTCBRZ": "BTC/BRZ", - "BTCEUR": "BTC/EUR", - "BTCTRYB": "BTC/TRYB", - "BTCUSD": "BTC/USD", - "BTCUSDT": "BTC/USDT", - "BTT-PERP": "BTT-PERP", - "BTTUSD": "BTT/USD", - "BULLSHITUSD": "BULLSHIT/USD", - "BULLUSD": "BULL/USD", - "BULLUSDT": "BULL/USDT", - "BVOLBTC": "BVOL/BTC", - "BVOLUSD": "BVOL/USD", - "BVOLUSDT": "BVOL/USDT", - "BYND-0325": "BYND-0325", - "BYNDUSD": "BYND/USD", - "C98-PERP": "C98-PERP", - "C98USD": "C98/USD", - "CADUSD": "CAD/USD", - "CAKE-PERP": "CAKE-PERP", - "CEL-0325": "CEL-0325", - "CEL-PERP": "CEL-PERP", - "CELBTC": "CEL/BTC", - "CELO-PERP": "CELO-PERP", - "CELUSD": "CEL/USD", - "CGC-0325": "CGC-0325", - "CGCUSD": "CGC/USD", - "CHR-PERP": "CHR-PERP", - "CHRUSD": "CHR/USD", - "CHZ-0325": "CHZ-0325", - "CHZ-PERP": "CHZ-PERP", - "CHZUSD": "CHZ/USD", - "CHZUSDT": "CHZ/USDT", - "CITYUSD": "CITY/USD", - "CLV-PERP": "CLV-PERP", - "CLVUSD": "CLV/USD", - "COINUSD": "COIN/USD", - "COMP-0325": "COMP-0325", - "COMP-PERP": "COMP-PERP", - "COMPBEARUSD": "COMPBEAR/USD", - "COMPBEARUSDT": "COMPBEAR/USDT", - "COMPBULLUSD": "COMPBULL/USD", - "COMPBULLUSDT": "COMPBULL/USDT", - "COMPHALFUSD": "COMPHALF/USD", - "COMPHEDGEUSD": "COMPHEDGE/USD", - "COMPUSD": "COMP/USD", - "COMPUSDT": "COMP/USDT", - "CONV-PERP": "CONV-PERP", - "CONVUSD": "CONV/USD", - "COPEUSD": "COPE/USD", - "CQTUSD": "CQT/USD", - "CREAM-PERP": "CREAM-PERP", - "CREAMUSD": "CREAM/USD", - "CREAMUSDT": "CREAM/USDT", - "CRO-PERP": "CRO-PERP", - "CRON-0325": "CRON-0325", - "CRONUSD": "CRON/USD", - "CROUSD": "CRO/USD", - "CRV-PERP": "CRV-PERP", - "CRVUSD": "CRV/USD", - "CUSDT-PERP": "CUSDT-PERP", - "CUSDTBEARUSD": "CUSDTBEAR/USD", - "CUSDTBEARUSDT": "CUSDTBEAR/USDT", - "CUSDTBULLUSD": "CUSDTBULL/USD", - "CUSDTBULLUSDT": "CUSDTBULL/USDT", - "CUSDTHALFUSD": "CUSDTHALF/USD", - "CUSDTHEDGEUSD": "CUSDTHEDGE/USD", - "CUSDTUSD": "CUSDT/USD", - "CUSDTUSDT": "CUSDT/USDT", - "CVC-PERP": "CVC-PERP", - "CVCUSD": "CVC/USD", - "DAIUSD": "DAI/USD", - "DAIUSDT": "DAI/USDT", - "DASH-PERP": "DASH-PERP", - "DAWN-PERP": "DAWN-PERP", - "DAWNUSD": "DAWN/USD", - "DEFI-0325": "DEFI-0325", - "DEFI-PERP": "DEFI-PERP", - "DEFIBEARUSD": "DEFIBEAR/USD", - "DEFIBEARUSDT": "DEFIBEAR/USDT", - "DEFIBULLUSD": "DEFIBULL/USD", - "DEFIBULLUSDT": "DEFIBULL/USDT", - "DEFIHALFUSD": "DEFIHALF/USD", - "DEFIHEDGEUSD": "DEFIHEDGE/USD", - "DENT-PERP": "DENT-PERP", - "DENTUSD": "DENT/USD", - "DFLUSD": "DFL/USD", - "DKNG-0325": "DKNG-0325", - "DKNGUSD": "DKNG/USD", - "DMGUSD": "DMG/USD", - "DMGUSDT": "DMG/USDT", - "DODO-PERP": "DODO-PERP", - "DODOUSD": "DODO/USD", - "DOGE-0325": "DOGE-0325", - "DOGE-PERP": "DOGE-PERP", - "DOGEBEAR2021USD": "DOGEBEAR2021/USD", - "DOGEBTC": "DOGE/BTC", - "DOGEBULLUSD": "DOGEBULL/USD", - "DOGEHALFUSD": "DOGEHALF/USD", - "DOGEHEDGEUSD": "DOGEHEDGE/USD", - "DOGEUSD": "DOGE/USD", - "DOGEUSDT": "DOGE/USDT", - "DOT-0325": "DOT-0325", - "DOT-PERP": "DOT-PERP", - "DOTBTC": "DOT/BTC", - "DOTUSD": "DOT/USD", - "DOTUSDT": "DOT/USDT", - "DRGN-0325": "DRGN-0325", - "DRGN-PERP": "DRGN-PERP", - "DRGNBEARUSD": "DRGNBEAR/USD", - "DRGNBULLUSD": "DRGNBULL/USD", - "DRGNHALFUSD": "DRGNHALF/USD", - "DRGNHEDGEUSD": "DRGNHEDGE/USD", - "DYDX-PERP": "DYDX-PERP", - "DYDXUSD": "DYDX/USD", - "EDEN-0325": "EDEN-0325", - "EDEN-PERP": "EDEN-PERP", - "EDENUSD": "EDEN/USD", - "EGLD-PERP": "EGLD-PERP", - "EMBUSD": "EMB/USD", - "ENJ-PERP": "ENJ-PERP", - "ENJUSD": "ENJ/USD", - "ENS-PERP": "ENS-PERP", - "ENSUSD": "ENS/USD", - "EOS-0325": "EOS-0325", - "EOS-PERP": "EOS-PERP", - "EOSBEARUSD": "EOSBEAR/USD", - "EOSBEARUSDT": "EOSBEAR/USDT", - "EOSBULLUSD": "EOSBULL/USD", - "EOSBULLUSDT": "EOSBULL/USDT", - "EOSHALFUSD": "EOSHALF/USD", - "EOSHEDGEUSD": "EOSHEDGE/USD", - "ETC-PERP": "ETC-PERP", - "ETCBEARUSD": "ETCBEAR/USD", - "ETCBULLUSD": "ETCBULL/USD", - "ETCHALFUSD": "ETCHALF/USD", - "ETCHEDGEUSD": "ETCHEDGE/USD", - "ETH-0325": "ETH-0325", - "ETH-0624": "ETH-0624", - "ETH-PERP": "ETH-PERP", - "ETHBEARUSD": "ETHBEAR/USD", - "ETHBEARUSDT": "ETHBEAR/USDT", - "ETHBRZ": "ETH/BRZ", - "ETHBTC": "ETH/BTC", - "ETHBULLUSD": "ETHBULL/USD", - "ETHBULLUSDT": "ETHBULL/USDT", - "ETHE-0325": "ETHE-0325", - "ETHEUR": "ETH/EUR", - "ETHEUSD": "ETHE/USD", - "ETHHALFUSD": "ETHHALF/USD", - "ETHHEDGEUSD": "ETHHEDGE/USD", - "ETHUSD": "ETH/USD", - "ETHUSDT": "ETH/USDT", - "EURTEUR": "EURT/EUR", - "EURTUSD": "EURT/USD", - "EURTUSDT": "EURT/USDT", - "EURUSD": "EUR/USD", - "EXCH-0325": "EXCH-0325", - "EXCH-PERP": "EXCH-PERP", - "EXCHBEARUSD": "EXCHBEAR/USD", - "EXCHBULLUSD": "EXCHBULL/USD", - "EXCHHALFUSD": "EXCHHALF/USD", - "EXCHHEDGEUSD": "EXCHHEDGE/USD", - "FB-0325": "FB-0325", - "FBUSD": "FB/USD", - "FIDA-PERP": "FIDA-PERP", - "FIDAUSD": "FIDA/USD", - "FIDAUSDT": "FIDA/USDT", - "FIL-0325": "FIL-0325", - "FIL-PERP": "FIL-PERP", - "FLM-PERP": "FLM-PERP", - "FLOW-PERP": "FLOW-PERP", - "FRONTUSD": "FRONT/USD", - "FRONTUSDT": "FRONT/USDT", - "FTM-PERP": "FTM-PERP", - "FTMUSD": "FTM/USD", - "FTT-PERP": "FTT-PERP", - "FTTBTC": "FTT/BTC", - "FTTUSD": "FTT/USD", - "FTTUSDT": "FTT/USDT", - "GALA-PERP": "GALA-PERP", - "GALAUSD": "GALA/USD", - "GALUSD": "GAL/USD", - "GARIUSD": "GARI/USD", - "GBPUSD": "GBP/USD", - "GBTC-0325": "GBTC-0325", - "GBTCUSD": "GBTC/USD", - "GDX-0325": "GDX-0325", - "GDXJ-0325": "GDXJ-0325", - "GDXJUSD": "GDXJ/USD", - "GDXUSD": "GDX/USD", - "GENEUSD": "GENE/USD", - "GLD-0325": "GLD-0325", - "GLDUSD": "GLD/USD", - "GLXYUSD": "GLXY/USD", - "GME-0325": "GME-0325", - "GMEUSD": "GME/USD", - "GODSUSD": "GODS/USD", - "GOGUSD": "GOG/USD", - "GOOGL-0325": "GOOGL-0325", - "GOOGLUSD": "GOOGL/USD", - "GRT-0325": "GRT-0325", - "GRT-PERP": "GRT-PERP", - "GRTBEARUSD": "GRTBEAR/USD", - "GRTBULLUSD": "GRTBULL/USD", - "GRTUSD": "GRT/USD", - "GTUSD": "GT/USD", - "HALFSHITUSD": "HALFSHIT/USD", - "HALFUSD": "HALF/USD", - "HBAR-PERP": "HBAR-PERP", - "HEDGESHITUSD": "HEDGESHIT/USD", - "HEDGEUSD": "HEDGE/USD", - "HGETUSD": "HGET/USD", - "HGETUSDT": "HGET/USDT", - "HMTUSD": "HMT/USD", - "HNT-PERP": "HNT-PERP", - "HNTUSD": "HNT/USD", - "HNTUSDT": "HNT/USDT", - "HOLY-PERP": "HOLY-PERP", - "HOLYUSD": "HOLY/USD", - "HOODUSD": "HOOD/USD", - "HOT-PERP": "HOT-PERP", - "HT-PERP": "HT-PERP", - "HTBEARUSD": "HTBEAR/USD", - "HTBULLUSD": "HTBULL/USD", - "HTHALFUSD": "HTHALF/USD", - "HTHEDGEUSD": "HTHEDGE/USD", - "HTUSD": "HT/USD", - "HUM-PERP": "HUM-PERP", - "HUMUSD": "HUM/USD", - "HXROUSD": "HXRO/USD", - "HXROUSDT": "HXRO/USDT", - "IBVOLBTC": "IBVOL/BTC", - "IBVOLUSD": "IBVOL/USD", - "IBVOLUSDT": "IBVOL/USDT", - "ICP-PERP": "ICP-PERP", - "ICX-PERP": "ICX-PERP", - "IMX-PERP": "IMX-PERP", - "IMXUSD": "IMX/USD", - "INTERUSD": "INTER/USD", - "IOTA-PERP": "IOTA-PERP", - "JETUSD": "JET/USD", - "JOEUSD": "JOE/USD", - "JSTUSD": "JST/USD", - "KAVA-PERP": "KAVA-PERP", - "KBTT-PERP": "KBTT-PERP", - "KBTTUSD": "KBTT/USD", - "KIN-PERP": "KIN-PERP", - "KINUSD": "KIN/USD", - "KNC-PERP": "KNC-PERP", - "KNCBEARUSD": "KNCBEAR/USD", - "KNCBEARUSDT": "KNCBEAR/USDT", - "KNCBULLUSD": "KNCBULL/USD", - "KNCBULLUSDT": "KNCBULL/USDT", - "KNCHALFUSD": "KNCHALF/USD", - "KNCHEDGEUSD": "KNCHEDGE/USD", - "KNCUSD": "KNC/USD", - "KNCUSDT": "KNC/USDT", - "KSHIB-PERP": "KSHIB-PERP", - "KSHIBUSD": "KSHIB/USD", - "KSM-PERP": "KSM-PERP", - "KSOS-PERP": "KSOS-PERP", - "KSOSUSD": "KSOS/USD", - "LEO-PERP": "LEO-PERP", - "LEOBEARUSD": "LEOBEAR/USD", - "LEOBULLUSD": "LEOBULL/USD", - "LEOHALFUSD": "LEOHALF/USD", - "LEOHEDGEUSD": "LEOHEDGE/USD", - "LEOUSD": "LEO/USD", - "LINA-PERP": "LINA-PERP", - "LINAUSD": "LINA/USD", - "LINK-0325": "LINK-0325", - "LINK-PERP": "LINK-PERP", - "LINKBEARUSD": "LINKBEAR/USD", - "LINKBEARUSDT": "LINKBEAR/USDT", - "LINKBTC": "LINK/BTC", - "LINKBULLUSD": "LINKBULL/USD", - "LINKBULLUSDT": "LINKBULL/USDT", - "LINKHALFUSD": "LINKHALF/USD", - "LINKHEDGEUSD": "LINKHEDGE/USD", - "LINKUSD": "LINK/USD", - "LINKUSDT": "LINK/USDT", - "LOOKS-PERP": "LOOKS-PERP", - "LOOKSUSD": "LOOKS/USD", - "LRC-PERP": "LRC-PERP", - "LRCUSD": "LRC/USD", - "LTC-0325": "LTC-0325", - "LTC-PERP": "LTC-PERP", - "LTCBEARUSD": "LTCBEAR/USD", - "LTCBEARUSDT": "LTCBEAR/USDT", - "LTCBTC": "LTC/BTC", - "LTCBULLUSD": "LTCBULL/USD", - "LTCBULLUSDT": "LTCBULL/USDT", - "LTCHALFUSD": "LTCHALF/USD", - "LTCHEDGEUSD": "LTCHEDGE/USD", - "LTCUSD": "LTC/USD", - "LTCUSDT": "LTC/USDT", - "LUAUSD": "LUA/USD", - "LUAUSDT": "LUA/USDT", - "LUNA-PERP": "LUNA-PERP", - "LUNAUSD": "LUNA/USD", - "LUNAUSDT": "LUNA/USDT", - "MANA-PERP": "MANA-PERP", - "MANAUSD": "MANA/USD", - "MAPS-PERP": "MAPS-PERP", - "MAPSUSD": "MAPS/USD", - "MAPSUSDT": "MAPS/USDT", - "MATHUSD": "MATH/USD", - "MATHUSDT": "MATH/USDT", - "MATIC-PERP": "MATIC-PERP", - "MATICBEAR2021USD": "MATICBEAR2021/USD", - "MATICBTC": "MATIC/BTC", - "MATICBULLUSD": "MATICBULL/USD", - "MATICHALFUSD": "MATICHALF/USD", - "MATICHEDGEUSD": "MATICHEDGE/USD", - "MATICUSD": "MATIC/USD", - "MBSUSD": "MBS/USD", - "MCB-PERP": "MCB-PERP", - "MCBUSD": "MCB/USD", - "MEDIA-PERP": "MEDIA-PERP", - "MEDIAUSD": "MEDIA/USD", - "MER-PERP": "MER-PERP", - "MERUSD": "MER/USD", - "MID-0325": "MID-0325", - "MID-PERP": "MID-PERP", - "MIDBEARUSD": "MIDBEAR/USD", - "MIDBULLUSD": "MIDBULL/USD", - "MIDHALFUSD": "MIDHALF/USD", - "MIDHEDGEUSD": "MIDHEDGE/USD", - "MINA-PERP": "MINA-PERP", - "MKR-PERP": "MKR-PERP", - "MKRBEARUSD": "MKRBEAR/USD", - "MKRBULLUSD": "MKRBULL/USD", - "MKRUSD": "MKR/USD", - "MKRUSDT": "MKR/USDT", - "MNGO-PERP": "MNGO-PERP", - "MNGOUSD": "MNGO/USD", - "MOBUSD": "MOB/USD", - "MOBUSDT": "MOB/USDT", - "MRNA-0325": "MRNA-0325", - "MRNAUSD": "MRNA/USD", - "MSOLUSD": "MSOL/USD", - "MSTR-0325": "MSTR-0325", - "MSTRUSD": "MSTR/USD", - "MTA-PERP": "MTA-PERP", - "MTAUSD": "MTA/USD", - "MTAUSDT": "MTA/USDT", - "MTL-PERP": "MTL-PERP", - "MTLUSD": "MTL/USD", - "MVDA10-PERP": "MVDA10-PERP", - "MVDA25-PERP": "MVDA25-PERP", - "NEAR-PERP": "NEAR-PERP", - "NEO-PERP": "NEO-PERP", - "NEXOUSD": "NEXO/USD", - "NFLX-0325": "NFLX-0325", - "NFLXUSD": "NFLX/USD", - "NIO-0325": "NIO-0325", - "NIOUSD": "NIO/USD", - "NOK-0325": "NOK-0325", - "NOKUSD": "NOK/USD", - "NVDA-0325": "NVDA-0325", - "NVDAUSD": "NVDA/USD", - "OKB-0325": "OKB-0325", - "OKB-PERP": "OKB-PERP", - "OKBBEARUSD": "OKBBEAR/USD", - "OKBBULLUSD": "OKBBULL/USD", - "OKBHALFUSD": "OKBHALF/USD", - "OKBHEDGEUSD": "OKBHEDGE/USD", - "OKBUSD": "OKB/USD", - "OMG-0325": "OMG-0325", - "OMG-PERP": "OMG-PERP", - "OMGUSD": "OMG/USD", - "ONE-PERP": "ONE-PERP", - "ONT-PERP": "ONT-PERP", - "ORBS-PERP": "ORBS-PERP", - "ORBSUSD": "ORBS/USD", - "OXY-PERP": "OXY-PERP", - "OXYUSD": "OXY/USD", - "OXYUSDT": "OXY/USDT", - "PAXG-PERP": "PAXG-PERP", - "PAXGBEARUSD": "PAXGBEAR/USD", - "PAXGBULLUSD": "PAXGBULL/USD", - "PAXGHALFUSD": "PAXGHALF/USD", - "PAXGHEDGEUSD": "PAXGHEDGE/USD", - "PAXGUSD": "PAXG/USD", - "PAXGUSDT": "PAXG/USDT", - "PENN-0325": "PENN-0325", - "PENNUSD": "PENN/USD", - "PEOPLE-PERP": "PEOPLE-PERP", - "PEOPLEUSD": "PEOPLE/USD", - "PERP-PERP": "PERP-PERP", - "PERPUSD": "PERP/USD", - "PFE-0325": "PFE-0325", - "PFEUSD": "PFE/USD", - "POLIS-PERP": "POLIS-PERP", - "POLISUSD": "POLIS/USD", - "PORTUSD": "PORT/USD", - "PRISMUSD": "PRISM/USD", - "PRIV-0325": "PRIV-0325", - "PRIV-PERP": "PRIV-PERP", - "PRIVBEARUSD": "PRIVBEAR/USD", - "PRIVBULLUSD": "PRIVBULL/USD", - "PRIVHALFUSD": "PRIVHALF/USD", - "PRIVHEDGEUSD": "PRIVHEDGE/USD", - "PROM-PERP": "PROM-PERP", - "PROMUSD": "PROM/USD", - "PSGUSD": "PSG/USD", - "PSYUSD": "PSY/USD", - "PTUUSD": "PTU/USD", - "PUNDIX-PERP": "PUNDIX-PERP", - "PUNDIXUSD": "PUNDIX/USD", - "PYPL-0325": "PYPL-0325", - "PYPLUSD": "PYPL/USD", - "QIUSD": "QI/USD", - "QTUM-PERP": "QTUM-PERP", - "RAMP-PERP": "RAMP-PERP", - "RAMPUSD": "RAMP/USD", - "RAY-PERP": "RAY-PERP", - "RAYUSD": "RAY/USD", - "REALUSD": "REAL/USD", - "REEF-0325": "REEF-0325", - "REEF-PERP": "REEF-PERP", - "REEFUSD": "REEF/USD", - "REN-PERP": "REN-PERP", - "RENUSD": "REN/USD", - "RNDR-PERP": "RNDR-PERP", - "RNDRUSD": "RNDR/USD", - "RON-PERP": "RON-PERP", - "ROOK-PERP": "ROOK-PERP", - "ROOKUSD": "ROOK/USD", - "ROOKUSDT": "ROOK/USDT", - "ROSE-PERP": "ROSE-PERP", - "RSR-PERP": "RSR-PERP", - "RSRUSD": "RSR/USD", - "RUNE-PERP": "RUNE-PERP", - "RUNEUSD": "RUNE/USD", - "RUNEUSDT": "RUNE/USDT", - "SAND-PERP": "SAND-PERP", - "SANDUSD": "SAND/USD", - "SC-PERP": "SC-PERP", - "SCRT-PERP": "SCRT-PERP", - "SECO-PERP": "SECO-PERP", - "SECOUSD": "SECO/USD", - "SHIB-PERP": "SHIB-PERP", - "SHIBUSD": "SHIB/USD", - "SHIT-0325": "SHIT-0325", - "SHIT-PERP": "SHIT-PERP", - "SKL-PERP": "SKL-PERP", - "SKLUSD": "SKL/USD", - "SLNDUSD": "SLND/USD", - "SLP-PERP": "SLP-PERP", - "SLPUSD": "SLP/USD", - "SLRSUSD": "SLRS/USD", - "SLV-0325": "SLV-0325", - "SLVUSD": "SLV/USD", - "SNX-PERP": "SNX-PERP", - "SNXUSD": "SNX/USD", - "SNYUSD": "SNY/USD", - "SOL-0325": "SOL-0325", - "SOL-PERP": "SOL-PERP", - "SOLBTC": "SOL/BTC", - "SOLUSD": "SOL/USD", - "SOLUSDT": "SOL/USDT", - "SOS-PERP": "SOS-PERP", - "SOSUSD": "SOS/USD", - "SPELL-PERP": "SPELL-PERP", - "SPELLUSD": "SPELL/USD", - "SPY-0325": "SPY-0325", - "SPYUSD": "SPY/USD", - "SQ-0325": "SQ-0325", - "SQUSD": "SQ/USD", - "SRM-PERP": "SRM-PERP", - "SRMUSD": "SRM/USD", - "SRMUSDT": "SRM/USDT", - "SRN-PERP": "SRN-PERP", - "STARSUSD": "STARS/USD", - "STEP-PERP": "STEP-PERP", - "STEPUSD": "STEP/USD", - "STETHUSD": "STETH/USD", - "STMX-PERP": "STMX-PERP", - "STMXUSD": "STMX/USD", - "STORJ-PERP": "STORJ-PERP", - "STORJUSD": "STORJ/USD", - "STSOLUSD": "STSOL/USD", - "STX-PERP": "STX-PERP", - "SUNUSD": "SUN/USD", - "SUSHI-0325": "SUSHI-0325", - "SUSHI-PERP": "SUSHI-PERP", - "SUSHIBEARUSD": "SUSHIBEAR/USD", - "SUSHIBTC": "SUSHI/BTC", - "SUSHIBULLUSD": "SUSHIBULL/USD", - "SUSHIUSD": "SUSHI/USD", - "SUSHIUSDT": "SUSHI/USDT", - "SXP-0325": "SXP-0325", - "SXP-PERP": "SXP-PERP", - "SXPBEARUSD": "SXPBEAR/USD", - "SXPBTC": "SXP/BTC", - "SXPBULLUSD": "SXPBULL/USD", - "SXPHALFUSD": "SXPHALF/USD", - "SXPHALFUSDT": "SXPHALF/USDT", - "SXPHEDGEUSD": "SXPHEDGE/USD", - "SXPUSD": "SXP/USD", - "SXPUSDT": "SXP/USDT", - "THETA-0325": "THETA-0325", - "THETA-PERP": "THETA-PERP", - "THETABEARUSD": "THETABEAR/USD", - "THETABULLUSD": "THETABULL/USD", - "THETAHALFUSD": "THETAHALF/USD", - "THETAHEDGEUSD": "THETAHEDGE/USD", - "TLM-PERP": "TLM-PERP", - "TLMUSD": "TLM/USD", - "TLRY-0325": "TLRY-0325", - "TLRYUSD": "TLRY/USD", - "TOMO-PERP": "TOMO-PERP", - "TOMOBEAR2021USD": "TOMOBEAR2021/USD", - "TOMOBULLUSD": "TOMOBULL/USD", - "TOMOHALFUSD": "TOMOHALF/USD", - "TOMOHEDGEUSD": "TOMOHEDGE/USD", - "TOMOUSD": "TOMO/USD", - "TOMOUSDT": "TOMO/USDT", - "TONCOIN-PERP": "TONCOIN-PERP", - "TONCOINUSD": "TONCOIN/USD", - "TRU-PERP": "TRU-PERP", - "TRUMP2024": "TRUMP2024", - "TRUUSD": "TRU/USD", - "TRUUSDT": "TRU/USDT", - "TRX-0325": "TRX-0325", - "TRX-PERP": "TRX-PERP", - "TRXBEARUSD": "TRXBEAR/USD", - "TRXBTC": "TRX/BTC", - "TRXBULLUSD": "TRXBULL/USD", - "TRXHALFUSD": "TRXHALF/USD", - "TRXHEDGEUSD": "TRXHEDGE/USD", - "TRXUSD": "TRX/USD", - "TRXUSDT": "TRX/USDT", - "TRYB-PERP": "TRYB-PERP", - "TRYBBEARUSD": "TRYBBEAR/USD", - "TRYBBULLUSD": "TRYBBULL/USD", - "TRYBHALFUSD": "TRYBHALF/USD", - "TRYBHEDGEUSD": "TRYBHEDGE/USD", - "TRYBUSD": "TRYB/USD", - "TSLA-0325": "TSLA-0325", - "TSLABTC": "TSLA/BTC", - "TSLADOGE": "TSLA/DOGE", - "TSLAUSD": "TSLA/USD", - "TSM-0325": "TSM-0325", - "TSMUSD": "TSM/USD", - "TULIP-PERP": "TULIP-PERP", - "TULIPUSD": "TULIP/USD", - "TWTR-0325": "TWTR-0325", - "TWTRUSD": "TWTR/USD", - "UBER-0325": "UBER-0325", - "UBERUSD": "UBER/USD", - "UBXTUSD": "UBXT/USD", - "UBXTUSDT": "UBXT/USDT", - "UMEEUSD": "UMEE/USD", - "UNI-0325": "UNI-0325", - "UNI-PERP": "UNI-PERP", - "UNIBTC": "UNI/BTC", - "UNISWAP-0325": "UNISWAP-0325", - "UNISWAP-PERP": "UNISWAP-PERP", - "UNISWAPBEARUSD": "UNISWAPBEAR/USD", - "UNISWAPBULLUSD": "UNISWAPBULL/USD", - "UNIUSD": "UNI/USD", - "UNIUSDT": "UNI/USDT", - "USDT-0325": "USDT-0325", - "USDT-PERP": "USDT-PERP", - "USDTBEARUSD": "USDTBEAR/USD", - "USDTBULLUSD": "USDTBULL/USD", - "USDTHALFUSD": "USDTHALF/USD", - "USDTHEDGEUSD": "USDTHEDGE/USD", - "USDTUSD": "USDT/USD", - "USO-0325": "USO-0325", - "USOUSD": "USO/USD", - "UST-PERP": "UST-PERP", - "USTUSD": "UST/USD", - "USTUSDT": "UST/USDT", - "VET-PERP": "VET-PERP", - "VETBEARUSD": "VETBEAR/USD", - "VETBEARUSDT": "VETBEAR/USDT", - "VETBULLUSD": "VETBULL/USD", - "VETBULLUSDT": "VETBULL/USDT", - "VETHEDGEUSD": "VETHEDGE/USD", - "VGXUSD": "VGX/USD", - "WAVES-0325": "WAVES-0325", - "WAVES-PERP": "WAVES-PERP", - "WAVESUSD": "WAVES/USD", - "WBTCBTC": "WBTC/BTC", - "WBTCUSD": "WBTC/USD", - "WNDRUSD": "WNDR/USD", - "WRXUSD": "WRX/USD", - "WRXUSDT": "WRX/USDT", - "WSB-0325": "WSB-0325", - "XAUT-0325": "XAUT-0325", - "XAUT-PERP": "XAUT-PERP", - "XAUTBEARUSD": "XAUTBEAR/USD", - "XAUTBULLUSD": "XAUTBULL/USD", - "XAUTHALFUSD": "XAUTHALF/USD", - "XAUTHEDGEUSD": "XAUTHEDGE/USD", - "XAUTUSD": "XAUT/USD", - "XAUTUSDT": "XAUT/USDT", - "XEM-PERP": "XEM-PERP", - "XLM-PERP": "XLM-PERP", - "XLMBEARUSD": "XLMBEAR/USD", - "XLMBULLUSD": "XLMBULL/USD", - "XMR-PERP": "XMR-PERP", - "XRP-0325": "XRP-0325", - "XRP-PERP": "XRP-PERP", - "XRPBEARUSD": "XRPBEAR/USD", - "XRPBEARUSDT": "XRPBEAR/USDT", - "XRPBTC": "XRP/BTC", - "XRPBULLUSD": "XRPBULL/USD", - "XRPBULLUSDT": "XRPBULL/USDT", - "XRPHALFUSD": "XRPHALF/USD", - "XRPHEDGEUSD": "XRPHEDGE/USD", - "XRPUSD": "XRP/USD", - "XRPUSDT": "XRP/USDT", - "XTZ-0325": "XTZ-0325", - "XTZ-PERP": "XTZ-PERP", - "XTZBEARUSD": "XTZBEAR/USD", - "XTZBEARUSDT": "XTZBEAR/USDT", - "XTZBULLUSD": "XTZBULL/USD", - "XTZBULLUSDT": "XTZBULL/USDT", - "XTZHALFUSD": "XTZHALF/USD", - "XTZHEDGEUSD": "XTZHEDGE/USD", - "YFI-0325": "YFI-0325", - "YFI-PERP": "YFI-PERP", - "YFIBTC": "YFI/BTC", - "YFII-PERP": "YFII-PERP", - "YFIIUSD": "YFII/USD", - "YFIUSD": "YFI/USD", - "YFIUSDT": "YFI/USDT", - "YGGUSD": "YGG/USD", - "ZEC-PERP": "ZEC-PERP", - "ZECBEARUSD": "ZECBEAR/USD", - "ZECBULLUSD": "ZECBULL/USD", - "ZIL-PERP": "ZIL-PERP", - "ZM-0325": "ZM-0325", - "ZMUSD": "ZM/USD", - "ZRX-PERP": "ZRX-PERP", - "ZRXUSD": "ZRX/USD", - "GMTUSD": "GMT/USD", - "GMT-PERP": "GMT-PERP", -} diff --git a/pkg/exchange/ftx/ticker_test.go b/pkg/exchange/ftx/ticker_test.go deleted file mode 100644 index 0bf0197538..0000000000 --- a/pkg/exchange/ftx/ticker_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package ftx - -import ( - "context" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestExchange_QueryTickers_AllSymbols(t *testing.T) { - key := os.Getenv("FTX_API_KEY") - secret := os.Getenv("FTX_API_SECRET") - subAccount := os.Getenv("FTX_SUBACCOUNT") - if len(key) == 0 && len(secret) == 0 { - t.Skip("api key/secret are not configured") - } - - e := NewExchange(key, secret, subAccount) - got, err := e.QueryTickers(context.Background()) - if assert.NoError(t, err) { - assert.True(t, len(got) > 1, "binance: attempting to get all symbol tickers, but get 1 or less") - } -} - -func TestExchange_QueryTickers_SomeSymbols(t *testing.T) { - key := os.Getenv("FTX_API_KEY") - secret := os.Getenv("FTX_API_SECRET") - subAccount := os.Getenv("FTX_SUBACCOUNT") - if len(key) == 0 && len(secret) == 0 { - t.Skip("api key/secret are not configured") - } - - e := NewExchange(key, secret, subAccount) - got, err := e.QueryTickers(context.Background(), "BTCUSDT", "ETHUSDT") - if assert.NoError(t, err) { - assert.Len(t, got, 2, "binance: attempting to get two symbols, but number of tickers do not match") - } -} - -func TestExchange_QueryTickers_SingleSymbol(t *testing.T) { - key := os.Getenv("FTX_API_KEY") - secret := os.Getenv("FTX_API_SECRET") - subAccount := os.Getenv("FTX_SUBACCOUNT") - if len(key) == 0 && len(secret) == 0 { - t.Skip("api key/secret are not configured") - } - - e := NewExchange(key, secret, subAccount) - got, err := e.QueryTickers(context.Background(), "BTCUSDT") - if assert.NoError(t, err) { - assert.Len(t, got, 1, "binance: attempting to get one symbol, but number of tickers do not match") - } -} diff --git a/pkg/exchange/ftx/websocket_messages.go b/pkg/exchange/ftx/websocket_messages.go deleted file mode 100644 index 2265e1de9a..0000000000 --- a/pkg/exchange/ftx/websocket_messages.go +++ /dev/null @@ -1,468 +0,0 @@ -package ftx - -import ( - "encoding/json" - "fmt" - "hash/crc32" - "math" - "strconv" - "strings" - "time" - - "github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi" - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -type operation string - -const ping operation = "ping" -const login operation = "login" -const subscribe operation = "subscribe" -const unsubscribe operation = "unsubscribe" - -type channel string - -const orderBookChannel channel = "orderbook" -const marketTradeChannel channel = "trades" -const bookTickerChannel channel = "ticker" -const privateOrdersChannel channel = "orders" -const privateTradesChannel channel = "fills" - -var errUnsupportedConversion = fmt.Errorf("unsupported conversion") - -/* -Private: - order update: `{'op': 'subscribe', 'channel': 'orders'}` - login: `{"args": { "key": "", "sign": "", "time": }, "op": "login" }` - -*/ -type websocketRequest struct { - Operation operation `json:"op"` - - // {'op': 'subscribe', 'channel': 'trades', 'market': 'BTC-PERP'} - Channel channel `json:"channel,omitempty"` - Market string `json:"market,omitempty"` - - Login loginArgs `json:"args,omitempty"` -} - -/* -{ - "args": { - "key": "", - "sign": "", - "time": - }, - "op": "login" -} -*/ -type loginArgs struct { - Key string `json:"key"` - Signature string `json:"sign"` - Time int64 `json:"time"` - SubAccount string `json:"subaccount,omitempty"` -} - -func newLoginRequest(key, secret string, t time.Time, subaccount string) websocketRequest { - millis := t.UnixNano() / int64(time.Millisecond) - return websocketRequest{ - Operation: login, - Login: loginArgs{ - Key: key, - Signature: sign(secret, loginBody(millis)), - Time: millis, - SubAccount: subaccount, - }, - } -} - -func loginBody(millis int64) string { - return fmt.Sprintf("%dwebsocket_login", millis) -} - -type respType string - -const pongRespType respType = "pong" -const errRespType respType = "error" -const subscribedRespType respType = "subscribed" -const unsubscribedRespType respType = "unsubscribed" -const infoRespType respType = "info" -const partialRespType respType = "partial" -const updateRespType respType = "update" - -type websocketResponse struct { - mandatoryFields - - optionalFields -} - -type mandatoryFields struct { - Channel channel `json:"channel"` - - Type respType `json:"type"` -} - -type optionalFields struct { - Market string `json:"market"` - - // Example: {"type": "error", "code": 404, "msg": "No such market: BTCUSDT"} - Code int64 `json:"code"` - - Message string `json:"msg"` - - Data json.RawMessage `json:"data"` -} - -type orderUpdateResponse struct { - mandatoryFields - - Data ftxapi.Order `json:"data"` -} - -type trade struct { - Price fixedpoint.Value `json:"price"` - Size fixedpoint.Value `json:"size"` - Side string `json:"side"` - Liquidation bool `json:"liquidation"` - Time time.Time `json:"time"` -} -type tradeResponse struct { - mandatoryFields - Data []trade `json:"data"` -} - -func (r websocketResponse) toMarketTradeResponse() (t []types.Trade, err error) { - if r.Channel != marketTradeChannel { - return t, fmt.Errorf("type %s, channel %s: channel incorrect", r.Type, r.Channel) - } - var tds []trade - if err = json.Unmarshal(r.Data, &tds); err != nil { - return t, err - } - t = make([]types.Trade, len(tds)) - for i, td := range tds { - tt := &t[i] - tt.Exchange = types.ExchangeFTX - tt.Price = td.Price - tt.Quantity = td.Size - tt.QuoteQuantity = td.Size - tt.Symbol = r.Market - tt.Side = types.SideType(TrimUpperString(string(td.Side))) - tt.IsBuyer = true - tt.IsMaker = false - tt.Time = types.Time(td.Time) - } - return t, nil -} - -func (r websocketResponse) toOrderUpdateResponse() (orderUpdateResponse, error) { - if r.Channel != privateOrdersChannel { - return orderUpdateResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) - } - var o orderUpdateResponse - if err := json.Unmarshal(r.Data, &o.Data); err != nil { - return orderUpdateResponse{}, err - } - o.mandatoryFields = r.mandatoryFields - return o, nil -} - -type tradeUpdateResponse struct { - mandatoryFields - - Data ftxapi.Fill `json:"data"` -} - -func (r websocketResponse) toTradeUpdateResponse() (tradeUpdateResponse, error) { - if r.Channel != privateTradesChannel { - return tradeUpdateResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) - } - var t tradeUpdateResponse - if err := json.Unmarshal(r.Data, &t.Data); err != nil { - return tradeUpdateResponse{}, err - } - t.mandatoryFields = r.mandatoryFields - return t, nil -} - -/* - Private: - order: {"type": "subscribed", "channel": "orders"} - -Public - orderbook: {"type": "subscribed", "channel": "orderbook", "market": "BTC/USDT"} - -*/ -type subscribedResponse struct { - mandatoryFields - - Market string `json:"market"` -} - -func (s subscribedResponse) String() string { - return fmt.Sprintf("%s channel is subscribed", strings.TrimSpace(fmt.Sprintf("%s %s", s.Market, s.Channel))) -} - -// {"type": "subscribed", "channel": "orderbook", "market": "BTC/USDT"} -func (r websocketResponse) toSubscribedResponse() (subscribedResponse, error) { - if r.Type != subscribedRespType { - return subscribedResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) - } - - return subscribedResponse{ - mandatoryFields: r.mandatoryFields, - Market: r.Market, - }, nil -} - -// {"type": "error", "code": 400, "msg": "Already logged in"} -type errResponse struct { - Code int64 `json:"code"` - Message string `json:"msg"` -} - -func (e errResponse) String() string { - return fmt.Sprintf("%d: %s", e.Code, e.Message) -} - -func (r websocketResponse) toErrResponse() errResponse { - return errResponse{ - Code: r.Code, - Message: r.Message, - } -} - -// sample :{"bid": 49194.0, "ask": 49195.0, "bidSize": 0.0775, "askSize": 0.0247, "last": 49200.0, "time": 1640171788.9339821} -func (r websocketResponse) toBookTickerResponse() (bookTickerResponse, error) { - if r.Channel != bookTickerChannel { - return bookTickerResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) - } - - var o bookTickerResponse - if err := json.Unmarshal(r.Data, &o); err != nil { - return bookTickerResponse{}, err - } - - o.mandatoryFields = r.mandatoryFields - o.Market = r.Market - o.Timestamp = nanoToTime(o.Time) - - return o, nil -} - -func (r websocketResponse) toPublicOrderBookResponse() (orderBookResponse, error) { - if r.Channel != orderBookChannel { - return orderBookResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) - } - - var o orderBookResponse - if err := json.Unmarshal(r.Data, &o); err != nil { - return orderBookResponse{}, err - } - - o.mandatoryFields = r.mandatoryFields - o.Market = r.Market - o.Timestamp = nanoToTime(o.Time) - - return o, nil -} - -func nanoToTime(input float64) time.Time { - sec, dec := math.Modf(input) - return time.Unix(int64(sec), int64(dec*1e9)) -} - -type orderBookResponse struct { - mandatoryFields - - Market string `json:"market"` - - Action string `json:"action"` - - Time float64 `json:"time"` - - Timestamp time.Time - - Checksum uint32 `json:"checksum"` - - // best 100 orders. Ex. {[100,1], [50, 2]} - Bids [][]json.Number `json:"bids"` - - // best 100 orders. Ex. {[51, 1], [102, 3]} - Asks [][]json.Number `json:"asks"` -} - -type bookTickerResponse struct { - mandatoryFields - Market string `json:"market"` - Bid fixedpoint.Value `json:"bid"` - Ask fixedpoint.Value `json:"ask"` - BidSize fixedpoint.Value `json:"bidSize"` - AskSize fixedpoint.Value `json:"askSize"` - Last fixedpoint.Value `json:"last"` - Time float64 `json:"time"` - Timestamp time.Time -} - -// only 100 orders so we use linear search here -func (r *orderBookResponse) update(orderUpdates orderBookResponse) { - r.Checksum = orderUpdates.Checksum - r.updateBids(orderUpdates.Bids) - r.updateAsks(orderUpdates.Asks) -} - -func (r *orderBookResponse) updateAsks(asks [][]json.Number) { - higherPrice := func(dst, src float64) bool { - return dst < src - } - for _, o := range asks { - if remove := o[1] == "0"; remove { - r.Asks = removePrice(r.Asks, o[0]) - } else { - r.Asks = upsertPriceVolume(r.Asks, o, higherPrice) - } - } -} - -func (r *orderBookResponse) updateBids(bids [][]json.Number) { - lessPrice := func(dst, src float64) bool { - return dst > src - } - for _, o := range bids { - if remove := o[1] == "0"; remove { - r.Bids = removePrice(r.Bids, o[0]) - } else { - r.Bids = upsertPriceVolume(r.Bids, o, lessPrice) - } - } -} - -func upsertPriceVolume(dst [][]json.Number, src []json.Number, priceComparator func(dst float64, src float64) bool) [][]json.Number { - for i, pv := range dst { - dstPrice := pv[0] - srcPrice := src[0] - - // update volume - if dstPrice == srcPrice { - pv[1] = src[1] - return dst - } - - // The value must be a number which is verified by json.Unmarshal, so the err - // should never happen. - dstPriceNum, err := strconv.ParseFloat(string(dstPrice), 64) - if err != nil { - logger.WithError(err).Errorf("unexpected price %s", dstPrice) - continue - } - srcPriceNum, err := strconv.ParseFloat(string(srcPrice), 64) - if err != nil { - logger.WithError(err).Errorf("unexpected price updates %s", srcPrice) - continue - } - - if !priceComparator(dstPriceNum, srcPriceNum) { - return insertAt(dst, i, src) - } - } - - return append(dst, src) -} - -func insertAt(dst [][]json.Number, id int, pv []json.Number) (result [][]json.Number) { - result = append(result, dst[:id]...) - result = append(result, pv) - result = append(result, dst[id:]...) - return -} - -func removePrice(dst [][]json.Number, price json.Number) [][]json.Number { - for i, pv := range dst { - if pv[0] == price { - return append(dst[:i], dst[i+1:]...) - } - } - - return dst -} - -func (r orderBookResponse) verifyChecksum() error { - if crc32Val := crc32.ChecksumIEEE([]byte(checksumString(r.Bids, r.Asks))); crc32Val != r.Checksum { - return fmt.Errorf("expected checksum %d, actual checksum %d: %w", r.Checksum, crc32Val, errUnmatchedChecksum) - } - return nil -} - -// :::... -func checksumString(bids, asks [][]json.Number) string { - sb := strings.Builder{} - appendNumber := func(pv []json.Number) { - if sb.Len() != 0 { - sb.WriteString(":") - } - sb.WriteString(string(pv[0])) - sb.WriteString(":") - sb.WriteString(string(pv[1])) - } - - bidsLen := len(bids) - asksLen := len(asks) - for i := 0; i < bidsLen || i < asksLen; i++ { - if i < bidsLen { - appendNumber(bids[i]) - } - if i < asksLen { - appendNumber(asks[i]) - } - } - return sb.String() -} - -var errUnmatchedChecksum = fmt.Errorf("unmatched checksum") - -func toGlobalOrderBook(r orderBookResponse) (types.SliceOrderBook, error) { - bids, err := toPriceVolumeSlice(r.Bids) - if err != nil { - return types.SliceOrderBook{}, fmt.Errorf("can't convert bids to priceVolumeSlice: %w", err) - } - asks, err := toPriceVolumeSlice(r.Asks) - if err != nil { - return types.SliceOrderBook{}, fmt.Errorf("can't convert asks to priceVolumeSlice: %w", err) - } - return types.SliceOrderBook{ - // ex. BTC/USDT - Symbol: toGlobalSymbol(strings.ToUpper(r.Market)), - Bids: bids, - Asks: asks, - }, nil -} - -func toGlobalBookTicker(r bookTickerResponse) (types.BookTicker, error) { - return types.BookTicker{ - // ex. BTC/USDT - Symbol: toGlobalSymbol(strings.ToUpper(r.Market)), - // Time: r.Timestamp, - Buy: r.Bid, - BuySize: r.BidSize, - Sell: r.Ask, - SellSize: r.AskSize, - // Last: r.Last, - }, nil -} - -func toPriceVolumeSlice(orders [][]json.Number) (types.PriceVolumeSlice, error) { - var pv types.PriceVolumeSlice - for _, o := range orders { - p, err := fixedpoint.NewFromString(string(o[0])) - if err != nil { - return nil, fmt.Errorf("can't convert price %+v to fixedpoint: %w", o[0], err) - } - v, err := fixedpoint.NewFromString(string(o[1])) - if err != nil { - return nil, fmt.Errorf("can't convert volume %+v to fixedpoint: %w", o[0], err) - } - pv = append(pv, types.PriceVolume{Price: p, Volume: v}) - } - return pv, nil -} diff --git a/pkg/exchange/ftx/websocket_messages_test.go b/pkg/exchange/ftx/websocket_messages_test.go deleted file mode 100644 index 5a9635e5a1..0000000000 --- a/pkg/exchange/ftx/websocket_messages_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package ftx - -import ( - "encoding/json" - "io/ioutil" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi" - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -func Test_rawResponse_toSubscribedResp(t *testing.T) { - input := `{"type": "subscribed", "channel": "orderbook", "market": "BTC/USDT"}` - var m websocketResponse - assert.NoError(t, json.Unmarshal([]byte(input), &m)) - r, err := m.toSubscribedResponse() - assert.NoError(t, err) - assert.Equal(t, subscribedResponse{ - mandatoryFields: mandatoryFields{ - Channel: orderBookChannel, - Type: subscribedRespType, - }, - Market: "BTC/USDT", - }, r) -} - -func Test_websocketResponse_toPublicOrderBookResponse(t *testing.T) { - f, err := ioutil.ReadFile("./orderbook_snapshot.json") - assert.NoError(t, err) - var m websocketResponse - assert.NoError(t, json.Unmarshal(f, &m)) - r, err := m.toPublicOrderBookResponse() - assert.NoError(t, err) - assert.Equal(t, partialRespType, r.Type) - assert.Equal(t, orderBookChannel, r.Channel) - assert.Equal(t, "BTC/USDT", r.Market) - assert.Equal(t, int64(1614520368), r.Timestamp.Unix()) - assert.Equal(t, uint32(2150525410), r.Checksum) - assert.Len(t, r.Bids, 100) - assert.Equal(t, []json.Number{"44555.0", "3.3968"}, r.Bids[0]) - assert.Equal(t, []json.Number{"44554.0", "0.0561"}, r.Bids[1]) - assert.Len(t, r.Asks, 100) - assert.Equal(t, []json.Number{"44574.0", "0.4591"}, r.Asks[0]) - assert.Equal(t, []json.Number{"44579.0", "0.15"}, r.Asks[1]) -} - -func Test_orderBookResponse_toGlobalOrderBook(t *testing.T) { - f, err := ioutil.ReadFile("./orderbook_snapshot.json") - assert.NoError(t, err) - var m websocketResponse - assert.NoError(t, json.Unmarshal(f, &m)) - r, err := m.toPublicOrderBookResponse() - assert.NoError(t, err) - - b, err := toGlobalOrderBook(r) - assert.NoError(t, err) - assert.Equal(t, "BTCUSDT", b.Symbol) - isValid, err := b.IsValid() - assert.True(t, isValid) - assert.NoError(t, err) - - assert.Len(t, b.Bids, 100) - assert.Equal(t, types.PriceVolume{ - Price: fixedpoint.MustNewFromString("44555.0"), - Volume: fixedpoint.MustNewFromString("3.3968"), - }, b.Bids[0]) - assert.Equal(t, types.PriceVolume{ - Price: fixedpoint.MustNewFromString("44222.0"), - Volume: fixedpoint.MustNewFromString("0.0002"), - }, b.Bids[99]) - - assert.Len(t, b.Asks, 100) - assert.Equal(t, types.PriceVolume{ - Price: fixedpoint.MustNewFromString("44574.0"), - Volume: fixedpoint.MustNewFromString("0.4591"), - }, b.Asks[0]) - assert.Equal(t, types.PriceVolume{ - Price: fixedpoint.MustNewFromString("45010.0"), - Volume: fixedpoint.MustNewFromString("0.0003"), - }, b.Asks[99]) - -} - -func Test_checksumString(t *testing.T) { - type args struct { - bids [][]json.Number - asks [][]json.Number - } - tests := []struct { - name string - args args - want string - }{ - { - name: "more bids", - args: args{ - bids: [][]json.Number{{"5000.5", "10"}, {"4995.0", "5"}}, - asks: [][]json.Number{{"5001.0", "6"}}, - }, - want: "5000.5:10:5001.0:6:4995.0:5", - }, - { - name: "lengths of bids and asks are the same", - args: args{ - bids: [][]json.Number{{"5000.5", "10"}, {"4995.0", "5"}}, - asks: [][]json.Number{{"5001.0", "6"}, {"5002.0", "7"}}, - }, - want: "5000.5:10:5001.0:6:4995.0:5:5002.0:7", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := checksumString(tt.args.bids, tt.args.asks); got != tt.want { - t.Errorf("checksumString() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_orderBookResponse_verifyChecksum(t *testing.T) { - for _, file := range []string{"./orderbook_snapshot.json"} { - f, err := ioutil.ReadFile(file) - assert.NoError(t, err) - var m websocketResponse - assert.NoError(t, json.Unmarshal(f, &m)) - r, err := m.toPublicOrderBookResponse() - assert.NoError(t, err) - assert.NoError(t, r.verifyChecksum(), "filename: "+file) - } -} - -func Test_removePrice(t *testing.T) { - pairs := [][]json.Number{{"123.99", "2.0"}, {"2234.12", "3.1"}} - assert.Equal(t, pairs, removePrice(pairs, "99333")) - - pairs = removePrice(pairs, "2234.12") - assert.Equal(t, [][]json.Number{{"123.99", "2.0"}}, pairs) - assert.Equal(t, [][]json.Number{}, removePrice(pairs, "123.99")) -} - -func Test_orderBookResponse_update(t *testing.T) { - ob := &orderBookResponse{Bids: nil, Asks: nil} - - ob.update(orderBookResponse{ - Bids: [][]json.Number{{"1.0", "0"}, {"10.0", "1"}, {"11.0", "1"}}, - Asks: [][]json.Number{{"1.0", "1"}}, - }) - assert.Equal(t, [][]json.Number{{"11.0", "1"}, {"10.0", "1"}}, ob.Bids) - assert.Equal(t, [][]json.Number{{"1.0", "1"}}, ob.Asks) - ob.update(orderBookResponse{ - Bids: [][]json.Number{{"9.0", "1"}, {"12.0", "1"}, {"10.5", "1"}}, - Asks: [][]json.Number{{"1.0", "0"}}, - }) - assert.Equal(t, [][]json.Number{{"12.0", "1"}, {"11.0", "1"}, {"10.5", "1"}, {"10.0", "1"}, {"9.0", "1"}}, ob.Bids) - assert.Equal(t, [][]json.Number{}, ob.Asks) - - // remove them - ob.update(orderBookResponse{ - Bids: [][]json.Number{{"9.0", "0"}, {"12.0", "0"}, {"10.5", "0"}}, - Asks: [][]json.Number{{"9.0", "1"}, {"12.0", "1"}, {"10.5", "1"}}, - }) - assert.Equal(t, [][]json.Number{{"11.0", "1"}, {"10.0", "1"}}, ob.Bids) - assert.Equal(t, [][]json.Number{{"9.0", "1"}, {"10.5", "1"}, {"12.0", "1"}}, ob.Asks) -} - -func Test_insertAt(t *testing.T) { - r := insertAt([][]json.Number{{"1.2", "2"}, {"1.4", "2"}}, 1, []json.Number{"1.3", "2"}) - assert.Equal(t, [][]json.Number{{"1.2", "2"}, {"1.3", "2"}, {"1.4", "2"}}, r) - - r = insertAt([][]json.Number{{"1.2", "2"}, {"1.4", "2"}}, 0, []json.Number{"1.1", "2"}) - assert.Equal(t, [][]json.Number{{"1.1", "2"}, {"1.2", "2"}, {"1.4", "2"}}, r) - - r = insertAt([][]json.Number{{"1.2", "2"}, {"1.4", "2"}}, 2, []json.Number{"1.5", "2"}) - assert.Equal(t, [][]json.Number{{"1.2", "2"}, {"1.4", "2"}, {"1.5", "2"}}, r) -} - -func Test_newLoginRequest(t *testing.T) { - // From API doc: https://docs.ftx.com/?javascript#authentication-2 - r := newLoginRequest("", "Y2QTHI23f23f23jfjas23f23To0RfUwX3H42fvN-", time.Unix(0, 1557246346499*int64(time.Millisecond)), "") - // pragma: allowlist nextline secret - expectedSignature := "d10b5a67a1a941ae9463a60b285ae845cdeac1b11edc7da9977bef0228b96de9" - assert.Equal(t, expectedSignature, r.Login.Signature) - jsonStr, err := json.Marshal(r) - assert.NoError(t, err) - assert.True(t, strings.Contains(string(jsonStr), expectedSignature)) -} - -func Test_websocketResponse_toOrderUpdateResponse(t *testing.T) { - input := []byte(` -{ - "channel": "orders", - "type": "update", - "data": { - "id": 12345, - "clientId": "test-client-id", - "market": "SOL/USD", - "type": "limit", - "side": "buy", - "price": 0.5, - "size": 100.0, - "status": "closed", - "filledSize": 0.0, - "remainingSize": 0.0, - "reduceOnly": false, - "liquidation": false, - "avgFillPrice": null, - "postOnly": false, - "ioc": false, - "createdAt": "2021-03-27T11:00:36.418674+00:00" - } -} -`) - - var raw websocketResponse - assert.NoError(t, json.Unmarshal(input, &raw)) - - r, err := raw.toOrderUpdateResponse() - assert.NoError(t, err) - - assert.Equal(t, orderUpdateResponse{ - mandatoryFields: mandatoryFields{ - Channel: privateOrdersChannel, - Type: updateRespType, - }, - Data: ftxapi.Order{ - Id: 12345, - ClientId: "test-client-id", - Market: "SOL/USD", - Type: "limit", - Side: "buy", - Price: fixedpoint.NewFromFloat(0.5), - Size: fixedpoint.NewFromInt(100), - Status: "closed", - FilledSize: fixedpoint.Zero, - RemainingSize: fixedpoint.Zero, - ReduceOnly: false, - AvgFillPrice: fixedpoint.Zero, - PostOnly: false, - Ioc: false, - CreatedAt: mustParseDatetime("2021-03-27T11:00:36.418674+00:00"), - Future: "", - }, - }, r) -} diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index cb6708f94c..bae287c79d 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -26,13 +26,13 @@ func (n *ExchangeName) UnmarshalJSON(data []byte) error { } switch s { - case "max", "binance", "ftx", "okex": + case "max", "binance", "okex", "kucoin": *n = ExchangeName(s) return nil } - return fmt.Errorf("unknown or unsupported exchange name: %s, valid names are: max, binance, ftx", s) + return fmt.Errorf("unknown or unsupported exchange name: %s, valid names are: max, binance, okex, kucoin", s) } func (n ExchangeName) String() string { @@ -42,7 +42,6 @@ func (n ExchangeName) String() string { const ( ExchangeMax ExchangeName = "max" ExchangeBinance ExchangeName = "binance" - ExchangeFTX ExchangeName = "ftx" ExchangeOKEx ExchangeName = "okex" ExchangeKucoin ExchangeName = "kucoin" ExchangeBacktest ExchangeName = "backtest" @@ -51,7 +50,6 @@ const ( var SupportedExchanges = []ExchangeName{ ExchangeMax, ExchangeBinance, - ExchangeFTX, ExchangeOKEx, ExchangeKucoin, // note: we are not using "backtest" @@ -63,8 +61,6 @@ func ValidExchangeName(a string) (ExchangeName, error) { return ExchangeMax, nil case "binance", "bn": return ExchangeBinance, nil - case "ftx": - return ExchangeFTX, nil case "okex": return ExchangeOKEx, nil case "kucoin": diff --git a/pkg/types/exchange_icon.go b/pkg/types/exchange_icon.go index a85624a134..7c4df85b09 100644 --- a/pkg/types/exchange_icon.go +++ b/pkg/types/exchange_icon.go @@ -8,8 +8,6 @@ func ExchangeFooterIcon(exName ExchangeName) string { footerIcon = "https://bin.bnbstatic.com/static/images/common/favicon.ico" case ExchangeMax: footerIcon = "https://max.maicoin.com/favicon-16x16.png" - case ExchangeFTX: - footerIcon = "https://ftx.com/favicon.ico?v=2" case ExchangeOKEx: footerIcon = "https://static.okex.com/cdn/assets/imgs/MjAxODg/D91A7323087D31A588E0D2A379DD7747.png" case ExchangeKucoin: From 66f0f3e1138e6d3c22af5ed665b409dcfca67132 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 24 Nov 2022 17:06:14 +0800 Subject: [PATCH 0061/1392] strategy/linregmaker: remove useTickerPrice --- pkg/strategy/linregmaker/strategy.go | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index ec11d95b92..59ad728e12 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -84,11 +84,6 @@ type Strategy struct { // When this is on, places orders only when the current price is in the bollinger band. TradeInBand bool `json:"tradeInBand"` - // useTickerPrice use the ticker api to get the mid-price instead of the closed kline price. - // The back-test engine is kline-based, so the ticker price api is not supported. - // Turn this on if you want to do real trading. - useTickerPrice bool - // Spread is the price spread from the middle price. // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) @@ -148,7 +143,6 @@ func (s *Strategy) InstanceID() string { } // Validate basic config parameters -// TODO LATER: Validate more func (s *Strategy) Validate() error { if len(s.Symbol) == 0 { return errors.New("symbol is required") @@ -162,11 +156,6 @@ func (s *Strategy) Validate() error { return errors.New("reverseEMA must be set") } - // Use interval of ReverseEMA if ReverseInterval is omitted - if s.ReverseInterval == "" { - s.ReverseInterval = s.ReverseEMA.Interval - } - if s.FastLinReg == nil { return errors.New("fastLinReg must be set") } @@ -186,7 +175,10 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Initialize ReverseEMA s.ReverseEMA = s.StandardIndicatorSet.EWMA(s.ReverseEMA.IntervalWindow) - // Subscribe for ReverseInterval + // Subscribe for ReverseInterval. Use interval of ReverseEMA if ReverseInterval is omitted + if s.ReverseInterval == "" { + s.ReverseInterval = s.ReverseEMA.Interval + } session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: s.ReverseInterval, }) @@ -435,12 +427,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.ExitMethods.Bind(session, s.orderExecutor) - if bbgo.IsBackTesting { - s.useTickerPrice = false - } else { - s.useTickerPrice = true - } - // Default spread if s.Spread == fixedpoint.Zero { s.Spread = fixedpoint.NewFromFloat(0.001) @@ -501,7 +487,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // midPrice for ask and bid prices var midPrice fixedpoint.Value - if s.useTickerPrice { + if !bbgo.IsBackTesting { ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { return From a8cd36fdf0694f7230e5ffbb4a53635980bc3f43 Mon Sep 17 00:00:00 2001 From: a dwarf <74894662+a-dwarf@users.noreply.github.com> Date: Thu, 24 Nov 2022 17:21:24 +0800 Subject: [PATCH 0062/1392] move bbgo completion to topics --- doc/README.md | 1 + doc/{commands/bbgo_completion.md => topics/bbgo-completion.md} | 0 2 files changed, 1 insertion(+) rename doc/{commands/bbgo_completion.md => topics/bbgo-completion.md} (100%) diff --git a/doc/README.md b/doc/README.md index 869a0a2c52..c9b4f6461a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -7,6 +7,7 @@ * [Back-testing](topics/back-testing.md) - How to back-test strategies * [TWAP](topics/twap.md) - TWAP order execution to buy/sell large quantity of order * [Dnum Installation](topics/dnum-binary.md) - installation of high-precision version of bbgo +* [bbgo completion](topics/bbgo-completion.md) - Convenient use of the command line ### Configuration * [Setting up Slack Notification](configuration/slack.md) diff --git a/doc/commands/bbgo_completion.md b/doc/topics/bbgo-completion.md similarity index 100% rename from doc/commands/bbgo_completion.md rename to doc/topics/bbgo-completion.md From daab130735b4dc0c4f9187512d6d365f336555ad Mon Sep 17 00:00:00 2001 From: a dwarf <74894662+a-dwarf@users.noreply.github.com> Date: Fri, 25 Nov 2022 10:37:54 +0800 Subject: [PATCH 0063/1392] upgrade cobra --- go.mod | 2 +- go.sum | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 536111a4d0..75acebd4af 100644 --- a/go.mod +++ b/go.mod @@ -90,7 +90,7 @@ require ( github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect diff --git a/go.sum b/go.sum index c7221e919e..0c04a2ca09 100644 --- a/go.sum +++ b/go.sum @@ -339,6 +339,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -631,6 +633,8 @@ github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= From 5c60ad0e416dc0a0009177faec8c8768c1996c2d Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 25 Nov 2022 12:27:47 +0800 Subject: [PATCH 0064/1392] strategy/linregmaker: re-organize strategy logic --- pkg/strategy/linregmaker/strategy.go | 195 ++++++++++++++++++++++----- 1 file changed, 159 insertions(+), 36 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 59ad728e12..141e39932a 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -25,6 +25,7 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) // TODO: Logic for backtest +// TODO: initial trend func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -240,6 +241,20 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu return s.orderExecutor.ClosePosition(ctx, percentage) } +// isAllowOppositePosition returns if opening opposite position is allowed +func (s *Strategy) isAllowOppositePosition() bool { + if !s.AllowOppositePosition { + return false + } + + if (s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0) || + (s.mainTrendCurrent == types.DirectionDown && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0) { + return true + } + + return false +} + // updateSpread for ask and bid price func (s *Strategy) updateSpread() { // Update spreads with dynamic spread @@ -291,8 +306,6 @@ func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoin // getOrderQuantities returns sell and buy qty func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { - // TODO: spot, margin, and futures - // Default sellQuantity = s.QuantityOrAmount.CalculateQuantity(askPrice) buyQuantity = s.QuantityOrAmount.CalculateQuantity(bidPrice) @@ -328,10 +341,21 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } // Faster position decrease - if s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0 { - sellQuantity = sellQuantity * s.FasterDecreaseRatio - } else if s.mainTrendCurrent == types.DirectionDown && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0 { - buyQuantity = buyQuantity * s.FasterDecreaseRatio + if s.isAllowOppositePosition() { + if s.mainTrendCurrent == types.DirectionUp { + sellQuantity = sellQuantity * s.FasterDecreaseRatio + } else if s.mainTrendCurrent == types.DirectionDown { + buyQuantity = buyQuantity * s.FasterDecreaseRatio + } + } + + // Reduce order qty to fit current position + if !s.isAllowOppositePosition() { + if s.Position.IsLong() && s.Position.Base.Abs().Compare(sellQuantity) < 0 { + sellQuantity = s.Position.Base.Abs() + } else if s.Position.IsShort() && s.Position.Base.Abs().Compare(buyQuantity) < 0 { + buyQuantity = s.Position.Base.Abs() + } } log.Infof("sell qty:%v buy qty: %v", sellQuantity, buyQuantity) @@ -339,8 +363,42 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp return sellQuantity, buyQuantity } +// getAllowedBalance returns the allowed qty of orders +// TODO LATER: Check max qty of margin and futures +func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { + // Default + baseQty = fixedpoint.PosInf + quoteQty = fixedpoint.PosInf + + balances := s.session.GetAccount().Balances() + baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] + quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] + + isMargin := s.session.Margin || s.session.IsolatedMargin + isFutures := s.session.Futures || s.session.IsolatedFutures + + if isMargin { + + } else if isFutures { + + } else { + if !hasBaseBalance { + baseQty = fixedpoint.Zero + } else { + baseQty = baseBalance.Available + } + if !hasQuoteBalance { + quoteQty = fixedpoint.Zero + } else { + quoteQty = quoteBalance.Available + } + } + + return baseQty, quoteQty +} + // getCanBuySell returns the buy sell switches -func (s *Strategy) getCanBuySell(midPrice fixedpoint.Value) (canBuy bool, canSell bool) { +func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice fixedpoint.Value) (canBuy bool, canSell bool) { // By default, both buy and sell are on, which means we will place buy and sell orders canBuy = true canSell = true @@ -352,33 +410,118 @@ func (s *Strategy) getCanBuySell(midPrice fixedpoint.Value) (canBuy bool, canSel } else if s.mainTrendCurrent == types.DirectionDown { canSell = false } + log.Infof("current position %v larger than max exposure %v, skip increase position", s.Position.GetBase().Abs(), s.MaxExposurePosition) } + // Check TradeInBand if s.TradeInBand { // Price too high - if midPrice.Float64() > s.neutralBoll.UpBand.Last() { + if bidPrice.Float64() > s.neutralBoll.UpBand.Last() { canBuy = false log.Infof("tradeInBand is set, skip buy when the price is higher than the neutralBB") } // Price too low in uptrend - if midPrice.Float64() < s.neutralBoll.DownBand.Last() { + if askPrice.Float64() < s.neutralBoll.DownBand.Last() { canSell = false log.Infof("tradeInBand is set, skip sell when the price is lower than the neutralBB") } } // Stop decrease when position closed unless both LinRegs are in the opposite direction to the main trend - if s.Position.IsClosed() || s.Position.IsDust(midPrice) { - if s.mainTrendCurrent == types.DirectionUp && !(s.AllowOppositePosition && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0) { + if !s.isAllowOppositePosition() { + if s.mainTrendCurrent == types.DirectionUp && (s.Position.IsClosed() || s.Position.IsDust(askPrice)) { canSell = false - } else if s.mainTrendCurrent == types.DirectionDown && !(s.AllowOppositePosition && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0) { + } else if s.mainTrendCurrent == types.DirectionDown && (s.Position.IsClosed() || s.Position.IsDust(bidPrice)) { canBuy = false } } + // Check against account balance + baseQty, quoteQty := s.getAllowedBalance() + if buyQuantity.Compare(quoteQty.Div(bidPrice)) > 0 { + canBuy = false + } + if sellQuantity.Compare(baseQty) > 0 { + canSell = false + } + return canBuy, canSell } +// getOrderForms returns buy and sell order form for submission +// TODO: Simplify +func (s *Strategy) getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice fixedpoint.Value) (buyOrder types.SubmitOrder, sellOrder types.SubmitOrder) { + sellOrder = types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: sellQuantity, + Price: askPrice, + Market: s.Market, + GroupID: s.groupID, + } + buyOrder = types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: buyQuantity, + Price: bidPrice, + Market: s.Market, + GroupID: s.groupID, + } + + isMargin := s.session.Margin || s.session.IsolatedMargin + isFutures := s.session.Futures || s.session.IsolatedFutures + + if s.Position.IsClosed() { + if isMargin { + buyOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + sellOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + } else if isFutures { + buyOrder.ReduceOnly = false + sellOrder.ReduceOnly = false + } + } else if s.Position.IsLong() { + if isMargin { + buyOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + sellOrder.MarginSideEffect = types.SideEffectTypeAutoRepay + } else if isFutures { + buyOrder.ReduceOnly = false + sellOrder.ReduceOnly = true + } + + if s.Position.Base.Abs().Compare(sellOrder.Quantity) < 0 { + if isMargin { + sellOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + } else if isFutures { + sellOrder.ReduceOnly = false + } + } + } else if s.Position.IsShort() { + if isMargin { + buyOrder.MarginSideEffect = types.SideEffectTypeAutoRepay + sellOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + } else if isFutures { + buyOrder.ReduceOnly = true + sellOrder.ReduceOnly = false + } + + if s.Position.Base.Abs().Compare(buyOrder.Quantity) < 0 { + if isMargin { + sellOrder.MarginSideEffect = types.SideEffectTypeMarginBuy + } else if isFutures { + sellOrder.ReduceOnly = false + } + } + } + + // TODO: Move these to qty calculation + sellOrder = adjustOrderQuantity(sellOrder, s.Market) + buyOrder = adjustOrderQuantity(buyOrder, s.Market) + + return buyOrder, sellOrder +} + func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { // initial required information s.session = session @@ -517,37 +660,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Order qty sellQuantity, buyQuantity := s.getOrderQuantities(askPrice, bidPrice) - // TODO: Reduce only in margin and futures - sellOrder := types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Quantity: sellQuantity, - Price: askPrice, - Market: s.Market, - GroupID: s.groupID, - } - buyOrder := types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimitMaker, - Quantity: buyQuantity, - Price: bidPrice, - Market: s.Market, - GroupID: s.groupID, - } - - canBuy, canSell := s.getCanBuySell(midPrice) + buyOrder, sellOrder := s.getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice) - // TODO: check enough balance? + canBuy, canSell := s.getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice) // Submit orders var submitOrders []types.SubmitOrder if canSell { - submitOrders = append(submitOrders, adjustOrderQuantity(sellOrder, s.Market)) + submitOrders = append(submitOrders, sellOrder) } if canBuy { - submitOrders = append(submitOrders, adjustOrderQuantity(buyOrder, s.Market)) + submitOrders = append(submitOrders, buyOrder) } if len(submitOrders) == 0 { From 02a67a3de86d1cfe5fa913e933df3a4fde0a19d5 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 25 Nov 2022 12:38:28 +0800 Subject: [PATCH 0065/1392] strategy/linregmaker: initial trend --- pkg/strategy/linregmaker/strategy.go | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 141e39932a..a694dc5b39 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -25,7 +25,6 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) // TODO: Logic for backtest -// TODO: initial trend func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -586,6 +585,31 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se _ = s.ClosePosition(ctx, fixedpoint.NewFromFloat(1.0)) }) + // Initial trend + session.UserDataStream.OnStart(func() { + var closePrice fixedpoint.Value + if !bbgo.IsBackTesting { + ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + return + } + + closePrice = ticker.Buy.Add(ticker.Sell).Div(two) + } else { + if price, ok := session.LastPrice(s.Symbol); ok { + closePrice = price + } + } + priceReverseEMA := fixedpoint.NewFromFloat(s.ReverseEMA.Last()) + + // Main trend by ReverseEMA + if closePrice.Compare(priceReverseEMA) > 0 { + s.mainTrendCurrent = types.DirectionUp + } else if closePrice.Compare(priceReverseEMA) < 0 { + s.mainTrendCurrent = types.DirectionDown + } + }) + // Check trend reversal session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.ReverseInterval, func(kline types.KLine) { // closePrice is the close price of current kline From 71137620bd4c1299def81cb60dc38f79b0716b04 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 25 Nov 2022 16:39:15 +0800 Subject: [PATCH 0066/1392] strategy/linregmaker: qty calculation for backtest --- config/linregmaker.yaml | 4 +-- pkg/strategy/linregmaker/strategy.go | 52 ++++++++++++++++------------ 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 2a4f97233f..5b5e59db7f 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -126,7 +126,7 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [ -1, 1 ] + domain: [ -0.05, 0.05 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum range: [ 0, 0.1 ] @@ -140,7 +140,7 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [ 1, -1 ] + domain: [0.05, -0.05 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum range: [ 0, 0.1 ] diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index a694dc5b39..a3dc3fe4c6 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -24,7 +24,7 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) -// TODO: Logic for backtest +// TODO: Check logic of dynamic qty func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -303,6 +303,20 @@ func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoin return askPrice, bidPrice } +// adjustQuantity to meet the min notional and qty requirement +func (s *Strategy) adjustQuantity(quantity, price fixedpoint.Value) fixedpoint.Value { + adjustedQty := quantity + if quantity.Mul(price).Compare(s.Market.MinNotional) < 0 { + adjustedQty = bbgo.AdjustFloatQuantityByMinAmount(quantity, price, s.Market.MinNotional.Mul(notionModifier)) + } + + if adjustedQty.Compare(s.Market.MinQuantity) < 0 { + adjustedQty = fixedpoint.Max(adjustedQty, s.Market.MinQuantity) + } + + return adjustedQty +} + // getOrderQuantities returns sell and buy qty func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedpoint.Value) (sellQuantity fixedpoint.Value, buyQuantity fixedpoint.Value) { // Default @@ -357,6 +371,9 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } + buyQuantity = s.adjustQuantity(buyQuantity, bidPrice) + sellQuantity = s.adjustQuantity(sellQuantity, askPrice) + log.Infof("sell qty:%v buy qty: %v", sellQuantity, buyQuantity) return sellQuantity, buyQuantity @@ -373,12 +390,18 @@ func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] - isMargin := s.session.Margin || s.session.IsolatedMargin - isFutures := s.session.Futures || s.session.IsolatedFutures - - if isMargin { + if bbgo.IsBackTesting { + if !hasQuoteBalance { + baseQty = fixedpoint.Zero + quoteQty = fixedpoint.Zero + } else { + lastPrice, _ := s.session.LastPrice(s.Symbol) + baseQty = quoteBalance.Available.Div(lastPrice) + quoteQty = quoteBalance.Available + } + } else if s.session.Margin || s.session.IsolatedMargin { - } else if isFutures { + } else if s.session.Futures || s.session.IsolatedFutures { } else { if !hasBaseBalance { @@ -514,10 +537,6 @@ func (s *Strategy) getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice f } } - // TODO: Move these to qty calculation - sellOrder = adjustOrderQuantity(sellOrder, s.Market) - buyOrder = adjustOrderQuantity(buyOrder, s.Market) - return buyOrder, sellOrder } @@ -711,16 +730,3 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } - -// adjustOrderQuantity to meet the min notional and qty requirement -func adjustOrderQuantity(submitOrder types.SubmitOrder, market types.Market) types.SubmitOrder { - if submitOrder.Quantity.Mul(submitOrder.Price).Compare(market.MinNotional) < 0 { - submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, market.MinNotional.Mul(notionModifier)) - } - - if submitOrder.Quantity.Compare(market.MinQuantity) < 0 { - submitOrder.Quantity = fixedpoint.Max(submitOrder.Quantity, market.MinQuantity) - } - - return submitOrder -} From 50d5449b9aaa436df7268d160e81e3de4a0ab455 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 27 Nov 2022 00:24:24 +0800 Subject: [PATCH 0067/1392] fix types.NewZeroAssetError panic error --- pkg/bbgo/order_executor_general.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 6051d7d232..86243708f8 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -286,7 +286,7 @@ func (e *GeneralOrderExecutor) reduceQuantityAndSubmitOrder(ctx context.Context, submitOrder.Quantity = q if e.position.Market.IsDustQuantity(submitOrder.Quantity, price) { - return nil, types.NewZeroAssetError(nil) + return nil, types.NewZeroAssetError(fmt.Errorf("dust quantity")) } createdOrder, err2 := e.SubmitOrders(ctx, submitOrder) From 4b0db6b3affe079845c191232e67445b0ff45aa4 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 27 Nov 2022 00:25:29 +0800 Subject: [PATCH 0068/1392] bbgo: fix quantity adjustment --- pkg/bbgo/order_executor_general.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 86243708f8..40b63c9eee 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -271,7 +271,7 @@ func (e *GeneralOrderExecutor) reduceQuantityAndSubmitOrder(ctx context.Context, var err error for i := 0; i < submitOrderRetryLimit; i++ { q := submitOrder.Quantity.Mul(fixedpoint.One.Sub(quantityReduceDelta)) - if !e.session.Futures { + if !e.session.Futures && !e.session.Margin { if submitOrder.Side == types.SideTypeSell { if baseBalance, ok := e.session.GetAccount().Balance(e.position.Market.BaseCurrency); ok { q = fixedpoint.Min(q, baseBalance.Available) From 0cb9bc2c7b2b2d99019cc34490e702e16ac661c6 Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 29 Nov 2022 16:25:12 +0900 Subject: [PATCH 0069/1392] doc: add series extend documentation --- doc/development/series.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/doc/development/series.md b/doc/development/series.md index 08eec47112..0f23756fc3 100644 --- a/doc/development/series.md +++ b/doc/development/series.md @@ -41,3 +41,21 @@ var _ types.Series = &INDICATOR_TYPENAME{} ``` and if any of the method in the interface not been implemented, this would generate compile time error messages. + +#### Extended Series + +Instead of simple Series interface, we have `types.SeriesExtend` interface that enriches the functionality of `types.Series`. An indicator struct could simply be extended to `types.SeriesExtend` type by embedding anonymous struct `types.SeriesBase`, and instanced by `types.NewSeries()` function. The `types.SeriesExtend` interface binds commonly used functions, such as `Add`, `Reverse`, `Shfit`, `Covariance` and `Entropy`, to the original `types.Series` object. Please check [pkg/types/seriesbase_imp.go](../../pkg/types/seriesbase_imp.go) for the extendable functions. + +Example: + +```go +a := types.NewQueue(3) // types.Queue is a SeriesExtend container type that holds limit number of floats +a.Update(100.) +a.Update(200.) +a.Update(300.) +assert.Equal(t, a.Sum(3), 600.) +``` + +#### Cautions + +Avoid using `floats.Slice` to hold unlimited number of floats, unless you clean up the memory regularly. Manipulate large array of numbers will give huge impact on the computation speed due to long malloc/dealloc time. From 19d4033aaa26816295c9445966dc82390c39b8f6 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 30 Nov 2022 13:00:29 +0800 Subject: [PATCH 0070/1392] strategy/linregmaker: adj config --- config/linregmaker.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 5b5e59db7f..84bbc4950d 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -126,7 +126,7 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [ -0.05, 0.05 ] + domain: [ -0.02, 0.02 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum range: [ 0, 0.1 ] @@ -140,7 +140,7 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [0.05, -0.05 ] + domain: [0.02, -0.02 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum range: [ 0, 0.1 ] From 2e315240d93772931abdfbee716a2123cfa279c0 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 30 Nov 2022 12:41:25 +0900 Subject: [PATCH 0071/1392] feature: add sync_time.sh utility --- README.md | 17 +++++++++++++++++ scripts/sync_time.sh | 3 +++ 2 files changed, 20 insertions(+) create mode 100755 scripts/sync_time.sh diff --git a/README.md b/README.md index e73bd531f4..d7bad59f27 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,23 @@ bbgo pnl --exchange binance --asset BTC --since "2019-01-01" ## Advanced Configuration +### Synchronize System Time With Binance + +BBGO provides the script for UNIX systems/subsystems to synchronize date with Binance. `jq` and `bc` are required to be installed in previous. +To install the dependencies in Ubuntu, try the following commands: + +```bash +sudo apt install -y bc jq +``` + +And to synchronize the date, try: + +```bash +sudo ./scripts/sync_time.sh +``` + +You could also add the script to crontab so that the system time could get synchronized with Binance regularly. + ### Testnet (Paper Trading) Currently only supports binance testnet. To run bbgo in testnet, apply new API keys diff --git a/scripts/sync_time.sh b/scripts/sync_time.sh new file mode 100755 index 0000000000..c3765bf9f0 --- /dev/null +++ b/scripts/sync_time.sh @@ -0,0 +1,3 @@ +#!/usr/bin/sh + +date -s @`echo "$(curl https://api.binance.com/api/v3/time 2>/dev/null | jq .serverTime)/1000"|bc -l` From cb612a22b1adb361dab76039879859349dfee814 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Nov 2022 18:26:30 +0800 Subject: [PATCH 0072/1392] add grid2 strategy --- pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/grid2/strategy.go | 164 +++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 pkg/strategy/grid2/strategy.go diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 1c29ab684c..93334d9f0a 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -17,6 +17,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/fmaker" _ "github.com/c9s/bbgo/pkg/strategy/funding" _ "github.com/c9s/bbgo/pkg/strategy/grid" + _ "github.com/c9s/bbgo/pkg/strategy/grid2" _ "github.com/c9s/bbgo/pkg/strategy/harmonic" _ "github.com/c9s/bbgo/pkg/strategy/irr" _ "github.com/c9s/bbgo/pkg/strategy/kline" diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go new file mode 100644 index 0000000000..66613b53bb --- /dev/null +++ b/pkg/strategy/grid2/strategy.go @@ -0,0 +1,164 @@ +package grid2 + +import ( + "context" + "fmt" + "sync" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +const ID = "grid2" + +var log = logrus.WithField("strategy", ID) + +var notionalModifier = fixedpoint.NewFromFloat(1.0001) + +func init() { + // Register the pointer of the strategy struct, + // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) + // Note: built-in strategies need to imported manually in the bbgo cmd package. + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc + // This field will be injected automatically since we defined the Symbol field. + types.Market `json:"-" yaml:"-"` + + // These fields will be filled from the config file (it translates YAML to JSON) + Symbol string `json:"symbol" yaml:"symbol"` + + // ProfitSpread is the fixed profit spread you want to submit the sell order + ProfitSpread fixedpoint.Value `json:"profitSpread" yaml:"profitSpread"` + + // GridNum is the grid number, how many orders you want to post on the orderbook. + GridNum int64 `json:"gridNumber" yaml:"gridNumber"` + + UpperPrice fixedpoint.Value `json:"upperPrice" yaml:"upperPrice"` + + LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"` + + bbgo.QuantityOrAmount + + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + Position *types.Position `persistence:"position"` + + // orderStore is used to store all the created orders, so that we can filter the trades. + orderStore *bbgo.OrderStore + + // activeOrders is the locally maintained active order book of the maker orders. + activeOrders *bbgo.ActiveOrderBook + + tradeCollector *bbgo.TradeCollector + + // groupID is the group ID used for the strategy instance for canceling orders + groupID uint32 +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Validate() error { + if s.UpperPrice.IsZero() { + return errors.New("upperPrice can not be zero, you forgot to set?") + } + + if s.LowerPrice.IsZero() { + return errors.New("lowerPrice can not be zero, you forgot to set?") + } + + if s.UpperPrice.Compare(s.LowerPrice) <= 0 { + return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) + } + + if s.ProfitSpread.Sign() <= 0 { + // If profitSpread is empty or its value is negative + return fmt.Errorf("profit spread should bigger than 0") + } + + if s.GridNum == 0 { + return fmt.Errorf("gridNum can not be zero") + } + + if err := s.QuantityOrAmount.Validate(); err != nil { + return err + } + + return nil +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) +} + +// InstanceID returns the instance identifier from the current grid configuration parameters +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s-%s-%d-%d-%d", ID, s.Symbol, s.GridNum, s.UpperPrice.Int(), s.LowerPrice.Int()) +} + +func (s *Strategy) handleOrderFilled(o types.Order) { + +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + + s.groupID = util.FNV32(instanceID) + + log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + s.orderStore = bbgo.NewOrderStore(s.Symbol) + s.orderStore.BindStream(session.UserDataStream) + + // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeOrders.OnFilled(s.handleOrderFilled) + s.activeOrders.BindStream(session.UserDataStream) + + s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) + + s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { + bbgo.Notify(trade) + s.ProfitStats.AddTrade(trade) + }) + + s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + bbgo.Notify(position) + }) + + s.tradeCollector.BindStream(session.UserDataStream) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + bbgo.Sync(ctx, s) + + // now we can cancel the open orders + log.Infof("canceling active orders...") + if err := session.Exchange.CancelOrders(context.Background(), s.activeOrders.Orders()...); err != nil { + log.WithError(err).Errorf("cancel order error") + } + }) + + session.UserDataStream.OnStart(func() { + + }) + + return nil +} From 21a1d550e3f9315d1315e5aaddf74b7bfa120bba Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 3 Nov 2022 13:41:47 +0800 Subject: [PATCH 0073/1392] grid2: add grid struct --- pkg/strategy/grid2/grid.go | 102 ++++++++++++++++++++++++++++++++ pkg/strategy/grid2/grid_test.go | 81 +++++++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 pkg/strategy/grid2/grid.go create mode 100644 pkg/strategy/grid2/grid_test.go diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go new file mode 100644 index 0000000000..73c142e254 --- /dev/null +++ b/pkg/strategy/grid2/grid.go @@ -0,0 +1,102 @@ +package grid2 + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type Grid struct { + UpperPrice fixedpoint.Value `json:"upperPrice"` + LowerPrice fixedpoint.Value `json:"lowerPrice"` + + // Spread is the spread of each grid + Spread fixedpoint.Value `json:"spread"` + + // Size is the number of total grids + Size fixedpoint.Value `json:"size"` + + // Pins are the pinned grid prices, from low to high + Pins []fixedpoint.Value `json:"pins"` + + pinsCache map[fixedpoint.Value]struct{} `json:"-"` +} + +func NewGrid(lower, upper, density fixedpoint.Value) *Grid { + var height = upper - lower + var size = height.Div(density) + var pins []fixedpoint.Value + + for p := lower; p <= upper; p += size { + pins = append(pins, p) + } + + grid := &Grid{ + UpperPrice: upper, + LowerPrice: lower, + Size: density, + Spread: size, + Pins: pins, + pinsCache: make(map[fixedpoint.Value]struct{}, len(pins)), + } + grid.updatePinsCache() + return grid +} + +func (g *Grid) Above(price fixedpoint.Value) bool { + return price > g.UpperPrice +} + +func (g *Grid) Below(price fixedpoint.Value) bool { + return price < g.LowerPrice +} + +func (g *Grid) OutOfRange(price fixedpoint.Value) bool { + return price < g.LowerPrice || price > g.UpperPrice +} + +func (g *Grid) updatePinsCache() { + for _, pin := range g.Pins { + g.pinsCache[pin] = struct{}{} + } +} + +func (g *Grid) HasPin(pin fixedpoint.Value) (ok bool) { + _, ok = g.pinsCache[pin] + return ok +} + +func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []fixedpoint.Value) { + g.UpperPrice = upper + + // since the grid is extended, the size should be updated as well + g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() + + lastPin := g.Pins[len(g.Pins)-1] + for p := lastPin + g.Spread; p <= g.UpperPrice; p += g.Spread { + newPins = append(newPins, p) + } + + g.Pins = append(g.Pins, newPins...) + g.updatePinsCache() + return newPins +} + +func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []fixedpoint.Value) { + g.LowerPrice = lower + + // since the grid is extended, the size should be updated as well + g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() + + firstPin := g.Pins[0] + numToAdd := (firstPin - g.LowerPrice).Div(g.Spread).Floor() + if numToAdd == 0 { + return newPins + } + + for p := firstPin - g.Spread.Mul(numToAdd); p < firstPin; p += g.Spread { + newPins = append(newPins, p) + } + + g.Pins = append(newPins, g.Pins...) + g.updatePinsCache() + return newPins +} diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go new file mode 100644 index 0000000000..3ed9d0e4ed --- /dev/null +++ b/pkg/strategy/grid2/grid_test.go @@ -0,0 +1,81 @@ +package grid2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +func TestNewGrid(t *testing.T) { + upper := fixedpoint.NewFromFloat(500.0) + lower := fixedpoint.NewFromFloat(100.0) + size := fixedpoint.NewFromFloat(100.0) + grid := NewGrid(lower, upper, size) + assert.Equal(t, upper, grid.UpperPrice) + assert.Equal(t, lower, grid.LowerPrice) + assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) + if assert.Len(t, grid.Pins, 101) { + assert.Equal(t, fixedpoint.NewFromFloat(100.0), grid.Pins[0]) + assert.Equal(t, fixedpoint.NewFromFloat(500.0), grid.Pins[100]) + } +} + +func TestGrid_HasPin(t *testing.T) { + upper := fixedpoint.NewFromFloat(500.0) + lower := fixedpoint.NewFromFloat(100.0) + size := fixedpoint.NewFromFloat(100.0) + grid := NewGrid(lower, upper, size) + + assert.True(t, grid.HasPin(fixedpoint.NewFromFloat(100.0))) + assert.True(t, grid.HasPin(fixedpoint.NewFromFloat(500.0))) + assert.False(t, grid.HasPin(fixedpoint.NewFromFloat(101.0))) +} + +func TestGrid_ExtendUpperPrice(t *testing.T) { + upper := fixedpoint.NewFromFloat(500.0) + lower := fixedpoint.NewFromFloat(100.0) + size := fixedpoint.NewFromFloat(100.0) + grid := NewGrid(lower, upper, size) + + originalSpread := grid.Spread + newPins := grid.ExtendUpperPrice(fixedpoint.NewFromFloat(1000.0)) + assert.Equal(t, originalSpread, grid.Spread) + assert.Len(t, newPins, 125) // (1000-500) / 4 + assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) + if assert.Len(t, grid.Pins, 226) { + assert.Equal(t, fixedpoint.NewFromFloat(100.0), grid.Pins[0]) + assert.Equal(t, fixedpoint.NewFromFloat(1000.0), grid.Pins[225]) + } +} + +func TestGrid_ExtendLowerPrice(t *testing.T) { + upper := fixedpoint.NewFromFloat(3000.0) + lower := fixedpoint.NewFromFloat(2000.0) + size := fixedpoint.NewFromFloat(100.0) + grid := NewGrid(lower, upper, size) + + // spread = (3000 - 2000) / 100.0 + expectedSpread := fixedpoint.NewFromFloat(10.0) + assert.Equal(t, expectedSpread, grid.Spread) + + originalSpread := grid.Spread + newPins := grid.ExtendLowerPrice(fixedpoint.NewFromFloat(1000.0)) + assert.Equal(t, originalSpread, grid.Spread) + + // 100 = (2000-1000) / 10 + if assert.Len(t, newPins, 100) { + assert.Equal(t, fixedpoint.NewFromFloat(2000.0)-expectedSpread, newPins[99]) + } + + assert.Equal(t, expectedSpread, grid.Spread) + if assert.Len(t, grid.Pins, 201) { + assert.Equal(t, fixedpoint.NewFromFloat(1000.0), grid.Pins[0]) + assert.Equal(t, fixedpoint.NewFromFloat(3000.0), grid.Pins[200]) + } + + newPins2 := grid.ExtendLowerPrice( + fixedpoint.NewFromFloat(1000.0 - 1.0)) + assert.Len(t, newPins2, 0) // should have no new pin generated +} From 2761cff2bf126866ff53830f89c56f59dc991a5b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 4 Nov 2022 00:26:28 +0800 Subject: [PATCH 0074/1392] grid2: add pin tests --- pkg/strategy/grid2/grid.go | 77 +++++++++++++++++---------- pkg/strategy/grid2/grid_test.go | 93 +++++++++++++++++++++++++++++---- pkg/types/market.go | 4 +- pkg/types/market_test.go | 10 ++-- 4 files changed, 138 insertions(+), 46 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 73c142e254..054e8f17c0 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -1,6 +1,8 @@ package grid2 import ( + "math" + "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -15,64 +17,83 @@ type Grid struct { Size fixedpoint.Value `json:"size"` // Pins are the pinned grid prices, from low to high - Pins []fixedpoint.Value `json:"pins"` + Pins []Pin `json:"pins"` - pinsCache map[fixedpoint.Value]struct{} `json:"-"` + pinsCache map[Pin]struct{} `json:"-"` } -func NewGrid(lower, upper, density fixedpoint.Value) *Grid { - var height = upper - lower - var size = height.Div(density) - var pins []fixedpoint.Value +type Pin fixedpoint.Value + +func calculateArithmeticPins(lower, upper, size, tickSize fixedpoint.Value) []Pin { + var height = upper.Sub(lower) + var spread = height.Div(size) + + var pins []Pin + for p := lower; p.Compare(upper) <= 0; p = p.Add(spread) { + // tickSize here = 0.01 + pp := math.Trunc(p.Float64()/tickSize.Float64()) * tickSize.Float64() + pins = append(pins, Pin(fixedpoint.NewFromFloat(pp))) + } + + return pins +} - for p := lower; p <= upper; p += size { - pins = append(pins, p) +func buildPinCache(pins []Pin) map[Pin]struct{} { + cache := make(map[Pin]struct{}, len(pins)) + for _, pin := range pins { + cache[pin] = struct{}{} } + return cache +} + +func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { + var height = upper.Sub(lower) + var spread = height.Div(size) + var pins = calculateArithmeticPins(lower, upper, size, tickSize) + grid := &Grid{ UpperPrice: upper, LowerPrice: lower, - Size: density, - Spread: size, + Size: size, + Spread: spread, Pins: pins, - pinsCache: make(map[fixedpoint.Value]struct{}, len(pins)), + pinsCache: buildPinCache(pins), } - grid.updatePinsCache() + return grid } func (g *Grid) Above(price fixedpoint.Value) bool { - return price > g.UpperPrice + return price.Compare(g.UpperPrice) > 0 } func (g *Grid) Below(price fixedpoint.Value) bool { - return price < g.LowerPrice + return price.Compare(g.LowerPrice) < 0 } func (g *Grid) OutOfRange(price fixedpoint.Value) bool { - return price < g.LowerPrice || price > g.UpperPrice + return price.Compare(g.LowerPrice) < 0 || price.Compare(g.UpperPrice) > 0 } func (g *Grid) updatePinsCache() { - for _, pin := range g.Pins { - g.pinsCache[pin] = struct{}{} - } + g.pinsCache = buildPinCache(g.Pins) } -func (g *Grid) HasPin(pin fixedpoint.Value) (ok bool) { +func (g *Grid) HasPin(pin Pin) (ok bool) { _, ok = g.pinsCache[pin] return ok } -func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []fixedpoint.Value) { +func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { g.UpperPrice = upper // since the grid is extended, the size should be updated as well g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() - lastPin := g.Pins[len(g.Pins)-1] - for p := lastPin + g.Spread; p <= g.UpperPrice; p += g.Spread { - newPins = append(newPins, p) + lastPinPrice := fixedpoint.Value(g.Pins[len(g.Pins)-1]) + for p := lastPinPrice.Add(g.Spread); p <= g.UpperPrice; p += g.Spread { + newPins = append(newPins, Pin(p)) } g.Pins = append(g.Pins, newPins...) @@ -80,20 +101,20 @@ func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []fixedpoint.Va return newPins } -func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []fixedpoint.Value) { +func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) { g.LowerPrice = lower // since the grid is extended, the size should be updated as well g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() - firstPin := g.Pins[0] - numToAdd := (firstPin - g.LowerPrice).Div(g.Spread).Floor() + firstPinPrice := fixedpoint.Value(g.Pins[0]) + numToAdd := (firstPinPrice.Sub(g.LowerPrice)).Div(g.Spread).Floor() if numToAdd == 0 { return newPins } - for p := firstPin - g.Spread.Mul(numToAdd); p < firstPin; p += g.Spread { - newPins = append(newPins, p) + for p := firstPinPrice.Sub(g.Spread.Mul(numToAdd)); p.Compare(firstPinPrice) < 0; p = p.Add(g.Spread) { + newPins = append(newPins, Pin(p)) } g.Pins = append(newPins, g.Pins...) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 3ed9d0e4ed..12992ea8a7 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -8,11 +8,20 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) +func number(a interface{}) fixedpoint.Value { + if s, ok := a.(string); ok { + return fixedpoint.MustNewFromString(s) + } + + f := a.(float64) + return fixedpoint.NewFromFloat(f) +} + func TestNewGrid(t *testing.T) { upper := fixedpoint.NewFromFloat(500.0) lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size) + grid := NewGrid(lower, upper, size, number(2.0)) assert.Equal(t, upper, grid.UpperPrice) assert.Equal(t, lower, grid.LowerPrice) assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) @@ -26,21 +35,21 @@ func TestGrid_HasPin(t *testing.T) { upper := fixedpoint.NewFromFloat(500.0) lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size) + grid := NewGrid(lower, upper, size, number(2)) - assert.True(t, grid.HasPin(fixedpoint.NewFromFloat(100.0))) - assert.True(t, grid.HasPin(fixedpoint.NewFromFloat(500.0))) - assert.False(t, grid.HasPin(fixedpoint.NewFromFloat(101.0))) + assert.True(t, grid.HasPin(Pin(number(100.0)))) + assert.True(t, grid.HasPin(Pin(number(500.0)))) + assert.False(t, grid.HasPin(Pin(number(101.0)))) } func TestGrid_ExtendUpperPrice(t *testing.T) { - upper := fixedpoint.NewFromFloat(500.0) - lower := fixedpoint.NewFromFloat(100.0) - size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size) + upper := number(500.0) + lower := number(100.0) + size := number(100.0) + grid := NewGrid(lower, upper, size, number(2.0)) originalSpread := grid.Spread - newPins := grid.ExtendUpperPrice(fixedpoint.NewFromFloat(1000.0)) + newPins := grid.ExtendUpperPrice(number(1000.0)) assert.Equal(t, originalSpread, grid.Spread) assert.Len(t, newPins, 125) // (1000-500) / 4 assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) @@ -54,7 +63,7 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { upper := fixedpoint.NewFromFloat(3000.0) lower := fixedpoint.NewFromFloat(2000.0) size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size) + grid := NewGrid(lower, upper, size, number(2.0)) // spread = (3000 - 2000) / 100.0 expectedSpread := fixedpoint.NewFromFloat(10.0) @@ -79,3 +88,65 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { fixedpoint.NewFromFloat(1000.0 - 1.0)) assert.Len(t, newPins2, 0) // should have no new pin generated } + +func Test_calculateArithmeticPins(t *testing.T) { + type args struct { + lower fixedpoint.Value + upper fixedpoint.Value + size fixedpoint.Value + tickSize fixedpoint.Value + } + tests := []struct { + name string + args args + want []Pin + }{ + { + name: "simple", + args: args{ + lower: number(1000.0), + upper: number(3000.0), + size: number(30.0), + tickSize: number(0.01), + }, + want: []Pin{ + Pin(number(1000.0)), + Pin(number(1066.660)), + Pin(number(1133.330)), + Pin(number(1199.990)), + Pin(number(1266.660)), + Pin(number(1333.330)), + Pin(number(1399.990)), + Pin(number(1466.660)), + Pin(number(1533.330)), + Pin(number(1599.990)), + Pin(number(1666.660)), + Pin(number(1733.330)), + Pin(number(1799.990)), + Pin(number(1866.660)), + Pin(number(1933.330)), + Pin(number(1999.990)), + Pin(number(2066.660)), + Pin(number(2133.330)), + Pin(number("2199.99")), + Pin(number(2266.660)), + Pin(number(2333.330)), + Pin(number("2399.99")), + Pin(number(2466.660)), + Pin(number(2533.330)), + Pin(number("2599.99")), + Pin(number(2666.660)), + Pin(number(2733.330)), + Pin(number(2799.990)), + Pin(number(2866.660)), + Pin(number(2933.330)), + Pin(number(2999.990)), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, calculateArithmeticPins(tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize), "calculateArithmeticPins(%v, %v, %v, %v)", tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize) + }) + } +} diff --git a/pkg/types/market.go b/pkg/types/market.go index 1092b441ef..e2b08cd211 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -150,10 +150,10 @@ func (m Market) FormatPriceCurrency(val fixedpoint.Value) string { func (m Market) FormatPrice(val fixedpoint.Value) string { // p := math.Pow10(m.PricePrecision) - return formatPrice(val, m.TickSize) + return FormatPrice(val, m.TickSize) } -func formatPrice(price fixedpoint.Value, tickSize fixedpoint.Value) string { +func FormatPrice(price fixedpoint.Value, tickSize fixedpoint.Value) string { prec := int(math.Round(math.Abs(math.Log10(tickSize.Float64())))) return price.FormatString(prec) } diff --git a/pkg/types/market_test.go b/pkg/types/market_test.go index d0544e9ba3..809e60b0d4 100644 --- a/pkg/types/market_test.go +++ b/pkg/types/market_test.go @@ -26,12 +26,12 @@ func TestFormatQuantity(t *testing.T) { } func TestFormatPrice(t *testing.T) { - price := formatPrice( + price := FormatPrice( s("26.288256"), s("0.0001")) assert.Equal(t, "26.2882", price) - price = formatPrice(s("26.288656"), s("0.001")) + price = FormatPrice(s("26.288656"), s("0.001")) assert.Equal(t, "26.288", price) } @@ -78,7 +78,7 @@ func TestDurationParse(t *testing.T) { } } -func Test_formatPrice(t *testing.T) { +func Test_FormatPrice(t *testing.T) { type args struct { price fixedpoint.Value tickSize fixedpoint.Value @@ -125,9 +125,9 @@ func Test_formatPrice(t *testing.T) { binanceFormatRE := regexp.MustCompile("^([0-9]{1,20})(.[0-9]{1,20})?$") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := formatPrice(tt.args.price, tt.args.tickSize) + got := FormatPrice(tt.args.price, tt.args.tickSize) if got != tt.want { - t.Errorf("formatPrice() = %v, want %v", got, tt.want) + t.Errorf("FormatPrice() = %v, want %v", got, tt.want) } assert.Regexp(t, binanceFormatRE, got) From e675a084e2c1ddc61ee55e903a64977b2429e486 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Nov 2022 10:06:57 +0800 Subject: [PATCH 0075/1392] grid2: refactor spread, height methods --- pkg/strategy/grid2/grid.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 054e8f17c0..555223fab1 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -10,9 +10,6 @@ type Grid struct { UpperPrice fixedpoint.Value `json:"upperPrice"` LowerPrice fixedpoint.Value `json:"lowerPrice"` - // Spread is the spread of each grid - Spread fixedpoint.Value `json:"spread"` - // Size is the number of total grids Size fixedpoint.Value `json:"size"` @@ -48,15 +45,12 @@ func buildPinCache(pins []Pin) map[Pin]struct{} { } func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { - var height = upper.Sub(lower) - var spread = height.Div(size) var pins = calculateArithmeticPins(lower, upper, size, tickSize) grid := &Grid{ UpperPrice: upper, LowerPrice: lower, Size: size, - Spread: spread, Pins: pins, pinsCache: buildPinCache(pins), } @@ -64,6 +58,15 @@ func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { return grid } +func (g *Grid) Height() fixedpoint.Value { + return g.UpperPrice.Sub(g.LowerPrice) +} + +// Spread returns the spread of each grid +func (g *Grid) Spread() fixedpoint.Value { + return g.Height().Div(g.Size) +} + func (g *Grid) Above(price fixedpoint.Value) bool { return price.Compare(g.UpperPrice) > 0 } @@ -89,10 +92,11 @@ func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { g.UpperPrice = upper // since the grid is extended, the size should be updated as well - g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() + spread := g.Spread() + g.Size = (g.UpperPrice - g.LowerPrice).Div(spread).Floor() lastPinPrice := fixedpoint.Value(g.Pins[len(g.Pins)-1]) - for p := lastPinPrice.Add(g.Spread); p <= g.UpperPrice; p += g.Spread { + for p := lastPinPrice.Add(spread); p <= g.UpperPrice; p = p.Add(spread) { newPins = append(newPins, Pin(p)) } @@ -102,18 +106,22 @@ func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { } func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) { + spread := g.Spread() + g.LowerPrice = lower + height := g.Height() + // since the grid is extended, the size should be updated as well - g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() + g.Size = height.Div(spread).Floor() firstPinPrice := fixedpoint.Value(g.Pins[0]) - numToAdd := (firstPinPrice.Sub(g.LowerPrice)).Div(g.Spread).Floor() + numToAdd := (firstPinPrice.Sub(g.LowerPrice)).Div(spread).Floor() if numToAdd == 0 { return newPins } - for p := firstPinPrice.Sub(g.Spread.Mul(numToAdd)); p.Compare(firstPinPrice) < 0; p = p.Add(g.Spread) { + for p := firstPinPrice.Sub(spread.Mul(numToAdd)); p.Compare(firstPinPrice) < 0; p = p.Add(spread) { newPins = append(newPins, Pin(p)) } From d6f751c027b6eca0ab5d958fe1819424ff06caf3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Nov 2022 10:12:50 +0800 Subject: [PATCH 0076/1392] grid2: improve ExtendLowerPrice --- pkg/strategy/grid2/grid.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 555223fab1..7e5d78aec2 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -2,6 +2,7 @@ package grid2 import ( "math" + "sort" "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -108,24 +109,23 @@ func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) { spread := g.Spread() - g.LowerPrice = lower - - height := g.Height() + startPrice := g.LowerPrice.Sub(spread) + for p := startPrice; p.Compare(lower) >= 0; p = p.Sub(spread) { + newPins = append(newPins, Pin(p)) + } - // since the grid is extended, the size should be updated as well - g.Size = height.Div(spread).Floor() + g.addPins(newPins) + return newPins +} - firstPinPrice := fixedpoint.Value(g.Pins[0]) - numToAdd := (firstPinPrice.Sub(g.LowerPrice)).Div(spread).Floor() - if numToAdd == 0 { - return newPins - } +func (g *Grid) addPins(pins []Pin) { + g.Pins = append(g.Pins, pins...) - for p := firstPinPrice.Sub(spread.Mul(numToAdd)); p.Compare(firstPinPrice) < 0; p = p.Add(spread) { - newPins = append(newPins, Pin(p)) - } + sort.Slice(g.Pins, func(i, j int) bool { + a := fixedpoint.Value(g.Pins[i]) + b := fixedpoint.Value(g.Pins[j]) + return a.Compare(b) < 0 + }) - g.Pins = append(newPins, g.Pins...) g.updatePinsCache() - return newPins } From 533587ffd2091f1be77f640d9cecf51dd4cc391d Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Nov 2022 10:13:13 +0800 Subject: [PATCH 0077/1392] grid2: update lowerPrice --- pkg/strategy/grid2/grid.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 7e5d78aec2..bc11d99b2a 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -114,6 +114,7 @@ func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) { newPins = append(newPins, Pin(p)) } + g.LowerPrice = lower g.addPins(newPins) return newPins } From 725c624281d253c2272c904f64a3648176fd022b Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Nov 2022 10:14:07 +0800 Subject: [PATCH 0078/1392] grid2: rewrite ExtendUpperPrice --- pkg/strategy/grid2/grid.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index bc11d99b2a..c6670228d0 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -90,19 +90,15 @@ func (g *Grid) HasPin(pin Pin) (ok bool) { } func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { - g.UpperPrice = upper - - // since the grid is extended, the size should be updated as well spread := g.Spread() - g.Size = (g.UpperPrice - g.LowerPrice).Div(spread).Floor() - lastPinPrice := fixedpoint.Value(g.Pins[len(g.Pins)-1]) - for p := lastPinPrice.Add(spread); p <= g.UpperPrice; p = p.Add(spread) { + startPrice := g.UpperPrice.Add(spread) + for p := startPrice; p.Compare(upper) <= 0; p = p.Add(spread) { newPins = append(newPins, Pin(p)) } - g.Pins = append(g.Pins, newPins...) - g.updatePinsCache() + g.UpperPrice = upper + g.addPins(newPins) return newPins } From 75c088eb9c1db4b370d209ed81742a8105a1ee1f Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 7 Nov 2022 13:51:44 +0800 Subject: [PATCH 0079/1392] refactor calculateArithmeticPins --- pkg/strategy/grid2/grid.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index c6670228d0..baed325f2a 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -14,6 +14,9 @@ type Grid struct { // Size is the number of total grids Size fixedpoint.Value `json:"size"` + // TickSize is the price tick size, this is used for truncating price + TickSize fixedpoint.Value `json:"tickSize"` + // Pins are the pinned grid prices, from low to high Pins []Pin `json:"pins"` @@ -22,10 +25,7 @@ type Grid struct { type Pin fixedpoint.Value -func calculateArithmeticPins(lower, upper, size, tickSize fixedpoint.Value) []Pin { - var height = upper.Sub(lower) - var spread = height.Div(size) - +func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) []Pin { var pins []Pin for p := lower; p.Compare(upper) <= 0; p = p.Add(spread) { // tickSize here = 0.01 @@ -46,16 +46,16 @@ func buildPinCache(pins []Pin) map[Pin]struct{} { } func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { - var pins = calculateArithmeticPins(lower, upper, size, tickSize) - grid := &Grid{ UpperPrice: upper, LowerPrice: lower, Size: size, - Pins: pins, - pinsCache: buildPinCache(pins), + TickSize: tickSize, } + var spread = grid.Spread() + var pins = calculateArithmeticPins(lower, upper, spread, tickSize) + grid.addPins(pins) return grid } @@ -80,10 +80,6 @@ func (g *Grid) OutOfRange(price fixedpoint.Value) bool { return price.Compare(g.LowerPrice) < 0 || price.Compare(g.UpperPrice) > 0 } -func (g *Grid) updatePinsCache() { - g.pinsCache = buildPinCache(g.Pins) -} - func (g *Grid) HasPin(pin Pin) (ok bool) { _, ok = g.pinsCache[pin] return ok @@ -126,3 +122,7 @@ func (g *Grid) addPins(pins []Pin) { g.updatePinsCache() } + +func (g *Grid) updatePinsCache() { + g.pinsCache = buildPinCache(g.Pins) +} From f98c00b7aa46deb30db00db165bd9d44ee0f0484 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Nov 2022 16:14:04 +0800 Subject: [PATCH 0080/1392] grid2: fix extendLowerPrice method and tests --- pkg/strategy/grid2/grid.go | 40 +++++++++++++++------------- pkg/strategy/grid2/grid_test.go | 46 +++++++++++++++++---------------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index baed325f2a..d075ed2bec 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -17,6 +17,9 @@ type Grid struct { // TickSize is the price tick size, this is used for truncating price TickSize fixedpoint.Value `json:"tickSize"` + // Spread is a immutable number + Spread fixedpoint.Value `json:"spread"` + // Pins are the pinned grid prices, from low to high Pins []Pin `json:"pins"` @@ -46,14 +49,17 @@ func buildPinCache(pins []Pin) map[Pin]struct{} { } func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { + height := upper.Sub(lower) + spread := height.Div(size) + grid := &Grid{ UpperPrice: upper, LowerPrice: lower, Size: size, TickSize: tickSize, + Spread: spread, } - var spread = grid.Spread() var pins = calculateArithmeticPins(lower, upper, spread, tickSize) grid.addPins(pins) return grid @@ -63,11 +69,6 @@ func (g *Grid) Height() fixedpoint.Value { return g.UpperPrice.Sub(g.LowerPrice) } -// Spread returns the spread of each grid -func (g *Grid) Spread() fixedpoint.Value { - return g.Height().Div(g.Size) -} - func (g *Grid) Above(price fixedpoint.Value) bool { return price.Compare(g.UpperPrice) > 0 } @@ -86,31 +87,34 @@ func (g *Grid) HasPin(pin Pin) (ok bool) { } func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { - spread := g.Spread() - - startPrice := g.UpperPrice.Add(spread) - for p := startPrice; p.Compare(upper) <= 0; p = p.Add(spread) { - newPins = append(newPins, Pin(p)) - } - + newPins = calculateArithmeticPins(g.UpperPrice, upper, g.Spread, g.TickSize) g.UpperPrice = upper g.addPins(newPins) return newPins } func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) { - spread := g.Spread() - - startPrice := g.LowerPrice.Sub(spread) - for p := startPrice; p.Compare(lower) >= 0; p = p.Sub(spread) { - newPins = append(newPins, Pin(p)) + if lower.Compare(g.LowerPrice) >= 0 { + return nil } + n := g.LowerPrice.Sub(lower).Div(g.Spread).Floor() + lower = g.LowerPrice.Sub(g.Spread.Mul(n)) + newPins = calculateArithmeticPins(lower, g.LowerPrice.Sub(g.Spread), g.Spread, g.TickSize) + g.LowerPrice = lower g.addPins(newPins) return newPins } +func (g *Grid) TopPin() Pin { + return g.Pins[len(g.Pins)-1] +} + +func (g *Grid) BottomPin() Pin { + return g.Pins[0] +} + func (g *Grid) addPins(pins []Pin) { g.Pins = append(g.Pins, pins...) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 12992ea8a7..df89c6b53a 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -45,48 +45,50 @@ func TestGrid_HasPin(t *testing.T) { func TestGrid_ExtendUpperPrice(t *testing.T) { upper := number(500.0) lower := number(100.0) - size := number(100.0) - grid := NewGrid(lower, upper, size, number(2.0)) - + size := number(40.0) + grid := NewGrid(lower, upper, size, number(0.01)) originalSpread := grid.Spread + + assert.Equal(t, number(10.0), originalSpread) + assert.Len(t, grid.Pins, 40) // (1000-500) / 4 + newPins := grid.ExtendUpperPrice(number(1000.0)) assert.Equal(t, originalSpread, grid.Spread) - assert.Len(t, newPins, 125) // (1000-500) / 4 - assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) - if assert.Len(t, grid.Pins, 226) { - assert.Equal(t, fixedpoint.NewFromFloat(100.0), grid.Pins[0]) - assert.Equal(t, fixedpoint.NewFromFloat(1000.0), grid.Pins[225]) - } + assert.Len(t, newPins, 51) // (1000-500) / 4 } func TestGrid_ExtendLowerPrice(t *testing.T) { upper := fixedpoint.NewFromFloat(3000.0) lower := fixedpoint.NewFromFloat(2000.0) - size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size, number(2.0)) + size := fixedpoint.NewFromFloat(10.0) + grid := NewGrid(lower, upper, size, number(0.01)) + + assert.Equal(t, Pin(number(2000.0)), grid.BottomPin(), "bottom pin should be 1000.0") + assert.Equal(t, Pin(number(3000.0)), grid.TopPin(), "top pin should be 3000.0") + assert.Len(t, grid.Pins, 11) - // spread = (3000 - 2000) / 100.0 - expectedSpread := fixedpoint.NewFromFloat(10.0) + // spread = (3000 - 2000) / 10.0 + expectedSpread := fixedpoint.NewFromFloat(100.0) assert.Equal(t, expectedSpread, grid.Spread) originalSpread := grid.Spread newPins := grid.ExtendLowerPrice(fixedpoint.NewFromFloat(1000.0)) assert.Equal(t, originalSpread, grid.Spread) + t.Logf("newPins: %+v", newPins) + // 100 = (2000-1000) / 10 - if assert.Len(t, newPins, 100) { - assert.Equal(t, fixedpoint.NewFromFloat(2000.0)-expectedSpread, newPins[99]) + if assert.Len(t, newPins, 10) { + assert.Equal(t, Pin(number(1000.0)), newPins[0]) + assert.Equal(t, Pin(number(1900.0)), newPins[len(newPins)-1]) } assert.Equal(t, expectedSpread, grid.Spread) - if assert.Len(t, grid.Pins, 201) { - assert.Equal(t, fixedpoint.NewFromFloat(1000.0), grid.Pins[0]) - assert.Equal(t, fixedpoint.NewFromFloat(3000.0), grid.Pins[200]) - } - newPins2 := grid.ExtendLowerPrice( - fixedpoint.NewFromFloat(1000.0 - 1.0)) - assert.Len(t, newPins2, 0) // should have no new pin generated + if assert.Len(t, grid.Pins, 21) { + assert.Equal(t, Pin(number(1000.0)), grid.BottomPin(), "bottom pin should be 1000.0") + assert.Equal(t, Pin(number(3000.0)), grid.TopPin(), "top pin should be 3000.0") + } } func Test_calculateArithmeticPins(t *testing.T) { From 4ddbeff7e43f0c7edd1bf721681fa3068bce1675 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Nov 2022 18:11:04 +0800 Subject: [PATCH 0081/1392] grid2: fix Test_calculateArithmeticPins --- pkg/strategy/grid2/grid_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index df89c6b53a..c04d76d595 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -104,6 +104,7 @@ func Test_calculateArithmeticPins(t *testing.T) { want []Pin }{ { + // (3000-1000)/30 = 66.6666666 name: "simple", args: args{ lower: number(1000.0), @@ -148,7 +149,8 @@ func Test_calculateArithmeticPins(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, calculateArithmeticPins(tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize), "calculateArithmeticPins(%v, %v, %v, %v)", tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize) + spread := tt.args.upper.Sub(tt.args.lower).Div(tt.args.size) + assert.Equalf(t, tt.want, calculateArithmeticPins(tt.args.lower, tt.args.upper, spread, tt.args.tickSize), "calculateArithmeticPins(%v, %v, %v, %v)", tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize) }) } } From f46fc7ee804be793b920436a0c0e346998a64b3a Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Nov 2022 20:09:07 +0800 Subject: [PATCH 0082/1392] grid2: fix tests --- pkg/strategy/grid2/grid_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index c04d76d595..8a5ad4d03d 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -26,8 +26,8 @@ func TestNewGrid(t *testing.T) { assert.Equal(t, lower, grid.LowerPrice) assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) if assert.Len(t, grid.Pins, 101) { - assert.Equal(t, fixedpoint.NewFromFloat(100.0), grid.Pins[0]) - assert.Equal(t, fixedpoint.NewFromFloat(500.0), grid.Pins[100]) + assert.Equal(t, Pin(number(100.0)), grid.Pins[0]) + assert.Equal(t, Pin(number(500.0)), grid.Pins[100]) } } From 4fb2230e5dd5744830ab5ba786863d52de1cb8df Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Nov 2022 20:23:25 +0800 Subject: [PATCH 0083/1392] grid2: improve number func --- pkg/strategy/grid2/grid_test.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 8a5ad4d03d..6568bdcc7c 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -9,19 +9,25 @@ import ( ) func number(a interface{}) fixedpoint.Value { - if s, ok := a.(string); ok { - return fixedpoint.MustNewFromString(s) + switch v := a.(type) { + case string: + return fixedpoint.MustNewFromString(v) + case int: + return fixedpoint.NewFromInt(int64(v)) + case int64: + return fixedpoint.NewFromInt(int64(v)) + case float64: + return fixedpoint.NewFromFloat(v) } - f := a.(float64) - return fixedpoint.NewFromFloat(f) + return fixedpoint.Zero } func TestNewGrid(t *testing.T) { upper := fixedpoint.NewFromFloat(500.0) lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size, number(2.0)) + grid := NewGrid(lower, upper, size, number(0.01)) assert.Equal(t, upper, grid.UpperPrice) assert.Equal(t, lower, grid.LowerPrice) assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) @@ -35,7 +41,7 @@ func TestGrid_HasPin(t *testing.T) { upper := fixedpoint.NewFromFloat(500.0) lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size, number(2)) + grid := NewGrid(lower, upper, size, number(0.01)) assert.True(t, grid.HasPin(Pin(number(100.0)))) assert.True(t, grid.HasPin(Pin(number(500.0)))) From 629cea0f44b45eb5590aa661a4e4f54c41e01ce4 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Nov 2022 20:27:55 +0800 Subject: [PATCH 0084/1392] grid2: fix ExtendUpperPrice and its tests --- pkg/strategy/grid2/grid.go | 6 +++++- pkg/strategy/grid2/grid_test.go | 11 +++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index d075ed2bec..35633f6336 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -87,7 +87,11 @@ func (g *Grid) HasPin(pin Pin) (ok bool) { } func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { - newPins = calculateArithmeticPins(g.UpperPrice, upper, g.Spread, g.TickSize) + if upper.Compare(g.UpperPrice) <= 0 { + return nil + } + + newPins = calculateArithmeticPins(g.UpperPrice.Add(g.Spread), upper, g.Spread, g.TickSize) g.UpperPrice = upper g.addPins(newPins) return newPins diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 6568bdcc7c..18e5c8095f 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -51,16 +51,19 @@ func TestGrid_HasPin(t *testing.T) { func TestGrid_ExtendUpperPrice(t *testing.T) { upper := number(500.0) lower := number(100.0) - size := number(40.0) + size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) originalSpread := grid.Spread - assert.Equal(t, number(10.0), originalSpread) - assert.Len(t, grid.Pins, 40) // (1000-500) / 4 + t.Logf("pins: %+v", grid.Pins) + assert.Equal(t, number(100.0), originalSpread) + assert.Len(t, grid.Pins, 5) // (1000-500) / 4 newPins := grid.ExtendUpperPrice(number(1000.0)) + assert.Len(t, grid.Pins, 10) + assert.Len(t, newPins, 5) assert.Equal(t, originalSpread, grid.Spread) - assert.Len(t, newPins, 51) // (1000-500) / 4 + t.Logf("pins: %+v", grid.Pins) } func TestGrid_ExtendLowerPrice(t *testing.T) { From 84c3d386ca48a117bdac78837647a3369e63c119 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Nov 2022 16:20:40 +0800 Subject: [PATCH 0085/1392] grid2: implement find next higher/lower pin --- pkg/strategy/grid2/grid.go | 28 +++++++++++++++++++++++++ pkg/strategy/grid2/grid_test.go | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 35633f6336..20aa541b21 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -86,6 +86,34 @@ func (g *Grid) HasPin(pin Pin) (ok bool) { return ok } +// NextHigherPin finds the next higher pin +func (g *Grid) NextHigherPin(price fixedpoint.Value) (Pin, bool) { + i := g.SearchPin(price) + if i < len(g.Pins) && fixedpoint.Value(g.Pins[i]).Compare(price) == 0 && i+1 < len(g.Pins) { + return g.Pins[i+1], true + } + + return Pin(fixedpoint.Zero), false +} + +// NextLowerPin finds the next lower pin +func (g *Grid) NextLowerPin(price fixedpoint.Value) (Pin, bool) { + i := g.SearchPin(price) + if i < len(g.Pins) && fixedpoint.Value(g.Pins[i]).Compare(price) == 0 && i-1 >= 0 { + return g.Pins[i-1], true + } + + return Pin(fixedpoint.Zero), false +} + +func (g *Grid) SearchPin(price fixedpoint.Value) int { + i := sort.Search(len(g.Pins), func(i int) bool { + a := fixedpoint.Value(g.Pins[i]) + return a.Compare(price) >= 0 + }) + return i +} + func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { if upper.Compare(g.UpperPrice) <= 0 { return nil diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 18e5c8095f..742c2116f1 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -100,6 +100,42 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { } } +func TestGrid_NextLowerPin(t *testing.T) { + upper := number(500.0) + lower := number(100.0) + size := number(4.0) + grid := NewGrid(lower, upper, size, number(0.01)) + t.Logf("pins: %+v", grid.Pins) + + next, ok := grid.NextLowerPin(number(200.0)) + assert.True(t, ok) + assert.Equal(t, Pin(number(100.0)), next) + + next, ok = grid.NextLowerPin(number(150.0)) + assert.False(t, ok) + assert.Equal(t, Pin(fixedpoint.Zero), next) +} + +func TestGrid_NextHigherPin(t *testing.T) { + upper := number(500.0) + lower := number(100.0) + size := number(4.0) + grid := NewGrid(lower, upper, size, number(0.01)) + t.Logf("pins: %+v", grid.Pins) + + next, ok := grid.NextHigherPin(number(100.0)) + assert.True(t, ok) + assert.Equal(t, Pin(number(200.0)), next) + + next, ok = grid.NextHigherPin(number(400.0)) + assert.True(t, ok) + assert.Equal(t, Pin(number(500.0)), next) + + next, ok = grid.NextHigherPin(number(500.0)) + assert.False(t, ok) + assert.Equal(t, Pin(fixedpoint.Zero), next) +} + func Test_calculateArithmeticPins(t *testing.T) { type args struct { lower fixedpoint.Value From 1fa51860028a0a35046cb7ef1c81d5d9dc8d7761 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Nov 2022 16:25:00 +0800 Subject: [PATCH 0086/1392] grid2: allocate grid object --- pkg/strategy/grid2/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 66613b53bb..68d9eec3f9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -45,6 +45,8 @@ type Strategy struct { LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"` + grid *Grid + bbgo.QuantityOrAmount ProfitStats *types.ProfitStats `persistence:"profit_stats"` @@ -123,6 +125,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position = types.NewPositionFromMarket(s.Market) } + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) From 32b6299b93c3fe0af9c35acf67fb6ba0335f17fc Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Nov 2022 16:26:04 +0800 Subject: [PATCH 0087/1392] grid2: pull out CalculatePins --- pkg/strategy/grid2/grid.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 20aa541b21..539c134dd3 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -60,11 +60,15 @@ func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { Spread: spread, } - var pins = calculateArithmeticPins(lower, upper, spread, tickSize) - grid.addPins(pins) + grid.CalculatePins() return grid } +func (g *Grid) CalculatePins() { + var pins = calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) + g.addPins(pins) +} + func (g *Grid) Height() fixedpoint.Value { return g.UpperPrice.Sub(g.LowerPrice) } From a8cbe0e488f1381570beed62984348dab1942653 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Nov 2022 18:56:33 +0800 Subject: [PATCH 0088/1392] grid2: pull out calculate pins call --- pkg/strategy/grid2/grid.go | 1 - pkg/strategy/grid2/grid_test.go | 9 +++++++++ pkg/strategy/grid2/strategy.go | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 539c134dd3..802e7f1034 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -60,7 +60,6 @@ func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { Spread: spread, } - grid.CalculatePins() return grid } diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 742c2116f1..82ab446a79 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -28,6 +28,8 @@ func TestNewGrid(t *testing.T) { lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() + assert.Equal(t, upper, grid.UpperPrice) assert.Equal(t, lower, grid.LowerPrice) assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) @@ -42,6 +44,7 @@ func TestGrid_HasPin(t *testing.T) { lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() assert.True(t, grid.HasPin(Pin(number(100.0)))) assert.True(t, grid.HasPin(Pin(number(500.0)))) @@ -53,6 +56,8 @@ func TestGrid_ExtendUpperPrice(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() + originalSpread := grid.Spread t.Logf("pins: %+v", grid.Pins) @@ -71,6 +76,7 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { lower := fixedpoint.NewFromFloat(2000.0) size := fixedpoint.NewFromFloat(10.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() assert.Equal(t, Pin(number(2000.0)), grid.BottomPin(), "bottom pin should be 1000.0") assert.Equal(t, Pin(number(3000.0)), grid.TopPin(), "top pin should be 3000.0") @@ -105,6 +111,8 @@ func TestGrid_NextLowerPin(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() + t.Logf("pins: %+v", grid.Pins) next, ok := grid.NextLowerPin(number(200.0)) @@ -121,6 +129,7 @@ func TestGrid_NextHigherPin(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() t.Logf("pins: %+v", grid.Pins) next, ok := grid.NextHigherPin(number(100.0)) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 68d9eec3f9..42ff8449b5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -126,6 +126,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculatePins() s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) From a42c1799e2eab35cb3d7658faf4fdccf6ce2cc6b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Nov 2022 19:43:14 +0800 Subject: [PATCH 0089/1392] grid2: define PinCalculator type --- pkg/strategy/grid2/grid.go | 22 +++++++++++++++++++--- pkg/strategy/grid2/grid_test.go | 12 ++++++------ pkg/strategy/grid2/strategy.go | 2 +- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 802e7f1034..f61b1cb2e0 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -7,6 +7,8 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) +type PinCalculator func() []Pin + type Grid struct { UpperPrice fixedpoint.Value `json:"upperPrice"` LowerPrice fixedpoint.Value `json:"lowerPrice"` @@ -24,6 +26,8 @@ type Grid struct { Pins []Pin `json:"pins"` pinsCache map[Pin]struct{} `json:"-"` + + calculator PinCalculator } type Pin fixedpoint.Value @@ -63,9 +67,21 @@ func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { return grid } -func (g *Grid) CalculatePins() { - var pins = calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) - g.addPins(pins) +func (g *Grid) CalculateGeometricPins() { + g.calculator = func() []Pin { + // return calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) + return nil + } + + g.addPins(g.calculator()) +} + +func (g *Grid) CalculateArithmeticPins() { + g.calculator = func() []Pin { + return calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) + } + + g.addPins(g.calculator()) } func (g *Grid) Height() fixedpoint.Value { diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 82ab446a79..ccd7245cfb 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -28,7 +28,7 @@ func TestNewGrid(t *testing.T) { lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() assert.Equal(t, upper, grid.UpperPrice) assert.Equal(t, lower, grid.LowerPrice) @@ -44,7 +44,7 @@ func TestGrid_HasPin(t *testing.T) { lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() assert.True(t, grid.HasPin(Pin(number(100.0)))) assert.True(t, grid.HasPin(Pin(number(500.0)))) @@ -56,7 +56,7 @@ func TestGrid_ExtendUpperPrice(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() originalSpread := grid.Spread @@ -76,7 +76,7 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { lower := fixedpoint.NewFromFloat(2000.0) size := fixedpoint.NewFromFloat(10.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() assert.Equal(t, Pin(number(2000.0)), grid.BottomPin(), "bottom pin should be 1000.0") assert.Equal(t, Pin(number(3000.0)), grid.TopPin(), "top pin should be 3000.0") @@ -111,7 +111,7 @@ func TestGrid_NextLowerPin(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() t.Logf("pins: %+v", grid.Pins) @@ -129,7 +129,7 @@ func TestGrid_NextHigherPin(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() t.Logf("pins: %+v", grid.Pins) next, ok := grid.NextHigherPin(number(100.0)) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 42ff8449b5..710607d8eb 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -126,7 +126,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) - s.grid.CalculatePins() + s.grid.CalculateArithmeticPins() s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) From 68b1fce634df55ed4670e5c25eaff56f3cf021a1 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 10 Nov 2022 20:58:46 +0800 Subject: [PATCH 0090/1392] grid2: get the last trade price and apply generalOrderExecutor --- pkg/strategy/grid2/grid.go | 1 + pkg/strategy/grid2/strategy.go | 106 ++++++++++++++++++++++++++------- 2 files changed, 85 insertions(+), 22 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index f61b1cb2e0..9121a7ea93 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -69,6 +69,7 @@ func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { func (g *Grid) CalculateGeometricPins() { g.calculator = func() []Pin { + // TODO: implement geometric calculator // return calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) return nil } diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 710607d8eb..98672c29c7 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -28,6 +28,8 @@ func init() { } type Strategy struct { + Environment *bbgo.Environment + // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc // This field will be injected automatically since we defined the Symbol field. types.Market `json:"-" yaml:"-"` @@ -60,6 +62,8 @@ type Strategy struct { tradeCollector *bbgo.TradeCollector + orderExecutor *bbgo.GeneralOrderExecutor + // groupID is the group ID used for the strategy instance for canceling orders groupID uint32 } @@ -125,29 +129,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position = types.NewPositionFromMarket(s.Market) } - s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) - s.grid.CalculateArithmeticPins() - - s.orderStore = bbgo.NewOrderStore(s.Symbol) - s.orderStore.BindStream(session.UserDataStream) - - // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. - s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - s.activeOrders.OnFilled(s.handleOrderFilled) - s.activeOrders.BindStream(session.UserDataStream) - - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) - - s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { - bbgo.Notify(trade) - s.ProfitStats.AddTrade(trade) - }) - - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { - bbgo.Notify(position) + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.Bind() + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) }) - s.tradeCollector.BindStream(session.UserDataStream) + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() @@ -162,8 +153,79 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) session.UserDataStream.OnStart(func() { - + if err := s.setupGridOrders(ctx, session); err != nil { + log.WithError(err).Errorf("failed to setup grid orders") + } }) return nil } + +func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { + lastPrice, err := s.getLastTradePrice(ctx, session) + if err != nil { + return errors.Wrap(err, "failed to get the last trade price") + } + + // shift 1 grid because we will start from the buy order + // if the buy order is filled, then we will submit another sell order at the higher grid. + for i := len(s.grid.Pins) - 2; i >= 0; i++ { + pin := s.grid.Pins[i] + price := fixedpoint.Value(pin) + + if price.Compare(lastPrice) >= 0 { + if s.QuantityOrAmount.Quantity.Sign() > 0 { + quantity := s.QuantityOrAmount.Quantity + + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", + }) + + if err2 != nil { + return err2 + } + + _ = createdOrders + + } else if s.QuantityOrAmount.Amount.Sign() > 0 { + + } + } + } + + return nil +} + +func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.ExchangeSession) (fixedpoint.Value, error) { + if bbgo.IsBackTesting { + price, ok := session.LastPrice(s.Symbol) + if !ok { + return fixedpoint.Zero, fmt.Errorf("last price of %s not found", s.Symbol) + } + + return price, nil + } + + tickers, err := session.Exchange.QueryTickers(ctx, s.Symbol) + if err != nil { + return fixedpoint.Zero, err + } + + if ticker, ok := tickers[s.Symbol]; ok { + if !ticker.Last.IsZero() { + return ticker.Last, nil + } + + // fallback to buy price + return ticker.Buy, nil + } + + return fixedpoint.Zero, fmt.Errorf("%s ticker price not found", s.Symbol) +} From 2c373959a8abf166e59ff3e193f1223766b71e34 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 10 Nov 2022 23:34:55 +0800 Subject: [PATCH 0091/1392] grid2: add investment check --- pkg/strategy/grid2/strategy.go | 114 +++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 28 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 98672c29c7..803737a217 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -32,25 +32,33 @@ type Strategy struct { // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc // This field will be injected automatically since we defined the Symbol field. - types.Market `json:"-" yaml:"-"` + types.Market `json:"-"` // These fields will be filled from the config file (it translates YAML to JSON) - Symbol string `json:"symbol" yaml:"symbol"` + Symbol string `json:"symbol"` // ProfitSpread is the fixed profit spread you want to submit the sell order - ProfitSpread fixedpoint.Value `json:"profitSpread" yaml:"profitSpread"` + ProfitSpread fixedpoint.Value `json:"profitSpread"` // GridNum is the grid number, how many orders you want to post on the orderbook. - GridNum int64 `json:"gridNumber" yaml:"gridNumber"` + GridNum int64 `json:"gridNumber"` - UpperPrice fixedpoint.Value `json:"upperPrice" yaml:"upperPrice"` + UpperPrice fixedpoint.Value `json:"upperPrice"` - LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"` - - grid *Grid + LowerPrice fixedpoint.Value `json:"lowerPrice"` + // QuantityOrAmount embeds the Quantity field and the Amount field + // If you set up the Quantity field or the Amount field, you don't need to set the QuoteInvestment and BaseInvestment bbgo.QuantityOrAmount + // If Quantity and Amount is not set, we can use the quote investment to calculate our quantity. + QuoteInvestment fixedpoint.Value `json:"quoteInvestment"` + + // BaseInvestment is the total base quantity you want to place as the sell order. + BaseInvestment fixedpoint.Value `json:"baseInvestment"` + + grid *Grid + ProfitStats *types.ProfitStats `persistence:"profit_stats"` Position *types.Position `persistence:"position"` @@ -169,33 +177,83 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. + quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() + + // check if base and quote are enough + baseBalance, ok := session.Account.Balance(s.Market.BaseCurrency) + if !ok { + return fmt.Errorf("base %s balance not found", s.Market.BaseCurrency) + } + + quoteBalance, ok := session.Account.Balance(s.Market.QuoteCurrency) + if !ok { + return fmt.Errorf("quote %s balance not found", s.Market.QuoteCurrency) + } + + totalBase := baseBalance.Available + totalQuote := quoteBalance.Available + + if quantityOrAmountIsSet { + requiredBase := fixedpoint.Zero + requiredQuote := fixedpoint.Zero + for i := len(s.grid.Pins) - 1; i >= 0; i++ { + pin := s.grid.Pins[i] + price := fixedpoint.Value(pin) + + q := s.QuantityOrAmount.CalculateQuantity(price) + if price.Compare(lastPrice) >= 0 { + // sell + requiredBase = requiredBase.Add(q) + } else { + requiredQuote = requiredQuote.Add(q) + } + } + + if requiredBase.Compare(totalBase) < 0 && requiredQuote.Compare(totalQuote) < 0 { + return fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enought", + totalBase.Float64(), s.Market.BaseCurrency, + totalQuote.Float64(), s.Market.QuoteCurrency) + } + + if requiredBase.Compare(totalBase) < 0 { + // see if we can convert some quotes to base + } + } + for i := len(s.grid.Pins) - 2; i >= 0; i++ { pin := s.grid.Pins[i] price := fixedpoint.Value(pin) if price.Compare(lastPrice) >= 0 { - if s.QuantityOrAmount.Quantity.Sign() > 0 { - quantity := s.QuantityOrAmount.Quantity - - createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Quantity: quantity, - Price: price, - Market: s.Market, - TimeInForce: types.TimeInForceGTC, - Tag: "grid", - }) - - if err2 != nil { - return err2 - } + // check sell order + if quantityOrAmountIsSet { + if s.QuantityOrAmount.Quantity.Sign() > 0 { + quantity := s.QuantityOrAmount.Quantity + + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", + }) + + if err2 != nil { + return err2 + } + + _ = createdOrders + + } else if s.QuantityOrAmount.Amount.Sign() > 0 { - _ = createdOrders - - } else if s.QuantityOrAmount.Amount.Sign() > 0 { + } + } else if s.BaseInvestment.Sign() > 0 { + } else { + // error: either quantity, amount, baseInvestment is not set. } } } From 4c8db08ccca4732fd5533e8421df6027952ca2b2 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 11 Nov 2022 02:24:52 +0800 Subject: [PATCH 0092/1392] grid2: fix require quote and require base calculation --- pkg/strategy/grid2/strategy.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 803737a217..c14326b6fb 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -200,12 +200,32 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe pin := s.grid.Pins[i] price := fixedpoint.Value(pin) - q := s.QuantityOrAmount.CalculateQuantity(price) if price.Compare(lastPrice) >= 0 { - // sell - requiredBase = requiredBase.Add(q) + // sell orders + if requiredBase.Compare(totalBase) < 0 { + if q := s.QuantityOrAmount.Quantity; !q.IsZero() { + requiredBase = requiredBase.Add(s.QuantityOrAmount.Quantity) + } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { + qq := s.QuantityOrAmount.CalculateQuantity(price) + requiredBase = requiredBase.Add(qq) + } + } else if i > 0 { + // convert buy quote to requiredQuote + nextLowerPin := s.grid.Pins[i-1] + nextLowerPrice := fixedpoint.Value(nextLowerPin) + if q := s.QuantityOrAmount.Quantity; !q.IsZero() { + requiredQuote = requiredQuote.Add(q.Mul(nextLowerPrice)) + } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { + requiredQuote = requiredQuote.Add(amount) + } + } } else { - requiredQuote = requiredQuote.Add(q) + // buy orders + if q := s.QuantityOrAmount.Quantity; !q.IsZero() { + requiredQuote = requiredQuote.Add(q.Mul(price)) + } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { + requiredQuote = requiredQuote.Add(amount) + } } } From 7fec736e7a5ad5c2002b6584b7a6708d8df8e029 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 14 Nov 2022 16:28:07 +0800 Subject: [PATCH 0093/1392] grid2: add GridProfitStats --- pkg/strategy/grid2/strategy.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c14326b6fb..ca01b01488 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -27,6 +27,13 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +type GridProfitStats struct { + TotalProfit fixedpoint.Value `json:"totalProfit"` + FloatProfit fixedpoint.Value `json:"floatProfit"` + GridProfit fixedpoint.Value `json:"gridProfit"` + ArbitrageCount int `json:"arbitrageCount"` +} + type Strategy struct { Environment *bbgo.Environment @@ -169,16 +176,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } +func (s *Strategy) calculateRequiredInvestment(baseInvestment, quoteInvestment, totalBaseBalance, totalQuoteBalance fixedpoint.Value) { + +} + func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { return errors.Wrap(err, "failed to get the last trade price") } - // shift 1 grid because we will start from the buy order - // if the buy order is filled, then we will submit another sell order at the higher grid. - quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() - // check if base and quote are enough baseBalance, ok := session.Account.Balance(s.Market.BaseCurrency) if !ok { @@ -192,7 +199,11 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe totalBase := baseBalance.Available totalQuote := quoteBalance.Available + s.calculateRequiredInvestment(s.BaseInvestment, s.QuoteInvestment, totalBase, totalQuote) + // shift 1 grid because we will start from the buy order + // if the buy order is filled, then we will submit another sell order at the higher grid. + quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() if quantityOrAmountIsSet { requiredBase := fixedpoint.Zero requiredQuote := fixedpoint.Zero @@ -201,7 +212,8 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe price := fixedpoint.Value(pin) if price.Compare(lastPrice) >= 0 { - // sell orders + // for orders that sell + // if we still have the base balance if requiredBase.Compare(totalBase) < 0 { if q := s.QuantityOrAmount.Quantity; !q.IsZero() { requiredBase = requiredBase.Add(s.QuantityOrAmount.Quantity) @@ -220,7 +232,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } } else { - // buy orders + // for orders that buy if q := s.QuantityOrAmount.Quantity; !q.IsZero() { requiredQuote = requiredQuote.Add(q.Mul(price)) } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { From fa692d835f46dce4da412d75b6f1773164687b86 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 14 Nov 2022 16:28:42 +0800 Subject: [PATCH 0094/1392] grid2: add totalFee field and volume field --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index ca01b01488..c237bc4c09 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -32,6 +32,8 @@ type GridProfitStats struct { FloatProfit fixedpoint.Value `json:"floatProfit"` GridProfit fixedpoint.Value `json:"gridProfit"` ArbitrageCount int `json:"arbitrageCount"` + TotalFee fixedpoint.Value `json:"totalFee"` + Volume fixedpoint.Value `json:"volume"` } type Strategy struct { From cde463e294254da751b7bf4d92c5100e996d3fa5 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 14 Nov 2022 16:29:25 +0800 Subject: [PATCH 0095/1392] grid2: remove notionalModifier --- pkg/strategy/grid2/strategy.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c237bc4c09..2faaaa9a12 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -18,8 +18,6 @@ const ID = "grid2" var log = logrus.WithField("strategy", ID) -var notionalModifier = fixedpoint.NewFromFloat(1.0001) - func init() { // Register the pointer of the strategy struct, // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) From 3da86ab2e1d7b15b61c5a4fc32fe260b15a4da10 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 14 Nov 2022 17:37:32 +0800 Subject: [PATCH 0096/1392] grid2: pull out check code to checkRequiredInvestmentByQuantity --- pkg/strategy/grid2/strategy.go | 86 ++++++++++++++++------------------ 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2faaaa9a12..1f91fc8b71 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -176,8 +176,42 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -func (s *Strategy) calculateRequiredInvestment(baseInvestment, quoteInvestment, totalBaseBalance, totalQuoteBalance fixedpoint.Value) { +func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, totalBaseBalance, totalQuoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) error { + requiredBase := fixedpoint.Zero + requiredQuote := fixedpoint.Zero + for i := len(s.grid.Pins) - 1; i >= 0; i++ { + pin := s.grid.Pins[i] + price := fixedpoint.Value(pin) + + // TODO: add fee if we don't have the platform token. BNB, OKEX or MAX... + if price.Compare(lastPrice) >= 0 { + // for orders that sell + // if we still have the base balance + if requiredBase.Compare(totalBaseBalance) < 0 { + requiredBase = requiredBase.Add(quantity) + } else if i > 0 { + // convert buy quote to requiredQuote + nextLowerPin := s.grid.Pins[i-1] + nextLowerPrice := fixedpoint.Value(nextLowerPin) + requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) + + } + } else { + requiredQuote = requiredQuote.Add(quantity.Mul(price)) + } + } + + if requiredBase.Compare(totalBaseBalance) < 0 && requiredQuote.Compare(totalQuoteBalance) < 0 { + return fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enought", + totalBaseBalance.Float64(), s.Market.BaseCurrency, + totalQuoteBalance.Float64(), s.Market.QuoteCurrency) + } + + if requiredBase.Compare(totalBaseBalance) < 0 { + // see if we can convert some quotes to base + } + return nil } func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { @@ -199,56 +233,16 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe totalBase := baseBalance.Available totalQuote := quoteBalance.Available - s.calculateRequiredInvestment(s.BaseInvestment, s.QuoteInvestment, totalBase, totalQuote) // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() if quantityOrAmountIsSet { - requiredBase := fixedpoint.Zero - requiredQuote := fixedpoint.Zero - for i := len(s.grid.Pins) - 1; i >= 0; i++ { - pin := s.grid.Pins[i] - price := fixedpoint.Value(pin) - - if price.Compare(lastPrice) >= 0 { - // for orders that sell - // if we still have the base balance - if requiredBase.Compare(totalBase) < 0 { - if q := s.QuantityOrAmount.Quantity; !q.IsZero() { - requiredBase = requiredBase.Add(s.QuantityOrAmount.Quantity) - } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { - qq := s.QuantityOrAmount.CalculateQuantity(price) - requiredBase = requiredBase.Add(qq) - } - } else if i > 0 { - // convert buy quote to requiredQuote - nextLowerPin := s.grid.Pins[i-1] - nextLowerPrice := fixedpoint.Value(nextLowerPin) - if q := s.QuantityOrAmount.Quantity; !q.IsZero() { - requiredQuote = requiredQuote.Add(q.Mul(nextLowerPrice)) - } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { - requiredQuote = requiredQuote.Add(amount) - } - } - } else { - // for orders that buy - if q := s.QuantityOrAmount.Quantity; !q.IsZero() { - requiredQuote = requiredQuote.Add(q.Mul(price)) - } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { - requiredQuote = requiredQuote.Add(amount) - } - } - } - - if requiredBase.Compare(totalBase) < 0 && requiredQuote.Compare(totalQuote) < 0 { - return fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enought", - totalBase.Float64(), s.Market.BaseCurrency, - totalQuote.Float64(), s.Market.QuoteCurrency) - } - - if requiredBase.Compare(totalBase) < 0 { - // see if we can convert some quotes to base + if err2 := s.checkRequiredInvestmentByQuantity( + s.BaseInvestment, s.QuoteInvestment, + totalBase, totalQuote, + lastPrice, s.QuantityOrAmount.Quantity, s.grid.Pins); err != nil { + return err2 } } From d0bdc859fbbc1f8832731a4d34632c1587fe161c Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 15 Nov 2022 15:29:30 +0800 Subject: [PATCH 0097/1392] grid2: add basic investment check test checkRequiredInvestmentByQuantity --- pkg/strategy/grid2/strategy.go | 40 ++++++++++++++++++++--------- pkg/strategy/grid2/strategy_test.go | 27 +++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 pkg/strategy/grid2/strategy_test.go diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 1f91fc8b71..eb33e430f5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -176,38 +176,54 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, totalBaseBalance, totalQuoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) error { +type InvestmentBudget struct { + baseInvestment fixedpoint.Value + quoteInvestment fixedpoint.Value + baseBalance fixedpoint.Value + quoteBalance fixedpoint.Value +} + +func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) error { + if baseInvestment.Compare(baseBalance) > 0 { + return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) + } + + if quoteInvestment.Compare(quoteBalance) > 0 { + return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) + } + + // check more investment budget details requiredBase := fixedpoint.Zero requiredQuote := fixedpoint.Zero - for i := len(s.grid.Pins) - 1; i >= 0; i++ { - pin := s.grid.Pins[i] + for i := len(pins) - 1; i >= 0; i++ { + pin := pins[i] price := fixedpoint.Value(pin) - // TODO: add fee if we don't have the platform token. BNB, OKEX or MAX... + // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... if price.Compare(lastPrice) >= 0 { // for orders that sell // if we still have the base balance - if requiredBase.Compare(totalBaseBalance) < 0 { + if requiredBase.Compare(baseBalance) < 0 { requiredBase = requiredBase.Add(quantity) - } else if i > 0 { - // convert buy quote to requiredQuote + } else if i > 0 { // we do not want to sell at i == 0 + // convert sell to buy quote and add to requiredQuote nextLowerPin := s.grid.Pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) - } } else { + // for orders that buy requiredQuote = requiredQuote.Add(quantity.Mul(price)) } } - if requiredBase.Compare(totalBaseBalance) < 0 && requiredQuote.Compare(totalQuoteBalance) < 0 { + if requiredBase.Compare(baseBalance) > 0 && requiredQuote.Compare(quoteBalance) > 0 { return fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enought", - totalBaseBalance.Float64(), s.Market.BaseCurrency, - totalQuoteBalance.Float64(), s.Market.QuoteCurrency) + baseBalance.Float64(), s.Market.BaseCurrency, + quoteBalance.Float64(), s.Market.QuoteCurrency) } - if requiredBase.Compare(totalBaseBalance) < 0 { + if requiredBase.Compare(baseBalance) < 0 { // see if we can convert some quotes to base } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go new file mode 100644 index 0000000000..a1cd344bcb --- /dev/null +++ b/pkg/strategy/grid2/strategy_test.go @@ -0,0 +1,27 @@ +package grid2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { + s := &Strategy{} + + t.Run("basic base balance check", func(t *testing.T) { + err := s.checkRequiredInvestmentByQuantity(number(2.0), number(10_000.0), + number(1.0), number(10_000.0), + number(0.1), number(19000.0), []Pin{}) + assert.Error(t, err) + assert.EqualError(t, err, "baseInvestment setup 2.000000 is greater than the total base balance 1.000000") + }) + + t.Run("basic quote balance check", func(t *testing.T) { + err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), + number(1.0), number(100.0), + number(0.1), number(19_000.0), []Pin{}) + assert.Error(t, err) + assert.EqualError(t, err, "quoteInvestment setup 10000.000000 is greater than the total quote balance 100.000000") + }) +} From dcbce8aa5c66d6bb4a80a74b3b2cf67000843ec5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 12:09:55 +0800 Subject: [PATCH 0098/1392] grid2: fix TestStrategy_checkRequiredInvestmentByQuantity --- pkg/strategy/grid2/strategy.go | 33 ++++++++++++++++------------- pkg/strategy/grid2/strategy_test.go | 19 +++++++++++++++-- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index eb33e430f5..b2f924076e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -183,19 +183,22 @@ type InvestmentBudget struct { quoteBalance fixedpoint.Value } -func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) error { +func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { if baseInvestment.Compare(baseBalance) > 0 { - return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) + return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) } if quoteInvestment.Compare(quoteBalance) > 0 { - return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) + return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) } // check more investment budget details - requiredBase := fixedpoint.Zero - requiredQuote := fixedpoint.Zero - for i := len(pins) - 1; i >= 0; i++ { + requiredBase = fixedpoint.Zero + requiredQuote = fixedpoint.Zero + + // when we need to place a buy-to-sell conversion order, we need to mark the price + buyPlacedPrice := fixedpoint.Zero + for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) @@ -203,31 +206,31 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvest if price.Compare(lastPrice) >= 0 { // for orders that sell // if we still have the base balance - if requiredBase.Compare(baseBalance) < 0 { + if requiredBase.Add(quantity).Compare(baseBalance) <= 0 { requiredBase = requiredBase.Add(quantity) } else if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote - nextLowerPin := s.grid.Pins[i-1] + nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) + buyPlacedPrice = nextLowerPrice } } else { // for orders that buy + if price.Compare(buyPlacedPrice) == 0 { + continue + } requiredQuote = requiredQuote.Add(quantity.Mul(price)) } } if requiredBase.Compare(baseBalance) > 0 && requiredQuote.Compare(quoteBalance) > 0 { - return fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enought", + return requiredBase, requiredQuote, fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enough", baseBalance.Float64(), s.Market.BaseCurrency, quoteBalance.Float64(), s.Market.QuoteCurrency) } - if requiredBase.Compare(baseBalance) < 0 { - // see if we can convert some quotes to base - } - - return nil + return requiredBase, requiredQuote, nil } func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { @@ -254,7 +257,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe // if the buy order is filled, then we will submit another sell order at the higher grid. quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() if quantityOrAmountIsSet { - if err2 := s.checkRequiredInvestmentByQuantity( + if _, _, err2 := s.checkRequiredInvestmentByQuantity( s.BaseInvestment, s.QuoteInvestment, totalBase, totalQuote, lastPrice, s.QuantityOrAmount.Quantity, s.grid.Pins); err != nil { diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index a1cd344bcb..f45558a52c 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -10,7 +10,7 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { s := &Strategy{} t.Run("basic base balance check", func(t *testing.T) { - err := s.checkRequiredInvestmentByQuantity(number(2.0), number(10_000.0), + _, _, err := s.checkRequiredInvestmentByQuantity(number(2.0), number(10_000.0), number(1.0), number(10_000.0), number(0.1), number(19000.0), []Pin{}) assert.Error(t, err) @@ -18,10 +18,25 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { }) t.Run("basic quote balance check", func(t *testing.T) { - err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), + _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), number(1.0), number(100.0), number(0.1), number(19_000.0), []Pin{}) assert.Error(t, err) assert.EqualError(t, err, "quoteInvestment setup 10000.000000 is greater than the total quote balance 100.000000") }) + + t.Run("quote to base balance conversion check", func(t *testing.T) { + _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(10_000.0), + number(0.0), number(10_000.0), + number(0.1), number(13_500.0), []Pin{ + Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) + Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) + Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) + Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) + Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) + Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD + }) + assert.NoError(t, err) + assert.Equal(t, number(6000.0), requiredQuote) + }) } From f5219ae56ba3b45154c00d6899074d445688f070 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 13:29:55 +0800 Subject: [PATCH 0099/1392] grid2: fix error checking and add more tests --- pkg/strategy/grid2/strategy.go | 20 ++++++++++++++++++-- pkg/strategy/grid2/strategy_test.go | 25 ++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b2f924076e..6bb867e821 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -225,9 +225,25 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvest } if requiredBase.Compare(baseBalance) > 0 && requiredQuote.Compare(quoteBalance) > 0 { - return requiredBase, requiredQuote, fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enough", + return requiredBase, requiredQuote, fmt.Errorf("both base balance (%f %s) or quote balance (%f %s) is not enough, required = base %f + quote %f", baseBalance.Float64(), s.Market.BaseCurrency, - quoteBalance.Float64(), s.Market.QuoteCurrency) + quoteBalance.Float64(), s.Market.QuoteCurrency, + requiredBase.Float64(), + requiredQuote.Float64()) + } + + if requiredBase.Compare(baseBalance) > 0 { + return requiredBase, requiredQuote, fmt.Errorf("base balance (%f %s), required = base %f", + baseBalance.Float64(), s.Market.BaseCurrency, + requiredBase.Float64(), + ) + } + + if requiredQuote.Compare(quoteBalance) > 0 { + return requiredBase, requiredQuote, fmt.Errorf("quote balance (%f %s) is not enough, required = quote %f", + quoteBalance.Float64(), s.Market.QuoteCurrency, + requiredQuote.Float64(), + ) } return requiredBase, requiredQuote, nil diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index f45558a52c..4c3ca31846 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -4,10 +4,17 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" ) func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { - s := &Strategy{} + s := &Strategy{ + Market: types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + }, + } t.Run("basic base balance check", func(t *testing.T) { _, _, err := s.checkRequiredInvestmentByQuantity(number(2.0), number(10_000.0), @@ -39,4 +46,20 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { assert.NoError(t, err) assert.Equal(t, number(6000.0), requiredQuote) }) + + t.Run("quote to base balance conversion not enough", func(t *testing.T) { + _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(5_000.0), + number(0.0), number(5_000.0), + number(0.1), number(13_500.0), []Pin{ + Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) + Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) + Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) + Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) + Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) + Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD + }) + assert.EqualError(t, err, "quote balance (5000.000000 USDT) is not enough, required = quote 6000.000000") + assert.Equal(t, number(6000.0), requiredQuote) + }) + } From 2aaa2e77750e1453c58306d274c12021f6b2729f Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 15:11:42 +0800 Subject: [PATCH 0100/1392] grid2: add checkRequiredInvestmentByAmount test --- pkg/strategy/grid2/strategy.go | 67 +++++++++++++++++++++++++++++ pkg/strategy/grid2/strategy_test.go | 24 +++++++++++ 2 files changed, 91 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6bb867e821..7d36d00d11 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -249,6 +249,73 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvest return requiredBase, requiredQuote, nil } +func (s *Strategy) checkRequiredInvestmentByAmount(baseInvestment, quoteInvestment, baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { + if baseInvestment.Compare(baseBalance) > 0 { + return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) + } + + if quoteInvestment.Compare(quoteBalance) > 0 { + return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) + } + + // check more investment budget details + requiredBase = fixedpoint.Zero + requiredQuote = fixedpoint.Zero + + // when we need to place a buy-to-sell conversion order, we need to mark the price + buyPlacedPrice := fixedpoint.Zero + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + + // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... + if price.Compare(lastPrice) >= 0 { + // for orders that sell + // if we still have the base balance + quantity := amount.Div(lastPrice) + if requiredBase.Add(quantity).Compare(baseBalance) <= 0 { + requiredBase = requiredBase.Add(quantity) + } else if i > 0 { // we do not want to sell at i == 0 + // convert sell to buy quote and add to requiredQuote + nextLowerPin := pins[i-1] + nextLowerPrice := fixedpoint.Value(nextLowerPin) + requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) + buyPlacedPrice = nextLowerPrice + } + } else { + // for orders that buy + if price.Compare(buyPlacedPrice) == 0 { + continue + } + requiredQuote = requiredQuote.Add(amount) + } + } + + if requiredBase.Compare(baseBalance) > 0 && requiredQuote.Compare(quoteBalance) > 0 { + return requiredBase, requiredQuote, fmt.Errorf("both base balance (%f %s) or quote balance (%f %s) is not enough, required = base %f + quote %f", + baseBalance.Float64(), s.Market.BaseCurrency, + quoteBalance.Float64(), s.Market.QuoteCurrency, + requiredBase.Float64(), + requiredQuote.Float64()) + } + + if requiredBase.Compare(baseBalance) > 0 { + return requiredBase, requiredQuote, fmt.Errorf("base balance (%f %s), required = base %f", + baseBalance.Float64(), s.Market.BaseCurrency, + requiredBase.Float64(), + ) + } + + if requiredQuote.Compare(quoteBalance) > 0 { + return requiredBase, requiredQuote, fmt.Errorf("quote balance (%f %s) is not enough, required = quote %f", + quoteBalance.Float64(), s.Market.QuoteCurrency, + requiredQuote.Float64(), + ) + } + + return requiredBase, requiredQuote, nil +} + func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 4c3ca31846..ff3163497f 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -61,5 +61,29 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { assert.EqualError(t, err, "quote balance (5000.000000 USDT) is not enough, required = quote 6000.000000") assert.Equal(t, number(6000.0), requiredQuote) }) +} + +func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { + s := &Strategy{ + Market: types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + }, + } + t.Run("quote to base balance conversion", func(t *testing.T) { + _, requiredQuote, err := s.checkRequiredInvestmentByAmount(number(0.0), number(3_000.0), + number(0.0), number(3_000.0), + number(1000.0), + number(13_500.0), []Pin{ + Pin(number(10_000.0)), + Pin(number(11_000.0)), + Pin(number(12_000.0)), + Pin(number(13_000.0)), + Pin(number(14_000.0)), + Pin(number(15_000.0)), + }) + assert.EqualError(t, err, "quote balance (3000.000000 USDT) is not enough, required = quote 4999.999890") + assert.Equal(t, number(4999.99989), requiredQuote) + }) } From 991dc4121c772cb2769fb75811ae07e8ffc987cb Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 16:42:14 +0800 Subject: [PATCH 0101/1392] fixedpoint: add Floor() method on dnum --- pkg/fixedpoint/dec.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/fixedpoint/dec.go b/pkg/fixedpoint/dec.go index 2b5f3743b4..43c52d499c 100644 --- a/pkg/fixedpoint/dec.go +++ b/pkg/fixedpoint/dec.go @@ -131,7 +131,7 @@ func NewFromInt(n int64) Value { if n == 0 { return Zero } - //n0 := n + // n0 := n sign := int8(signPos) if n < 0 { n = -n @@ -527,7 +527,7 @@ func NewFromString(s string) (Value, error) { coef *= pow10[p] exp -= p } - //check(coefMin <= coef && coef <= coefMax) + // check(coefMin <= coef && coef <= coefMax) return Value{coef, sign, exp}, nil } @@ -577,7 +577,7 @@ func NewFromBytes(s []byte) (Value, error) { coef *= pow10[p] exp -= p } - //check(coefMin <= coef && coef <= coefMax) + // check(coefMin <= coef && coef <= coefMax) return Value{coef, sign, exp}, nil } @@ -958,6 +958,10 @@ func (dn Value) integer(mode RoundingMode) Value { return Value{i, dn.sign, dn.exp} } +func (dn Value) Floor() Value { + return dn.Round(0, Down) +} + func (dn Value) Round(r int, mode RoundingMode) Value { if dn.sign == 0 || dn.sign == signNegInf || dn.sign == signPosInf || r >= digitsMax { @@ -1171,9 +1175,9 @@ func align(x, y *Value) bool { return false } yshift = e - //check(0 <= yshift && yshift <= 20) + // check(0 <= yshift && yshift <= 20) y.coef = (y.coef + halfpow10[yshift]) / pow10[yshift] - //check(int(y.exp)+yshift == int(x.exp)) + // check(int(y.exp)+yshift == int(x.exp)) return true } @@ -1278,7 +1282,7 @@ func (dn Value) Format(mask string) string { nd := len(digits) di := e - before - //check(di <= 0) + // check(di <= 0) var buf strings.Builder sign := n.Sign() signok := (sign >= 0) From 051755ec54d1de4bebb54ba9f77a9c87d7bd420c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 17:08:40 +0800 Subject: [PATCH 0102/1392] fixedpoint: add Floor test --- pkg/fixedpoint/dec_dnum_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/fixedpoint/dec_dnum_test.go b/pkg/fixedpoint/dec_dnum_test.go index d92d496397..051a8f0e6a 100644 --- a/pkg/fixedpoint/dec_dnum_test.go +++ b/pkg/fixedpoint/dec_dnum_test.go @@ -3,8 +3,9 @@ package fixedpoint import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestDelta(t *testing.T) { @@ -13,6 +14,12 @@ func TestDelta(t *testing.T) { assert.InDelta(t, f1.Mul(f2).Float64(), 41.3, 1e-14) } +func TestFloor(t *testing.T) { + f1 := MustNewFromString("10.333333") + f2 := f1.Floor() + assert.Equal(t, "10", f2.String()) +} + func TestInternal(t *testing.T) { r := &reader{"1.1e-15", 0} c, e := r.getCoef() From 4eb21d5209e7a92a56af0bf8e59341ac743657e5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 17:08:57 +0800 Subject: [PATCH 0103/1392] grid2: move out baseInvestment, quoteInvestment check --- pkg/strategy/grid2/strategy.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7d36d00d11..d4b981f79f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -250,13 +250,6 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvest } func (s *Strategy) checkRequiredInvestmentByAmount(baseInvestment, quoteInvestment, baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { - if baseInvestment.Compare(baseBalance) > 0 { - return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) - } - - if quoteInvestment.Compare(quoteBalance) > 0 { - return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) - } // check more investment budget details requiredBase = fixedpoint.Zero @@ -336,6 +329,15 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe totalBase := baseBalance.Available totalQuote := quoteBalance.Available + if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { + if s.BaseInvestment.Compare(totalBase) > 0 { + return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", s.BaseInvestment.Float64(), totalBase.Float64()) + } + if s.QuoteInvestment.Compare(totalQuote) > 0 { + return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", s.QuoteInvestment.Float64(), totalQuote.Float64()) + } + } + // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() From 4407aa7f9788875950d0ed0178ddac2d2ea6102d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 17 Nov 2022 17:40:59 +0800 Subject: [PATCH 0104/1392] grid2: refactor checkRequiredInvestmentByAmount and checkRequiredInvestmentByQuantity --- pkg/strategy/grid2/strategy.go | 29 ++++++++----------- pkg/strategy/grid2/strategy_test.go | 44 ++++++++++++----------------- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index d4b981f79f..4b2d697658 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -183,15 +183,7 @@ type InvestmentBudget struct { quoteBalance fixedpoint.Value } -func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { - if baseInvestment.Compare(baseBalance) > 0 { - return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) - } - - if quoteInvestment.Compare(quoteBalance) > 0 { - return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) - } - +func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { // check more investment budget details requiredBase = fixedpoint.Zero requiredQuote = fixedpoint.Zero @@ -249,7 +241,7 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvest return requiredBase, requiredQuote, nil } -func (s *Strategy) checkRequiredInvestmentByAmount(baseInvestment, quoteInvestment, baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { +func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { // check more investment budget details requiredBase = fixedpoint.Zero @@ -340,13 +332,16 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. - quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() - if quantityOrAmountIsSet { - if _, _, err2 := s.checkRequiredInvestmentByQuantity( - s.BaseInvestment, s.QuoteInvestment, - totalBase, totalQuote, - lastPrice, s.QuantityOrAmount.Quantity, s.grid.Pins); err != nil { - return err2 + if s.QuantityOrAmount.IsSet() { + if quantity := s.QuantityOrAmount.Quantity; !quantity.IsZero() { + if _, _, err2 := s.checkRequiredInvestmentByQuantity(totalBase, totalQuote, lastPrice, s.QuantityOrAmount.Quantity, s.grid.Pins); err != nil { + return err2 + } + } + if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { + if _, _, err2 := s.checkRequiredInvestmentByAmount(totalBase, totalQuote, lastPrice, amount, s.grid.Pins); err != nil { + return err2 + } } } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index ff3163497f..41217c4bb8 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -17,47 +17,39 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { } t.Run("basic base balance check", func(t *testing.T) { - _, _, err := s.checkRequiredInvestmentByQuantity(number(2.0), number(10_000.0), - number(1.0), number(10_000.0), - number(0.1), number(19000.0), []Pin{}) + _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), number(0.1), number(19000.0), []Pin{}) assert.Error(t, err) assert.EqualError(t, err, "baseInvestment setup 2.000000 is greater than the total base balance 1.000000") }) t.Run("basic quote balance check", func(t *testing.T) { - _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), - number(1.0), number(100.0), - number(0.1), number(19_000.0), []Pin{}) + _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(100.0), number(0.1), number(19_000.0), []Pin{}) assert.Error(t, err) assert.EqualError(t, err, "quoteInvestment setup 10000.000000 is greater than the total quote balance 100.000000") }) t.Run("quote to base balance conversion check", func(t *testing.T) { - _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(10_000.0), - number(0.0), number(10_000.0), - number(0.1), number(13_500.0), []Pin{ - Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) - Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) - Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) - Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) - Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) - Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD - }) + _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(10_000.0), number(0.1), number(13_500.0), []Pin{ + Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) + Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) + Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) + Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) + Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) + Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD + }) assert.NoError(t, err) assert.Equal(t, number(6000.0), requiredQuote) }) t.Run("quote to base balance conversion not enough", func(t *testing.T) { - _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(5_000.0), - number(0.0), number(5_000.0), - number(0.1), number(13_500.0), []Pin{ - Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) - Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) - Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) - Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) - Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) - Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD - }) + _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(5_000.0), number(0.1), number(13_500.0), []Pin{ + Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) + Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) + Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) + Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) + Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) + Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD + }) assert.EqualError(t, err, "quote balance (5000.000000 USDT) is not enough, required = quote 6000.000000") assert.Equal(t, number(6000.0), requiredQuote) }) From e3c735b7005a7de78944d2a2e9ecfbe817459a86 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 24 Nov 2022 15:55:02 +0800 Subject: [PATCH 0105/1392] grid2: add more code to setupGridOrders --- pkg/strategy/grid2/strategy.go | 109 +++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 4b2d697658..6a28059c27 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -301,6 +301,9 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am return requiredBase, requiredQuote, nil } +// setupGridOrders +// 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. +// 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { @@ -321,15 +324,6 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe totalBase := baseBalance.Available totalQuote := quoteBalance.Available - if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { - if s.BaseInvestment.Compare(totalBase) > 0 { - return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", s.BaseInvestment.Float64(), totalBase.Float64()) - } - if s.QuoteInvestment.Compare(totalQuote) > 0 { - return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", s.QuoteInvestment.Float64(), totalQuote.Float64()) - } - } - // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. if s.QuantityOrAmount.IsSet() { @@ -345,42 +339,77 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } - for i := len(s.grid.Pins) - 2; i >= 0; i++ { - pin := s.grid.Pins[i] + if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { + if s.BaseInvestment.Compare(totalBase) > 0 { + return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", s.BaseInvestment.Float64(), totalBase.Float64()) + } + if s.QuoteInvestment.Compare(totalQuote) > 0 { + return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", s.QuoteInvestment.Float64(), totalQuote.Float64()) + } + + if !s.QuantityOrAmount.IsSet() { + // TODO: calculate and override the quantity here + } + } + + var buyPlacedPrice = fixedpoint.Zero + var pins = s.grid.Pins + var usedBase = fixedpoint.Zero + var usedQuote = fixedpoint.Zero + var submitOrders []types.SubmitOrder + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] price := fixedpoint.Value(pin) + quantity := s.QuantityOrAmount.Quantity + if quantity.IsZero() { + quantity = s.QuantityOrAmount.Amount.Div(price) + } + // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... if price.Compare(lastPrice) >= 0 { - // check sell order - if quantityOrAmountIsSet { - if s.QuantityOrAmount.Quantity.Sign() > 0 { - quantity := s.QuantityOrAmount.Quantity - - createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Quantity: quantity, - Price: price, - Market: s.Market, - TimeInForce: types.TimeInForceGTC, - Tag: "grid", - }) - - if err2 != nil { - return err2 - } - - _ = createdOrders - - } else if s.QuantityOrAmount.Amount.Sign() > 0 { - - } - } else if s.BaseInvestment.Sign() > 0 { - - } else { - // error: either quantity, amount, baseInvestment is not set. + if usedBase.Add(quantity).Compare(totalBase) < 0 { + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeSell, + Price: price, + Quantity: quantity, + }) + usedBase = usedBase.Add(quantity) + } else if i > 0 { + // next price + nextPin := pins[i-1] + nextPrice := fixedpoint.Value(nextPin) + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeBuy, + Price: nextPrice, + Quantity: quantity, + }) + quoteQuantity := quantity.Mul(price) + usedQuote = usedQuote.Add(quoteQuantity) + buyPlacedPrice = nextPrice } + } else { } + + /* + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", + }) + + if err2 != nil { + return err2 + } + */ } return nil From 020e7c8604622f1de05fe8357ff8aab175d0f220 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 24 Nov 2022 16:35:31 +0800 Subject: [PATCH 0106/1392] grid2: handle grid orders submission --- pkg/strategy/grid2/strategy.go | 49 +++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6a28059c27..fbbbed77c6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -369,11 +369,14 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe if price.Compare(lastPrice) >= 0 { if usedBase.Add(quantity).Compare(totalBase) < 0 { submitOrders = append(submitOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimitMaker, - Side: types.SideTypeSell, - Price: price, - Quantity: quantity, + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeSell, + Price: price, + Quantity: quantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", }) usedBase = usedBase.Add(quantity) } else if i > 0 { @@ -381,35 +384,43 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe nextPin := pins[i-1] nextPrice := fixedpoint.Value(nextPin) submitOrders = append(submitOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimitMaker, - Side: types.SideTypeBuy, - Price: nextPrice, - Quantity: quantity, + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeBuy, + Price: nextPrice, + Quantity: quantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", }) quoteQuantity := quantity.Mul(price) usedQuote = usedQuote.Add(quoteQuantity) buyPlacedPrice = nextPrice } } else { - } + if price.Compare(buyPlacedPrice) >= 0 { + continue + } - /* - createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Quantity: quantity, Price: price, + Quantity: quantity, Market: s.Market, TimeInForce: types.TimeInForceGTC, Tag: "grid", }) + quoteQuantity := quantity.Mul(price) + usedQuote = usedQuote.Add(quoteQuantity) + } - if err2 != nil { - return err2 - } - */ + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...) + if err2 != nil { + return err + } + _ = createdOrders } return nil From 622fe75ed3db432e39cf96c3e8aba76cdd2cbe6b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 25 Nov 2022 15:05:11 +0800 Subject: [PATCH 0107/1392] grid2: check buy placed order price --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index fbbbed77c6..42897e184c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -209,7 +209,7 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, } } else { // for orders that buy - if price.Compare(buyPlacedPrice) == 0 { + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { continue } requiredQuote = requiredQuote.Add(quantity.Mul(price)) @@ -269,7 +269,7 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am } } else { // for orders that buy - if price.Compare(buyPlacedPrice) == 0 { + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { continue } requiredQuote = requiredQuote.Add(amount) @@ -398,7 +398,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe buyPlacedPrice = nextPrice } } else { - if price.Compare(buyPlacedPrice) >= 0 { + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) >= 0 { continue } From 1629a25beb5c3417d6200bf35df47f1091478d06 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 25 Nov 2022 15:42:45 +0800 Subject: [PATCH 0108/1392] grid2: fix tests --- pkg/strategy/grid2/strategy_test.go | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 41217c4bb8..75759c8cda 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -16,18 +16,6 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { }, } - t.Run("basic base balance check", func(t *testing.T) { - _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), number(0.1), number(19000.0), []Pin{}) - assert.Error(t, err) - assert.EqualError(t, err, "baseInvestment setup 2.000000 is greater than the total base balance 1.000000") - }) - - t.Run("basic quote balance check", func(t *testing.T) { - _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(100.0), number(0.1), number(19_000.0), []Pin{}) - assert.Error(t, err) - assert.EqualError(t, err, "quoteInvestment setup 10000.000000 is greater than the total quote balance 100.000000") - }) - t.Run("quote to base balance conversion check", func(t *testing.T) { _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(10_000.0), number(0.1), number(13_500.0), []Pin{ Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) @@ -64,7 +52,7 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { } t.Run("quote to base balance conversion", func(t *testing.T) { - _, requiredQuote, err := s.checkRequiredInvestmentByAmount(number(0.0), number(3_000.0), + _, requiredQuote, err := s.checkRequiredInvestmentByAmount( number(0.0), number(3_000.0), number(1000.0), number(13_500.0), []Pin{ @@ -76,6 +64,6 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { Pin(number(15_000.0)), }) assert.EqualError(t, err, "quote balance (3000.000000 USDT) is not enough, required = quote 4999.999890") - assert.Equal(t, number(4999.99989), requiredQuote) + assert.InDelta(t, 4999.99989, requiredQuote.Float64(), number(0.001).Float64()) }) } From e981ad641a313fc1abf7b8e635e05bebaa1b1a9c Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 25 Nov 2022 18:07:02 +0800 Subject: [PATCH 0109/1392] grid2: ignore test build for dnum --- pkg/strategy/grid2/grid.go | 7 +++++-- pkg/strategy/grid2/grid_test.go | 13 ++++++++++--- pkg/strategy/grid2/strategy_test.go | 2 ++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 9121a7ea93..b2939e2238 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -34,10 +34,13 @@ type Pin fixedpoint.Value func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) []Pin { var pins []Pin + var ts = tickSize.Float64() for p := lower; p.Compare(upper) <= 0; p = p.Add(spread) { // tickSize here = 0.01 - pp := math.Trunc(p.Float64()/tickSize.Float64()) * tickSize.Float64() - pins = append(pins, Pin(fixedpoint.NewFromFloat(pp))) + pp := p.Float64() / ts + pp = math.Trunc(pp) * ts + pin := Pin(fixedpoint.NewFromFloat(pp)) + pins = append(pins, pin) } return pins diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index ccd7245cfb..8edfe59cd2 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -1,3 +1,5 @@ +//go:build !dnum + package grid2 import ( @@ -170,7 +172,7 @@ func Test_calculateArithmeticPins(t *testing.T) { Pin(number(1000.0)), Pin(number(1066.660)), Pin(number(1133.330)), - Pin(number(1199.990)), + Pin(number("1199.99")), Pin(number(1266.660)), Pin(number(1333.330)), Pin(number(1399.990)), @@ -197,14 +199,19 @@ func Test_calculateArithmeticPins(t *testing.T) { Pin(number(2799.990)), Pin(number(2866.660)), Pin(number(2933.330)), - Pin(number(2999.990)), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { spread := tt.args.upper.Sub(tt.args.lower).Div(tt.args.size) - assert.Equalf(t, tt.want, calculateArithmeticPins(tt.args.lower, tt.args.upper, spread, tt.args.tickSize), "calculateArithmeticPins(%v, %v, %v, %v)", tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize) + pins := calculateArithmeticPins(tt.args.lower, tt.args.upper, spread, tt.args.tickSize) + for i := 0; i < len(tt.want); i++ { + assert.InDelta(t, fixedpoint.Value(tt.want[i]).Float64(), + fixedpoint.Value(pins[i]).Float64(), + 0.001, + "calculateArithmeticPins(%v, %v, %v, %v)", tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize) + } }) } } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 75759c8cda..e27c87ac9b 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -1,3 +1,5 @@ +//go:build !dnum + package grid2 import ( From e385b589b66bddb78ea99d24a8670cb7d041a340 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 27 Nov 2022 00:19:59 +0800 Subject: [PATCH 0110/1392] config: add grid2 config --- config/grid2.yaml | 33 +++++++++++++++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 6 +++++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 config/grid2.yaml diff --git a/config/grid2.yaml b/config/grid2.yaml new file mode 100644 index 0000000000..e4338bcfd0 --- /dev/null +++ b/config/grid2.yaml @@ -0,0 +1,33 @@ +--- +sessions: + binance: + exchange: binance + envVarPrefix: binance + max: + exchange: max + envVarPrefix: max + +# example command: +# godotenv -f .env.local -- go run ./cmd/bbgo backtest --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline +backtest: + startTime: "2022-01-01" + endTime: "2022-11-25" + symbols: + - BTCUSDT + sessions: [max] + accounts: + binance: + balances: + BTC: 0.0 + USDT: 10000.0 + +exchangeStrategies: + +- on: max + grid2: + symbol: BTCUSDT + upperPrice: 10_000.0 + lowerPrice: 15_000.0 + gridNumber: 10 + quantity: 0.001 + # profitSpread: 1000.0 # The profit price spread that you want to add to your sell order when your buy order is executed diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 42897e184c..1110917a93 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -337,6 +337,8 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe return err2 } } + } else { + // TODO: calculate the quantity from the investment configuration } if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { @@ -420,7 +422,9 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe if err2 != nil { return err } - _ = createdOrders + for _, order := range createdOrders { + log.Infof(order.String()) + } } return nil From 9f2e4d3f711a09c7fac3cff1be551c8cc3bf2c2b Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 27 Nov 2022 19:11:45 +0800 Subject: [PATCH 0111/1392] grid2: add calculateQuoteInvestmentQuantity so that we can calculate quantity from the quote investment --- pkg/strategy/grid2/strategy.go | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 1110917a93..261f3494e7 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -301,6 +301,43 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am return requiredBase, requiredQuote, nil } +func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { + buyPlacedPrice := fixedpoint.Zero + + // quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + .... + // => + // quoteInvestment = (p1 + p2 + p3) * q + // q = quoteInvestment / (p1 + p2 + p3) + totalQuotePrice := fixedpoint.Zero + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + + if price.Compare(lastPrice) >= 0 { + // for orders that sell + // if we still have the base balance + // quantity := amount.Div(lastPrice) + if i > 0 { // we do not want to sell at i == 0 + // convert sell to buy quote and add to requiredQuote + nextLowerPin := pins[i-1] + nextLowerPrice := fixedpoint.Value(nextLowerPin) + // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) + totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) + buyPlacedPrice = nextLowerPrice + } + } else { + // for orders that buy + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { + continue + } + + totalQuotePrice = totalQuotePrice.Add(price) + } + } + + return quoteInvestment.Div(totalQuotePrice), nil +} + // setupGridOrders // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. @@ -339,6 +376,13 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } else { // TODO: calculate the quantity from the investment configuration + if !s.QuoteInvestment.IsZero() { + quantity, err2 := s.calculateQuoteInvestmentQuantity(s.QuoteInvestment, lastPrice, s.grid.Pins) + if err2 != nil { + return err2 + } + _ = quantity + } } if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { From 2260fd6908d6886d39e61fd8ac993e69e3852a33 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 27 Nov 2022 19:19:54 +0800 Subject: [PATCH 0112/1392] grid2: add TestStrategy_calculateQuoteInvestmentQuantity test case --- pkg/strategy/grid2/strategy_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index e27c87ac9b..7e2f064837 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -69,3 +69,29 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { assert.InDelta(t, 4999.99989, requiredQuote.Float64(), number(0.001).Float64()) }) } + +func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { + s := &Strategy{ + Market: types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + }, + } + + t.Run("calculate quote quantity from quote investment", func(t *testing.T) { + // quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000) * q + // q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000) + // q = 12_000 / (10,000 + 11,000 + 12,000 + 13,000 + 14,000) + // q = 0.2 + quantity, err := s.calculateQuoteInvestmentQuantity(number(12_000.0), number(13_500.0), []Pin{ + Pin(number(10_000.0)), // buy + Pin(number(11_000.0)), // buy + Pin(number(12_000.0)), // buy + Pin(number(13_000.0)), // buy + Pin(number(14_000.0)), // buy + Pin(number(15_000.0)), + }) + assert.NoError(t, err) + assert.Equal(t, number(0.2).String(), quantity.String()) + }) +} From 4eb652b560a2804fa8c2968291dbcaf0da0040a8 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 30 Nov 2022 12:46:39 +0800 Subject: [PATCH 0113/1392] grid2: add calculateQuoteBaseInvestmentQuantity --- pkg/strategy/grid2/strategy.go | 70 +++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 261f3494e7..daf3636a46 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -338,6 +338,65 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f return quoteInvestment.Div(totalQuotePrice), nil } +func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { + // q_p1 = q_p2 = q_p3 = q_p4 + // baseInvestment = q_p1 + q_p2 + q_p3 + q_p4 + .... + // baseInvestment = numberOfSellOrders * q + // maxBaseQuantity = baseInvestment / numberOfSellOrders + // if maxBaseQuantity < minQuantity or maxBaseQuantity * priceLowest < minNotional + // then reduce the numberOfSellOrders + numberOfSellOrders := 0 + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + if price.Compare(lastPrice) < 0 { + break + } + numberOfSellOrders++ + } + + numberOfSellOrders++ + maxBaseQuantity := fixedpoint.Zero + for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 { + numberOfSellOrders-- + maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(numberOfSellOrders))) + } + + buyPlacedPrice := fixedpoint.Zero + totalQuotePrice := fixedpoint.Zero + // quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + .... + // => + // quoteInvestment = (p1 + p2 + p3) * q + // maxBuyQuantity = quoteInvestment / (p1 + p2 + p3) + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + + if price.Compare(lastPrice) >= 0 { + // for orders that sell + // if we still have the base balance + // quantity := amount.Div(lastPrice) + if i > 0 { // we do not want to sell at i == 0 + // convert sell to buy quote and add to requiredQuote + nextLowerPin := pins[i-1] + nextLowerPrice := fixedpoint.Value(nextLowerPin) + // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) + totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) + buyPlacedPrice = nextLowerPrice + } + } else { + // for orders that buy + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { + continue + } + + totalQuotePrice = totalQuotePrice.Add(price) + } + } + + return quoteInvestment.Div(totalQuotePrice), nil +} + // setupGridOrders // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. @@ -376,12 +435,19 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } else { // TODO: calculate the quantity from the investment configuration - if !s.QuoteInvestment.IsZero() { + if !s.QuoteInvestment.IsZero() && !s.BaseInvestment.IsZero() { + quantity, err2 := s.calculateQuoteBaseInvestmentQuantity(s.QuoteInvestment, s.BaseInvestment, lastPrice, s.grid.Pins) + if err2 != nil { + return err2 + } + s.QuantityOrAmount.Quantity = quantity + + } else if !s.QuoteInvestment.IsZero() { quantity, err2 := s.calculateQuoteInvestmentQuantity(s.QuoteInvestment, lastPrice, s.grid.Pins) if err2 != nil { return err2 } - _ = quantity + s.QuantityOrAmount.Quantity = quantity } } From 45328a9f3d29ea4b0428f1690cd31a281d4b423a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 30 Nov 2022 12:52:04 +0800 Subject: [PATCH 0114/1392] grid2: add comment for the quantity loop --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index daf3636a46..4cc2eb1310 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -355,6 +355,8 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv numberOfSellOrders++ } + // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders + // so that the quantity can be increased. numberOfSellOrders++ maxBaseQuantity := fixedpoint.Zero for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 { From 46bebb102209f8f7c4b03d47f007fdcbc46de6e4 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 30 Nov 2022 12:55:23 +0800 Subject: [PATCH 0115/1392] grid2: calculate minBaseQuantity --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 4cc2eb1310..25cf7940c6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -358,11 +358,13 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders // so that the quantity can be increased. numberOfSellOrders++ + minBaseQuantity := fixedpoint.Max(s.Market.MinNotional.Div(lastPrice), s.Market.MinQuantity) maxBaseQuantity := fixedpoint.Zero for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 { numberOfSellOrders-- maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(numberOfSellOrders))) } + log.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) buyPlacedPrice := fixedpoint.Zero totalQuotePrice := fixedpoint.Zero From e80c8f295974c8ac6e96490b8e9737f55f031844 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 30 Nov 2022 12:57:53 +0800 Subject: [PATCH 0116/1392] grid2: pull out maxNumberOfSellOrders --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 25cf7940c6..76b72d084d 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -357,12 +357,12 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders // so that the quantity can be increased. - numberOfSellOrders++ + maxNumberOfSellOrders := numberOfSellOrders + 1 minBaseQuantity := fixedpoint.Max(s.Market.MinNotional.Div(lastPrice), s.Market.MinQuantity) maxBaseQuantity := fixedpoint.Zero for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 { - numberOfSellOrders-- - maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(numberOfSellOrders))) + maxNumberOfSellOrders-- + maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) } log.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) From 22569fcb30231db819a06778ec62122de166f333 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Dec 2022 14:51:28 +0800 Subject: [PATCH 0117/1392] grid2: fix quantity calculation --- pkg/strategy/grid2/strategy.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 76b72d084d..7fd199a5be 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -360,7 +360,7 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv maxNumberOfSellOrders := numberOfSellOrders + 1 minBaseQuantity := fixedpoint.Max(s.Market.MinNotional.Div(lastPrice), s.Market.MinQuantity) maxBaseQuantity := fixedpoint.Zero - for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 { + for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 || maxBaseQuantity.Compare(minBaseQuantity) <= 0 { maxNumberOfSellOrders-- maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) } @@ -398,7 +398,8 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv } } - return quoteInvestment.Div(totalQuotePrice), nil + quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) + return fixedpoint.Max(quoteSideQuantity, maxBaseQuantity), nil } // setupGridOrders From 29f3ff7ba2e9f023b67b885911d220fd98acca58 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Dec 2022 16:30:44 +0800 Subject: [PATCH 0118/1392] grid2: remove todo --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7fd199a5be..f698ce0ba0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -439,7 +439,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } } else { - // TODO: calculate the quantity from the investment configuration + // calculate the quantity from the investment configuration if !s.QuoteInvestment.IsZero() && !s.BaseInvestment.IsZero() { quantity, err2 := s.calculateQuoteBaseInvestmentQuantity(s.QuoteInvestment, s.BaseInvestment, lastPrice, s.grid.Pins) if err2 != nil { From 2b148038296de94726b9067d40b0823ee39f7950 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 2 Dec 2022 00:09:47 +0800 Subject: [PATCH 0119/1392] grid2: add comment --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f698ce0ba0..889ce038b0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -456,6 +456,8 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } + // if base investment and quote investment is set, when we should check if the + // investment configuration is valid with the current balances if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { if s.BaseInvestment.Compare(totalBase) > 0 { return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", s.BaseInvestment.Float64(), totalBase.Float64()) From 3fc65122b6e95bcef88a3dfb514ef6ecf74e5c1e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 2 Dec 2022 13:46:09 +0800 Subject: [PATCH 0120/1392] config: update grid2 config --- config/grid2.yaml | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/config/grid2.yaml b/config/grid2.yaml index e4338bcfd0..6696201cf1 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -15,6 +15,7 @@ backtest: symbols: - BTCUSDT sessions: [max] + # sessions: [binance] accounts: binance: balances: @@ -29,5 +30,25 @@ exchangeStrategies: upperPrice: 10_000.0 lowerPrice: 15_000.0 gridNumber: 10 - quantity: 0.001 - # profitSpread: 1000.0 # The profit price spread that you want to add to your sell order when your buy order is executed + + ## profitSpread is the profit spread of the arbitrage order (sell order) + ## greater the profitSpread, greater the profit you make when the sell order is filled. + ## you can set this instead of the default grid profit spread. + ## by default, profitSpread = (upperPrice - lowerPrice) / gridNumber + ## that is, greater the gridNumber, lesser the profit of each grid. + # profitSpread: 1000.0 + + ## There are 3 kinds of setup + ## NOTICE: you can only choose one, uncomment the config to enable it + ## + ## 1) fixed amount: amount is the quote unit (e.g. USDT in BTCUSDT) + # amount: 10.0 + + ## 2) fixed quantity: it will use your balance to place orders with the fixed quantity. e.g. 0.001 BTC + # quantity: 0.001 + + ## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid. + ## quoteInvestment is required, and baseInvestment is optional (could be zero) + ## if you have existing BTC position and want to reuse it you can set the baseInvestment. + quoteInvestment: 10_000 + baseInvestment: 1.0 From 26e221cf7e84a4d16ecbae101bb0ed20d79bfc41 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:02:36 +0800 Subject: [PATCH 0121/1392] service: fix backtest test for binance restrict --- pkg/service/backtest_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/service/backtest_test.go b/pkg/service/backtest_test.go index ca863e2871..2d7b5bbb24 100644 --- a/pkg/service/backtest_test.go +++ b/pkg/service/backtest_test.go @@ -3,6 +3,8 @@ package service import ( "context" "database/sql" + "os" + "strconv" "testing" "time" @@ -15,6 +17,10 @@ import ( ) func TestBacktestService_FindMissingTimeRanges_EmptyData(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + db, err := prepareDB(t) if err != nil { t.Fatal(err) @@ -40,6 +46,10 @@ func TestBacktestService_FindMissingTimeRanges_EmptyData(t *testing.T) { } func TestBacktestService_QueryExistingDataRange(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + db, err := prepareDB(t) if err != nil { t.Fatal(err) @@ -67,6 +77,10 @@ func TestBacktestService_QueryExistingDataRange(t *testing.T) { } func TestBacktestService_SyncPartial(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + db, err := prepareDB(t) if err != nil { t.Fatal(err) @@ -113,6 +127,10 @@ func TestBacktestService_SyncPartial(t *testing.T) { } func TestBacktestService_FindMissingTimeRanges(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + db, err := prepareDB(t) if err != nil { t.Fatal(err) From 1e13fe619124ec43779def0bb81a18023d21be26 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:02:55 +0800 Subject: [PATCH 0122/1392] grid2: fix grid2 strategy validation --- pkg/strategy/grid2/strategy.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 889ce038b0..db94b253a5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -100,17 +100,18 @@ func (s *Strategy) Validate() error { return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) } - if s.ProfitSpread.Sign() <= 0 { - // If profitSpread is empty or its value is negative - return fmt.Errorf("profit spread should bigger than 0") - } - if s.GridNum == 0 { return fmt.Errorf("gridNum can not be zero") } if err := s.QuantityOrAmount.Validate(); err != nil { - return err + if s.QuoteInvestment.IsZero() && s.BaseInvestment.IsZero() { + return err + } + } + + if !s.QuantityOrAmount.IsSet() && s.QuoteInvestment.IsZero() && s.BaseInvestment.IsZero() { + return fmt.Errorf("one of quantity, amount, quoteInvestment must be set") } return nil From f5bb22c82da5d3f6df83ebeac3dcfd2519808697 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:16:29 +0800 Subject: [PATCH 0123/1392] config: update grid2 config files --- config/grid2-max.yaml | 50 +++++++++++++++++++++++++++++++++++++++++++ config/grid2.yaml | 14 +++++------- 2 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 config/grid2-max.yaml diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml new file mode 100644 index 0000000000..9fadfa312c --- /dev/null +++ b/config/grid2-max.yaml @@ -0,0 +1,50 @@ +--- +sessions: + max: + exchange: max + envVarPrefix: max + +# example command: +# godotenv -f .env.local -- go run ./cmd/bbgo backtest --config config/grid2-max.yaml --base-asset-baseline +backtest: + startTime: "2022-01-01" + endTime: "2022-11-25" + symbols: + - BTCUSDT + sessions: [max] + accounts: + binance: + balances: + BTC: 0.0 + USDT: 10000.0 + +exchangeStrategies: + +- on: max + grid2: + symbol: BTCUSDT + upperPrice: 15_000.0 + lowerPrice: 10_000.0 + gridNumber: 10 + + ## profitSpread is the profit spread of the arbitrage order (sell order) + ## greater the profitSpread, greater the profit you make when the sell order is filled. + ## you can set this instead of the default grid profit spread. + ## by default, profitSpread = (upperPrice - lowerPrice) / gridNumber + ## that is, greater the gridNumber, lesser the profit of each grid. + # profitSpread: 1000.0 + + ## There are 3 kinds of setup + ## NOTICE: you can only choose one, uncomment the config to enable it + ## + ## 1) fixed amount: amount is the quote unit (e.g. USDT in BTCUSDT) + # amount: 10.0 + + ## 2) fixed quantity: it will use your balance to place orders with the fixed quantity. e.g. 0.001 BTC + # quantity: 0.001 + + ## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid. + ## quoteInvestment is required, and baseInvestment is optional (could be zero) + ## if you have existing BTC position and want to reuse it you can set the baseInvestment. + quoteInvestment: 10_000 + baseInvestment: 1.0 diff --git a/config/grid2.yaml b/config/grid2.yaml index 6696201cf1..a6aa3d9201 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -3,19 +3,15 @@ sessions: binance: exchange: binance envVarPrefix: binance - max: - exchange: max - envVarPrefix: max # example command: -# godotenv -f .env.local -- go run ./cmd/bbgo backtest --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline +# go run ./cmd/bbgo backtest --config config/grid2.yaml --base-asset-baseline backtest: startTime: "2022-01-01" endTime: "2022-11-25" symbols: - BTCUSDT - sessions: [max] - # sessions: [binance] + sessions: [binance] accounts: binance: balances: @@ -24,11 +20,11 @@ backtest: exchangeStrategies: -- on: max +- on: binance grid2: symbol: BTCUSDT - upperPrice: 10_000.0 - lowerPrice: 15_000.0 + upperPrice: 15_000.0 + lowerPrice: 10_000.0 gridNumber: 10 ## profitSpread is the profit spread of the arbitrage order (sell order) From d91921f6c2b9e32e613cc18bd5a9c2698da776f7 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:25:18 +0800 Subject: [PATCH 0124/1392] grid2: fix grid sell order quantity calculation --- pkg/strategy/grid2/strategy.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index db94b253a5..7c3a82dd59 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -340,6 +340,7 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f } func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { + log.Infof("calculating quantity by quote/base investment: %f / %f", baseInvestment.Float64(), quoteInvestment.Float64()) // q_p1 = q_p2 = q_p3 = q_p4 // baseInvestment = q_p1 + q_p2 + q_p3 + q_p4 + .... // baseInvestment = numberOfSellOrders * q @@ -365,7 +366,10 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv maxNumberOfSellOrders-- maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) } - log.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) + log.Infof("grid %s base investment sell orders: %d", s.Symbol, maxNumberOfSellOrders) + if maxNumberOfSellOrders > 0 { + log.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) + } buyPlacedPrice := fixedpoint.Zero totalQuotePrice := fixedpoint.Zero @@ -400,7 +404,11 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv } quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) - return fixedpoint.Max(quoteSideQuantity, maxBaseQuantity), nil + if maxNumberOfSellOrders > 0 { + return fixedpoint.Max(quoteSideQuantity, maxBaseQuantity), nil + } + + return quoteSideQuantity, nil } // setupGridOrders From a71593310631c3906452cfc80aff7710157470de Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:31:44 +0800 Subject: [PATCH 0125/1392] grid2: allocate logger instance for fields --- pkg/strategy/grid2/strategy.go | 20 ++++++++++++-------- pkg/strategy/grid2/strategy_test.go | 6 ++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7c3a82dd59..221af5b4c9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -72,15 +72,14 @@ type Strategy struct { // orderStore is used to store all the created orders, so that we can filter the trades. orderStore *bbgo.OrderStore - // activeOrders is the locally maintained active order book of the maker orders. - activeOrders *bbgo.ActiveOrderBook - tradeCollector *bbgo.TradeCollector orderExecutor *bbgo.GeneralOrderExecutor // groupID is the group ID used for the strategy instance for canceling orders groupID uint32 + + logger *logrus.Entry } func (s *Strategy) ID() string { @@ -133,9 +132,13 @@ func (s *Strategy) handleOrderFilled(o types.Order) { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() + s.logger = log.WithFields(logrus.Fields{ + "symbol": s.Symbol, + }) + s.groupID = util.FNV32(instanceID) - log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.Market) @@ -162,9 +165,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.Sync(ctx, s) // now we can cancel the open orders - log.Infof("canceling active orders...") - if err := session.Exchange.CancelOrders(context.Background(), s.activeOrders.Orders()...); err != nil { - log.WithError(err).Errorf("cancel order error") + s.logger.Infof("canceling active orders...") + + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("graceful order cancel error") } }) @@ -340,7 +344,7 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f } func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { - log.Infof("calculating quantity by quote/base investment: %f / %f", baseInvestment.Float64(), quoteInvestment.Float64()) + s.logger.Infof("calculating quantity by quote/base investment: %f / %f", baseInvestment.Float64(), quoteInvestment.Float64()) // q_p1 = q_p2 = q_p3 = q_p4 // baseInvestment = q_p1 + q_p2 + q_p3 + q_p4 + .... // baseInvestment = numberOfSellOrders * q diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 7e2f064837..fd7b8493fc 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -5,6 +5,7 @@ package grid2 import ( "testing" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/types" @@ -12,6 +13,8 @@ import ( func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { s := &Strategy{ + logger: logrus.NewEntry(logrus.New()), + Market: types.Market{ BaseCurrency: "BTC", QuoteCurrency: "USDT", @@ -47,6 +50,8 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { s := &Strategy{ + + logger: logrus.NewEntry(logrus.New()), Market: types.Market{ BaseCurrency: "BTC", QuoteCurrency: "USDT", @@ -72,6 +77,7 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { s := &Strategy{ + logger: logrus.NewEntry(logrus.New()), Market: types.Market{ BaseCurrency: "BTC", QuoteCurrency: "USDT", From a825ae5d04da5fe56f4cffdf9fce3d438a5ddeeb Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:36:14 +0800 Subject: [PATCH 0126/1392] grid2: use custom logger entry --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 221af5b4c9..dfefe60d81 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -370,9 +370,9 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv maxNumberOfSellOrders-- maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) } - log.Infof("grid %s base investment sell orders: %d", s.Symbol, maxNumberOfSellOrders) + s.logger.Infof("grid %s base investment sell orders: %d", s.Symbol, maxNumberOfSellOrders) if maxNumberOfSellOrders > 0 { - log.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) + s.logger.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) } buyPlacedPrice := fixedpoint.Zero @@ -553,7 +553,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe return err } for _, order := range createdOrders { - log.Infof(order.String()) + s.logger.Infof(order.String()) } } From de398ef146c289327e143b487c4099f2e5872b8c Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 12:31:09 +0800 Subject: [PATCH 0127/1392] config: update grid2 max config --- config/grid2-max.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 9fadfa312c..fb5416da91 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -23,9 +23,9 @@ exchangeStrategies: - on: max grid2: symbol: BTCUSDT - upperPrice: 15_000.0 + upperPrice: 20_000.0 lowerPrice: 10_000.0 - gridNumber: 10 + gridNumber: 50 ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. From d5f8c3e7568082b84f8f726abbf16ad5a85dac8d Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 12:35:04 +0800 Subject: [PATCH 0128/1392] binance: fix binanceapi client test --- pkg/exchange/binance/binanceapi/client_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/exchange/binance/binanceapi/client_test.go b/pkg/exchange/binance/binanceapi/client_test.go index e86bba7832..2c06f858ff 100644 --- a/pkg/exchange/binance/binanceapi/client_test.go +++ b/pkg/exchange/binance/binanceapi/client_test.go @@ -4,6 +4,8 @@ import ( "context" "log" "net/http/httputil" + "os" + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -12,6 +14,10 @@ import ( ) func getTestClientOrSkip(t *testing.T) *RestClient { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + key, secret, ok := testutil.IntegrationTestConfigured(t, "BINANCE") if !ok { t.SkipNow() @@ -101,6 +107,10 @@ func TestClient_NewGetMarginInterestRateHistoryRequest(t *testing.T) { } func TestClient_privateCall(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + key, secret, ok := testutil.IntegrationTestConfigured(t, "BINANCE") if !ok { t.SkipNow() @@ -136,6 +146,10 @@ func TestClient_privateCall(t *testing.T) { } func TestClient_setTimeOffsetFromServer(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + client := NewClient("") err := client.SetTimeOffsetFromServer(context.Background()) assert.NoError(t, err) From 3521d423105dc8fb57e6520b7699b809df10b149 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 12:36:51 +0800 Subject: [PATCH 0129/1392] trendtrader: fix converge lint issue --- pkg/strategy/trendtrader/trend.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/trendtrader/trend.go b/pkg/strategy/trendtrader/trend.go index fb27c8568d..be12f575a9 100644 --- a/pkg/strategy/trendtrader/trend.go +++ b/pkg/strategy/trendtrader/trend.go @@ -2,6 +2,7 @@ package trendtrader import ( "context" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" @@ -149,8 +150,5 @@ func line(p1, p2, p3 float64) int64 { } func converge(mr, ms float64) bool { - if ms > mr { - return true - } - return false + return ms > mr } From 3d77a319fc9cc7ebe840813dfa8c47e85cdf77f4 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 12:42:48 +0800 Subject: [PATCH 0130/1392] github: set default timeout --- .github/workflows/go.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 196516304c..b18cdf76a8 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -9,6 +9,7 @@ on: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 15 strategy: matrix: From 54ffc8cbccd0fb2470c81237894253808d3db0d7 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 14:46:05 +0800 Subject: [PATCH 0131/1392] grid2: add order filled handler --- pkg/strategy/grid2/strategy.go | 43 +++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index dfefe60d81..7ac8bf1bdc 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -126,7 +126,43 @@ func (s *Strategy) InstanceID() string { } func (s *Strategy) handleOrderFilled(o types.Order) { + // check order fee + newSide := types.SideTypeSell + newPrice := o.Price + newQuantity := o.Quantity + + // quantityReduction := fixedpoint.Zero + + switch o.Side { + case types.SideTypeSell: + newSide = types.SideTypeBuy + if pin, ok := s.grid.NextLowerPin(newPrice); ok { + newPrice = fixedpoint.Value(pin) + } + + case types.SideTypeBuy: + newSide = types.SideTypeSell + if pin, ok := s.grid.NextHigherPin(newPrice); ok { + newPrice = fixedpoint.Value(pin) + } + } + orderForm := types.SubmitOrder{ + Symbol: s.Symbol, + Market: s.Market, + Type: types.OrderTypeLimit, + Price: newPrice, + Side: newSide, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", + Quantity: newQuantity, + } + + if createdOrders, err := s.orderExecutor.SubmitOrders(context.Background(), orderForm); err != nil { + s.logger.WithError(err).Errorf("can not submit arbitrage order") + } else { + s.logger.Infof("order created: %+v", createdOrders) + } } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { @@ -155,6 +191,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) + s.orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) s.grid.CalculateArithmeticPins() @@ -502,7 +539,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe if usedBase.Add(quantity).Compare(totalBase) < 0 { submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, - Type: types.OrderTypeLimitMaker, + Type: types.OrderTypeLimit, Side: types.SideTypeSell, Price: price, Quantity: quantity, @@ -517,7 +554,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe nextPrice := fixedpoint.Value(nextPin) submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, - Type: types.OrderTypeLimitMaker, + Type: types.OrderTypeLimit, Side: types.SideTypeBuy, Price: nextPrice, Quantity: quantity, @@ -536,7 +573,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, - Type: types.OrderTypeLimitMaker, + Type: types.OrderTypeLimit, Side: types.SideTypeBuy, Price: price, Quantity: quantity, From c0573210b31b0c08711d9396b3a3d10a3670539b Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 14:58:53 +0800 Subject: [PATCH 0132/1392] grid2: log grid info --- config/grid2-max.yaml | 6 +++--- pkg/strategy/grid2/grid.go | 5 +++++ pkg/strategy/grid2/strategy.go | 8 ++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index fb5416da91..224c09f120 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -23,9 +23,9 @@ exchangeStrategies: - on: max grid2: symbol: BTCUSDT - upperPrice: 20_000.0 - lowerPrice: 10_000.0 - gridNumber: 50 + upperPrice: 18_000.0 + lowerPrice: 12_000.0 + gridNumber: 100 ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index b2939e2238..c533e7421b 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -1,6 +1,7 @@ package grid2 import ( + "fmt" "math" "sort" @@ -185,3 +186,7 @@ func (g *Grid) addPins(pins []Pin) { func (g *Grid) updatePinsCache() { g.pinsCache = buildPinCache(g.Pins) } + +func (g *Grid) String() string { + return fmt.Sprintf("grid: priceRange: %f <=> %f size: %f spread: %f", g.LowerPrice.Float64(), g.UpperPrice.Float64(), g.Size.Float64(), g.Spread.Float64()) +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7ac8bf1bdc..be15a84638 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -126,6 +126,8 @@ func (s *Strategy) InstanceID() string { } func (s *Strategy) handleOrderFilled(o types.Order) { + s.logger.Infof("order filled: %s", o.String()) + // check order fee newSide := types.SideTypeSell newPrice := o.Price @@ -196,6 +198,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) s.grid.CalculateArithmeticPins() + s.logger.Info(s.grid.String()) + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() @@ -407,9 +411,9 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv maxNumberOfSellOrders-- maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) } - s.logger.Infof("grid %s base investment sell orders: %d", s.Symbol, maxNumberOfSellOrders) + s.logger.Infof("grid base investment sell orders: %d", maxNumberOfSellOrders) if maxNumberOfSellOrders > 0 { - s.logger.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) + s.logger.Infof("grid base investment quantity range: %f <=> %f", minBaseQuantity.Float64(), maxBaseQuantity.Float64()) } buyPlacedPrice := fixedpoint.Zero From d5cf1a7311bf0fe052d63eb4a4709c4229f29853 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 15:17:31 +0800 Subject: [PATCH 0133/1392] grid2: log submitOrder --- pkg/strategy/grid2/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index be15a84638..c7dc66d9b6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -126,7 +126,7 @@ func (s *Strategy) InstanceID() string { } func (s *Strategy) handleOrderFilled(o types.Order) { - s.logger.Infof("order filled: %s", o.String()) + s.logger.Infof("GRID ORDER FILLED: %s", o.String()) // check order fee newSide := types.SideTypeSell @@ -160,6 +160,8 @@ func (s *Strategy) handleOrderFilled(o types.Order) { Quantity: newQuantity, } + s.logger.Infof("SUBMIT ORDER: %s", orderForm.String()) + if createdOrders, err := s.orderExecutor.SubmitOrders(context.Background(), orderForm); err != nil { s.logger.WithError(err).Errorf("can not submit arbitrage order") } else { From 9bb628328c25949bd3f277abd55f60aeed81add3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 15:18:47 +0800 Subject: [PATCH 0134/1392] grid2: use profit to buy more inventory --- pkg/strategy/grid2/strategy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c7dc66d9b6..41638fbc97 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -142,6 +142,10 @@ func (s *Strategy) handleOrderFilled(o types.Order) { newPrice = fixedpoint.Value(pin) } + // use the profit to buy more inventory in the grid + quoteQuantity := o.Quantity.Mul(o.Price) + newQuantity = quoteQuantity.Div(newPrice) + case types.SideTypeBuy: newSide = types.SideTypeSell if pin, ok := s.grid.NextHigherPin(newPrice); ok { @@ -156,8 +160,8 @@ func (s *Strategy) handleOrderFilled(o types.Order) { Price: newPrice, Side: newSide, TimeInForce: types.TimeInForceGTC, - Tag: "grid", Quantity: newQuantity, + Tag: "grid", } s.logger.Infof("SUBMIT ORDER: %s", orderForm.String()) From 6ed09c847df064a25854517d583798388437a453 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 15:21:03 +0800 Subject: [PATCH 0135/1392] grid2: add compound mode option --- pkg/strategy/grid2/strategy.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 41638fbc97..f2e181f0c6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -54,6 +54,10 @@ type Strategy struct { LowerPrice fixedpoint.Value `json:"lowerPrice"` + // Compound option is used for buying more inventory when + // the profit is made by the filled sell order. + Compound bool `json:"compound"` + // QuantityOrAmount embeds the Quantity field and the Amount field // If you set up the Quantity field or the Amount field, you don't need to set the QuoteInvestment and BaseInvestment bbgo.QuantityOrAmount @@ -143,8 +147,10 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } // use the profit to buy more inventory in the grid - quoteQuantity := o.Quantity.Mul(o.Price) - newQuantity = quoteQuantity.Div(newPrice) + if s.Compound { + quoteQuantity := o.Quantity.Mul(o.Price) + newQuantity = quoteQuantity.Div(newPrice) + } case types.SideTypeBuy: newSide = types.SideTypeSell From 28d47ba05562f5fb8a322835652b4e2a9d8f61ad Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 15:22:15 +0800 Subject: [PATCH 0136/1392] grid2: update config for compound mode --- config/grid2-max.yaml | 5 +++++ config/grid2.yaml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 224c09f120..f6723a6b74 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -27,6 +27,11 @@ exchangeStrategies: lowerPrice: 12_000.0 gridNumber: 100 + # compound is used for buying more inventory when the profit is made by the filled SELL order. + # when compound is disabled, fixed quantity is used for each grid order. + # default: false + compound: true + ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. ## you can set this instead of the default grid profit spread. diff --git a/config/grid2.yaml b/config/grid2.yaml index a6aa3d9201..19da8c81ec 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -27,6 +27,11 @@ exchangeStrategies: lowerPrice: 10_000.0 gridNumber: 10 + # compound is used for buying more inventory when the profit is made by the filled SELL order. + # when compound is disabled, fixed quantity is used for each grid order. + # default: false + compound: true + ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. ## you can set this instead of the default grid profit spread. From 5f7ad125c68d5962c3ad7ddf51d7fbb1c4c5f23c Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 16:03:01 +0800 Subject: [PATCH 0137/1392] grid2: add earnBase option --- config/grid2-max.yaml | 3 +++ pkg/strategy/grid2/strategy.go | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index f6723a6b74..d48ead43e9 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -32,6 +32,9 @@ exchangeStrategies: # default: false compound: true + stopLossPrice: 10_000.0 + takeProfitPrice: 20_000.0 + ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. ## you can set this instead of the default grid profit spread. diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f2e181f0c6..3d454e7842 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -58,6 +58,11 @@ type Strategy struct { // the profit is made by the filled sell order. Compound bool `json:"compound"` + // EarnBase option is used for earning profit in base currency. + // e.g. earn BTC in BTCUSDT and earn ETH in ETHUSDT + // instead of earn USDT in BTCUSD + EarnBase bool `json:"earnBase"` + // QuantityOrAmount embeds the Quantity field and the Amount field // If you set up the Quantity field or the Amount field, you don't need to set the QuoteInvestment and BaseInvestment bbgo.QuantityOrAmount From 64d8a30ecc0597dd0555e62db1d183b474e9c42a Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 16:40:40 +0800 Subject: [PATCH 0138/1392] grid2: add earnBase option --- pkg/strategy/grid2/strategy.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3d454e7842..67c7555852 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -152,7 +152,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } // use the profit to buy more inventory in the grid - if s.Compound { + if s.Compound || s.EarnBase { quoteQuantity := o.Quantity.Mul(o.Price) newQuantity = quoteQuantity.Div(newPrice) } @@ -162,6 +162,11 @@ func (s *Strategy) handleOrderFilled(o types.Order) { if pin, ok := s.grid.NextHigherPin(newPrice); ok { newPrice = fixedpoint.Value(pin) } + + if s.EarnBase { + quoteQuantity := o.Quantity.Mul(o.Price) + newQuantity = quoteQuantity.Div(newPrice) + } } orderForm := types.SubmitOrder{ From 24910f0b226b231803ca669c55c28f635c555dca Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 16:49:46 +0800 Subject: [PATCH 0139/1392] config: add earnBase option to config --- config/grid2-max.yaml | 10 +++++++--- config/grid2.yaml | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index d48ead43e9..10bb72ea97 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -27,11 +27,15 @@ exchangeStrategies: lowerPrice: 12_000.0 gridNumber: 100 - # compound is used for buying more inventory when the profit is made by the filled SELL order. - # when compound is disabled, fixed quantity is used for each grid order. - # default: false + ## compound is used for buying more inventory when the profit is made by the filled SELL order. + ## when compound is disabled, fixed quantity is used for each grid order. + ## default: false compound: true + ## earnBase is used to profit base quantity instead of quote quantity. + ## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair. + # earnBase: true + stopLossPrice: 10_000.0 takeProfitPrice: 20_000.0 diff --git a/config/grid2.yaml b/config/grid2.yaml index 19da8c81ec..2b46449901 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -27,11 +27,15 @@ exchangeStrategies: lowerPrice: 10_000.0 gridNumber: 10 - # compound is used for buying more inventory when the profit is made by the filled SELL order. - # when compound is disabled, fixed quantity is used for each grid order. - # default: false + ## compound is used for buying more inventory when the profit is made by the filled SELL order. + ## when compound is disabled, fixed quantity is used for each grid order. + ## default: false compound: true + ## earnBase is used to profit base quantity instead of quote quantity. + ## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair. + # earnBase: true + ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. ## you can set this instead of the default grid profit spread. From 175ab289e22eb138958ba4afff50f1d0351799ca Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 16:52:58 +0800 Subject: [PATCH 0140/1392] config: add more doc comment --- config/grid2-max.yaml | 4 +++- config/grid2.yaml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 10bb72ea97..550953ead7 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -20,12 +20,14 @@ backtest: exchangeStrategies: +## You can run the following command to cancel all grid orders if the orders are not successfully canceled: +## go run ./cmd/bbgo --dotenv .env.local.max-staging cancel-order --all --symbol BTCUSDT --config config/grid2-max.yaml - on: max grid2: symbol: BTCUSDT upperPrice: 18_000.0 lowerPrice: 12_000.0 - gridNumber: 100 + gridNumber: 10 ## compound is used for buying more inventory when the profit is made by the filled SELL order. ## when compound is disabled, fixed quantity is used for each grid order. diff --git a/config/grid2.yaml b/config/grid2.yaml index 2b46449901..ad61b7e437 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -35,7 +35,7 @@ exchangeStrategies: ## earnBase is used to profit base quantity instead of quote quantity. ## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair. # earnBase: true - + ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. ## you can set this instead of the default grid profit spread. From 2977c80dd10872e80edb2d48e1731203d9b10971 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 16:59:47 +0800 Subject: [PATCH 0141/1392] grid2: check profitSpread for profit --- pkg/strategy/grid2/strategy.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 67c7555852..7e502044d5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -108,6 +108,14 @@ func (s *Strategy) Validate() error { return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) } + if !s.ProfitSpread.IsZero() { + percent := s.ProfitSpread.Div(s.LowerPrice) + feeRate := fixedpoint.NewFromFloat(0.075 * 0.01) + if percent.Compare(feeRate) < 0 { + return fmt.Errorf("profitSpread %f %s is too small, less than the fee rate: %s", s.ProfitSpread.Float64(), percent.Percentage(), feeRate.Percentage()) + } + } + if s.GridNum == 0 { return fmt.Errorf("gridNum can not be zero") } @@ -147,8 +155,13 @@ func (s *Strategy) handleOrderFilled(o types.Order) { switch o.Side { case types.SideTypeSell: newSide = types.SideTypeBuy - if pin, ok := s.grid.NextLowerPin(newPrice); ok { - newPrice = fixedpoint.Value(pin) + + if !s.ProfitSpread.IsZero() { + newPrice = newPrice.Sub(s.ProfitSpread) + } else { + if pin, ok := s.grid.NextLowerPin(newPrice); ok { + newPrice = fixedpoint.Value(pin) + } } // use the profit to buy more inventory in the grid @@ -159,8 +172,12 @@ func (s *Strategy) handleOrderFilled(o types.Order) { case types.SideTypeBuy: newSide = types.SideTypeSell - if pin, ok := s.grid.NextHigherPin(newPrice); ok { - newPrice = fixedpoint.Value(pin) + if !s.ProfitSpread.IsZero() { + newPrice = newPrice.Add(s.ProfitSpread) + } else { + if pin, ok := s.grid.NextHigherPin(newPrice); ok { + newPrice = fixedpoint.Value(pin) + } } if s.EarnBase { From dd2d48fde03e96a05f1721737a36737d57eee853 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 11:39:43 +0800 Subject: [PATCH 0142/1392] bbgo: handle order cancel event --- pkg/bbgo/activeorderbook.go | 13 +++++++++---- pkg/bbgo/activeorderbook_callbacks.go | 10 ++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 70d15ad43f..f38b3dcd0a 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -17,9 +17,10 @@ const CancelOrderWaitTime = 20 * time.Millisecond // ActiveOrderBook manages the local active order books. //go:generate callbackgen -type ActiveOrderBook type ActiveOrderBook struct { - Symbol string - orders *types.SyncOrderMap - filledCallbacks []func(o types.Order) + Symbol string + orders *types.SyncOrderMap + filledCallbacks []func(o types.Order) + canceledCallbacks []func(o types.Order) // sig is the order update signal // this signal will be emitted when a new order is added or removed. @@ -242,7 +243,11 @@ func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) { b.C.Emit() case types.OrderStatusCanceled, types.OrderStatusRejected: - log.Debugf("[ActiveOrderBook] order status %s, removing order %s", order.Status, order) + if order.Status == types.OrderStatusCanceled { + b.EmitCanceled(order) + } + + log.Debugf("[ActiveOrderBook] order is %s, removing order %s", order.Status, order) b.Remove(order) b.C.Emit() diff --git a/pkg/bbgo/activeorderbook_callbacks.go b/pkg/bbgo/activeorderbook_callbacks.go index 5110476043..014fd6a59b 100644 --- a/pkg/bbgo/activeorderbook_callbacks.go +++ b/pkg/bbgo/activeorderbook_callbacks.go @@ -15,3 +15,13 @@ func (b *ActiveOrderBook) EmitFilled(o types.Order) { cb(o) } } + +func (b *ActiveOrderBook) OnCanceled(cb func(o types.Order)) { + b.canceledCallbacks = append(b.canceledCallbacks, cb) +} + +func (b *ActiveOrderBook) EmitCanceled(o types.Order) { + for _, cb := range b.canceledCallbacks { + cb(o) + } +} From c00d59806f11183fe708711e3b06504559ea40b7 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 11:47:01 +0800 Subject: [PATCH 0143/1392] grid2: add closeGrid option --- pkg/strategy/grid2/strategy.go | 41 ++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7e502044d5..242b521f81 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -73,6 +73,10 @@ type Strategy struct { // BaseInvestment is the total base quantity you want to place as the sell order. BaseInvestment fixedpoint.Value `json:"baseInvestment"` + // CloseWhenCancelOrder option is used to close the grid if any of the order is canceled. + // This option let you simply remote control the grid from the crypto exchange mobile app. + CloseWhenCancelOrder bool `json:"closeWhenCancelOrder"` + grid *Grid ProfitStats *types.ProfitStats `persistence:"profit_stats"` @@ -142,6 +146,31 @@ func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s-%s-%d-%d-%d", ID, s.Symbol, s.GridNum, s.UpperPrice.Int(), s.LowerPrice.Int()) } +func (s *Strategy) CloseGrid(ctx context.Context) error { + bbgo.Sync(ctx, s) + + // now we can cancel the open orders + s.logger.Infof("canceling grid orders...") + + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + return err + } + + return nil +} + +func (s *Strategy) handleOrderCanceled(o types.Order) { + s.logger.Infof("GRID ORDER CANCELED: %s", o.String()) + + ctx := context.Background() + if s.CloseWhenCancelOrder { + s.logger.Infof("one of the grid orders is canceled, now closing grid...") + if err := s.CloseGrid(ctx); err != nil { + s.logger.WithError(err).Errorf("graceful order cancel error") + } + } +} + func (s *Strategy) handleOrderFilled(o types.Order) { s.logger.Infof("GRID ORDER FILLED: %s", o.String()) @@ -241,20 +270,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - - bbgo.Sync(ctx, s) - - // now we can cancel the open orders - s.logger.Infof("canceling active orders...") - - if err := s.orderExecutor.GracefulCancel(ctx); err != nil { - log.WithError(err).Errorf("graceful order cancel error") + if err := s.CloseGrid(ctx); err != nil { + s.logger.WithError(err).Errorf("grid graceful order cancel error") } }) session.UserDataStream.OnStart(func() { if err := s.setupGridOrders(ctx, session); err != nil { - log.WithError(err).Errorf("failed to setup grid orders") + s.logger.WithError(err).Errorf("failed to setup grid orders") } }) From 7dc3c448bb22b92185f7ec6944002bb70db77917 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 11:47:30 +0800 Subject: [PATCH 0144/1392] grid2: remove unused fields --- pkg/strategy/grid2/strategy.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 242b521f81..ed9311167f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -82,11 +82,6 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` Position *types.Position `persistence:"position"` - // orderStore is used to store all the created orders, so that we can filter the trades. - orderStore *bbgo.OrderStore - - tradeCollector *bbgo.TradeCollector - orderExecutor *bbgo.GeneralOrderExecutor // groupID is the group ID used for the strategy instance for canceling orders From c77bb83b95c781b196ee334d189d2d26a6a97956 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 12:58:01 +0800 Subject: [PATCH 0145/1392] grid2: move OpenGrid method and add KeepOrdersWhenShutdown --- pkg/strategy/grid2/strategy.go | 137 ++++++++++++++++++--------------- 1 file changed, 73 insertions(+), 64 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index ed9311167f..bae5e3ee78 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -77,6 +77,9 @@ type Strategy struct { // This option let you simply remote control the grid from the crypto exchange mobile app. CloseWhenCancelOrder bool `json:"closeWhenCancelOrder"` + // KeepOrdersWhenShutdown option is used for keeping the grid orders when shutting down bbgo + KeepOrdersWhenShutdown bool `json:"keepOrdersWhenShutdown"` + grid *Grid ProfitStats *types.ProfitStats `persistence:"profit_stats"` @@ -141,19 +144,6 @@ func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s-%s-%d-%d-%d", ID, s.Symbol, s.GridNum, s.UpperPrice.Int(), s.LowerPrice.Int()) } -func (s *Strategy) CloseGrid(ctx context.Context) error { - bbgo.Sync(ctx, s) - - // now we can cancel the open orders - s.logger.Infof("canceling grid orders...") - - if err := s.orderExecutor.GracefulCancel(ctx); err != nil { - return err - } - - return nil -} - func (s *Strategy) handleOrderCanceled(o types.Order) { s.logger.Infof("GRID ORDER CANCELED: %s", o.String()) @@ -230,55 +220,6 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } } -func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - instanceID := s.InstanceID() - - s.logger = log.WithFields(logrus.Fields{ - "symbol": s.Symbol, - }) - - s.groupID = util.FNV32(instanceID) - - s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) - - if s.ProfitStats == nil { - s.ProfitStats = types.NewProfitStats(s.Market) - } - - if s.Position == nil { - s.Position = types.NewPositionFromMarket(s.Market) - } - - s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) - s.orderExecutor.BindEnvironment(s.Environment) - s.orderExecutor.BindProfitStats(s.ProfitStats) - s.orderExecutor.Bind() - s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { - bbgo.Sync(ctx, s) - }) - s.orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) - - s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) - s.grid.CalculateArithmeticPins() - - s.logger.Info(s.grid.String()) - - bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { - defer wg.Done() - if err := s.CloseGrid(ctx); err != nil { - s.logger.WithError(err).Errorf("grid graceful order cancel error") - } - }) - - session.UserDataStream.OnStart(func() { - if err := s.setupGridOrders(ctx, session); err != nil { - s.logger.WithError(err).Errorf("failed to setup grid orders") - } - }) - - return nil -} - type InvestmentBudget struct { baseInvestment fixedpoint.Value quoteInvestment fixedpoint.Value @@ -513,10 +454,24 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv return quoteSideQuantity, nil } -// setupGridOrders +// CloseGrid closes the grid orders +func (s *Strategy) CloseGrid(ctx context.Context) error { + bbgo.Sync(ctx, s) + + // now we can cancel the open orders + s.logger.Infof("canceling grid orders...") + + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + return err + } + + return nil +} + +// OpenGrid // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. -func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { +func (s *Strategy) OpenGrid(ctx context.Context, session *bbgo.ExchangeSession) error { lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { return errors.Wrap(err, "failed to get the last trade price") @@ -684,3 +639,57 @@ func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.Exchange return fixedpoint.Zero, fmt.Errorf("%s ticker price not found", s.Symbol) } + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + + s.logger = log.WithFields(logrus.Fields{ + "symbol": s.Symbol, + }) + + s.groupID = util.FNV32(instanceID) + + s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.Bind() + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) + + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + + s.logger.Info(s.grid.String()) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + if s.KeepOrdersWhenShutdown { + return + } + + if err := s.CloseGrid(ctx); err != nil { + s.logger.WithError(err).Errorf("grid graceful order cancel error") + } + }) + + session.UserDataStream.OnStart(func() { + if err := s.OpenGrid(ctx, session); err != nil { + s.logger.WithError(err).Errorf("failed to setup grid orders") + } + }) + + return nil +} From 7abc799da46f55336be5690648cd9e7d76d928d2 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 13:04:14 +0800 Subject: [PATCH 0146/1392] grid2: make openGrid and closeGrid as private method --- pkg/strategy/grid2/strategy.go | 39 ++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index bae5e3ee78..7b8c768142 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -80,6 +80,10 @@ type Strategy struct { // KeepOrdersWhenShutdown option is used for keeping the grid orders when shutting down bbgo KeepOrdersWhenShutdown bool `json:"keepOrdersWhenShutdown"` + // ClearOpenOrdersWhenStart + // If this is set, when bbgo started, it will clear the open orders in the same market (by symbol) + ClearOpenOrdersWhenStart bool `json:"clearOpenOrdersWhenStart"` + grid *Grid ProfitStats *types.ProfitStats `persistence:"profit_stats"` @@ -150,7 +154,7 @@ func (s *Strategy) handleOrderCanceled(o types.Order) { ctx := context.Background() if s.CloseWhenCancelOrder { s.logger.Infof("one of the grid orders is canceled, now closing grid...") - if err := s.CloseGrid(ctx); err != nil { + if err := s.closeGrid(ctx); err != nil { s.logger.WithError(err).Errorf("graceful order cancel error") } } @@ -454,8 +458,8 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv return quoteSideQuantity, nil } -// CloseGrid closes the grid orders -func (s *Strategy) CloseGrid(ctx context.Context) error { +// closeGrid closes the grid orders +func (s *Strategy) closeGrid(ctx context.Context) error { bbgo.Sync(ctx, s) // now we can cancel the open orders @@ -468,10 +472,10 @@ func (s *Strategy) CloseGrid(ctx context.Context) error { return nil } -// OpenGrid +// openGrid // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. -func (s *Strategy) OpenGrid(ctx context.Context, session *bbgo.ExchangeSession) error { +func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) error { lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { return errors.Wrap(err, "failed to get the last trade price") @@ -613,6 +617,21 @@ func (s *Strategy) OpenGrid(ctx context.Context, session *bbgo.ExchangeSession) return nil } +func (s *Strategy) clearOpenOrders(ctx context.Context, session *bbgo.ExchangeSession) error { + // clear open orders when start + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } + + err = session.Exchange.CancelOrders(ctx, openOrders...) + if err != nil { + return err + } + + return nil +} + func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.ExchangeSession) (fixedpoint.Value, error) { if bbgo.IsBackTesting { price, ok := session.LastPrice(s.Symbol) @@ -668,6 +687,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) + if s.ClearOpenOrdersWhenStart { + if err := s.clearOpenOrders(ctx, session); err != nil { + return err + } + } + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) s.grid.CalculateArithmeticPins() @@ -680,13 +705,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - if err := s.CloseGrid(ctx); err != nil { + if err := s.closeGrid(ctx); err != nil { s.logger.WithError(err).Errorf("grid graceful order cancel error") } }) session.UserDataStream.OnStart(func() { - if err := s.OpenGrid(ctx, session); err != nil { + if err := s.openGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("failed to setup grid orders") } }) From 5148fadf67f8a4b91aca0cfae11ab7fc172598f3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 14:22:01 +0800 Subject: [PATCH 0147/1392] types: remove duplciated klineCallback type --- pkg/types/kline.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 0ca77696e3..9f8dd5d724 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -544,7 +544,7 @@ func (k KLineWindow) SlackAttachment() slack.Attachment { } } -type KLineCallback func(kline KLine) +type KLineCallback func(k KLine) type KValueType int @@ -642,9 +642,7 @@ func (k *KLineSeries) Length() int { var _ Series = &KLineSeries{} -type KLineCallBack func(k KLine) - -func KLineWith(symbol string, interval Interval, callback KLineCallBack) KLineCallBack { +func KLineWith(symbol string, interval Interval, callback KLineCallback) KLineCallback { return func(k KLine) { if k.Symbol != symbol || (k.Interval != "" && k.Interval != interval) { return From 0ea6dfb1584ca5c8dd3a98406471392a329a76ad Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 14:22:11 +0800 Subject: [PATCH 0148/1392] grid2: add triggerPrice protection --- pkg/strategy/grid2/strategy.go | 39 ++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7b8c768142..6436956a26 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -73,6 +73,10 @@ type Strategy struct { // BaseInvestment is the total base quantity you want to place as the sell order. BaseInvestment fixedpoint.Value `json:"baseInvestment"` + TriggerPrice fixedpoint.Value `json:"triggerPrice"` + StopLossPrice fixedpoint.Value `json:"stopLossPrice"` + TakeProfitPrice fixedpoint.Value `json:"takeProfitPrice"` + // CloseWhenCancelOrder option is used to close the grid if any of the order is canceled. // This option let you simply remote control the grid from the crypto exchange mobile app. CloseWhenCancelOrder bool `json:"closeWhenCancelOrder"` @@ -140,7 +144,7 @@ func (s *Strategy) Validate() error { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) } // InstanceID returns the instance identifier from the current grid configuration parameters @@ -458,6 +462,18 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv return quoteSideQuantity, nil } +func (s *Strategy) newTriggerPriceHandler(ctx context.Context, session *bbgo.ExchangeSession) types.KLineCallback { + return types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { + if s.TriggerPrice.Compare(k.High) > 0 || s.TriggerPrice.Compare(k.Low) < 0 { + return + } + + if err := s.openGrid(ctx, session); err != nil { + s.logger.WithError(err).Errorf("failed to setup grid orders") + } + }) +} + // closeGrid closes the grid orders func (s *Strategy) closeGrid(ctx context.Context) error { bbgo.Sync(ctx, s) @@ -476,6 +492,14 @@ func (s *Strategy) closeGrid(ctx context.Context) error { // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) error { + if s.grid != nil { + return nil + } + + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + s.logger.Info(s.grid.String()) + lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { return errors.Wrap(err, "failed to get the last trade price") @@ -693,11 +717,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } - s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) - s.grid.CalculateArithmeticPins() - - s.logger.Info(s.grid.String()) - bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() @@ -710,7 +729,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) + if !s.TriggerPrice.IsZero() { + session.MarketDataStream.OnKLineClosed(s.newTriggerPriceHandler(ctx, session)) + } + session.UserDataStream.OnStart(func() { + if !s.TriggerPrice.IsZero() { + return + } + if err := s.openGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("failed to setup grid orders") } From dc2ce372c4fac3449cf2641e34ee1c542c55f25e Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 14:23:00 +0800 Subject: [PATCH 0149/1392] grid2: reset grid field when it's closed --- pkg/strategy/grid2/strategy.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6436956a26..efa1b858e8 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -165,6 +165,10 @@ func (s *Strategy) handleOrderCanceled(o types.Order) { } func (s *Strategy) handleOrderFilled(o types.Order) { + if s.grid == nil { + return + } + s.logger.Infof("GRID ORDER FILLED: %s", o.String()) // check order fee @@ -485,6 +489,8 @@ func (s *Strategy) closeGrid(ctx context.Context) error { return err } + // free the grid object + s.grid = nil return nil } From 8d601a6cb47543484c79514ab3283c11311a4bbc Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 14:24:04 +0800 Subject: [PATCH 0150/1392] grid2: add exchange session field --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index efa1b858e8..3aa2864246 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -88,11 +88,11 @@ type Strategy struct { // If this is set, when bbgo started, it will clear the open orders in the same market (by symbol) ClearOpenOrdersWhenStart bool `json:"clearOpenOrdersWhenStart"` - grid *Grid - ProfitStats *types.ProfitStats `persistence:"profit_stats"` Position *types.Position `persistence:"position"` + grid *Grid + session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor // groupID is the group ID used for the strategy instance for canceling orders @@ -692,12 +692,12 @@ func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.Exchange func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() + s.session = session s.logger = log.WithFields(logrus.Fields{ "symbol": s.Symbol, }) s.groupID = util.FNV32(instanceID) - s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) if s.ProfitStats == nil { From 9506516ea3fe0d3db6ef02c97f83144171bc9933 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 14:45:04 +0800 Subject: [PATCH 0151/1392] grid2: add grid profit stats to the strategy --- pkg/strategy/grid2/strategy.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3aa2864246..d8c6179407 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -25,7 +25,12 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +type GridProfit struct { + Profit fixedpoint.Value +} + type GridProfitStats struct { + Symbol string `json:"symbol"` TotalProfit fixedpoint.Value `json:"totalProfit"` FloatProfit fixedpoint.Value `json:"floatProfit"` GridProfit fixedpoint.Value `json:"gridProfit"` @@ -34,6 +39,12 @@ type GridProfitStats struct { Volume fixedpoint.Value `json:"volume"` } +func newGridProfitStats(symbol string) *GridProfitStats { + return &GridProfitStats{ + Symbol: symbol, + } +} + type Strategy struct { Environment *bbgo.Environment @@ -88,8 +99,9 @@ type Strategy struct { // If this is set, when bbgo started, it will clear the open orders in the same market (by symbol) ClearOpenOrdersWhenStart bool `json:"clearOpenOrdersWhenStart"` - ProfitStats *types.ProfitStats `persistence:"profit_stats"` - Position *types.Position `persistence:"position"` + GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + Position *types.Position `persistence:"position"` grid *Grid session *bbgo.ExchangeSession @@ -171,6 +183,8 @@ func (s *Strategy) handleOrderFilled(o types.Order) { s.logger.Infof("GRID ORDER FILLED: %s", o.String()) + var profit *GridProfit = nil + // check order fee newSide := types.SideTypeSell newPrice := o.Price @@ -700,6 +714,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.groupID = util.FNV32(instanceID) s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + if s.GridProfitStats == nil { + s.GridProfitStats = newGridProfitStats(s.Symbol) + } + if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.Market) } From bf62fb7d2d675076b81ee25454e1f3cf76833756 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 15:01:52 +0800 Subject: [PATCH 0152/1392] grid2: calculate grid profit --- pkg/strategy/grid2/strategy.go | 39 +++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index d8c6179407..46f570b826 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "sync" + "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -26,7 +27,9 @@ func init() { } type GridProfit struct { - Profit fixedpoint.Value + Currency string + Profit fixedpoint.Value + Time time.Time } type GridProfitStats struct { @@ -176,6 +179,30 @@ func (s *Strategy) handleOrderCanceled(o types.Order) { } } +// TODO: consider order fee +func (s *Strategy) calculateProfit(o types.Order, buyPrice, buyQuantity fixedpoint.Value) *GridProfit { + if s.EarnBase { + // sell quantity - buy quantity + profitQuantity := o.Quantity.Sub(buyQuantity) + profit := &GridProfit{ + Currency: s.Market.BaseCurrency, + Profit: profitQuantity, + Time: o.UpdateTime.Time(), + } + return profit + } + + // earn quote + // (sell_price - buy_price) * quantity + profitQuantity := o.Price.Sub(buyPrice).Mul(o.Quantity) + profit := &GridProfit{ + Currency: s.Market.QuoteCurrency, + Profit: profitQuantity, + Time: o.UpdateTime.Time(), + } + return profit +} + func (s *Strategy) handleOrderFilled(o types.Order) { if s.grid == nil { return @@ -183,14 +210,13 @@ func (s *Strategy) handleOrderFilled(o types.Order) { s.logger.Infof("GRID ORDER FILLED: %s", o.String()) - var profit *GridProfit = nil + // var profit *GridProfit = nil // check order fee newSide := types.SideTypeSell newPrice := o.Price newQuantity := o.Quantity - - // quantityReduction := fixedpoint.Zero + quoteQuantity := o.Quantity.Mul(o.Price) switch o.Side { case types.SideTypeSell: @@ -206,10 +232,13 @@ func (s *Strategy) handleOrderFilled(o types.Order) { // use the profit to buy more inventory in the grid if s.Compound || s.EarnBase { - quoteQuantity := o.Quantity.Mul(o.Price) newQuantity = quoteQuantity.Div(newPrice) } + // calculate profit + profit := s.calculateProfit(o, newPrice, newQuantity) + s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) + case types.SideTypeBuy: newSide = types.SideTypeSell if !s.ProfitSpread.IsZero() { From bc4c22f633dd562d9d309bc89369bd0b8248ef95 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 15:15:16 +0800 Subject: [PATCH 0153/1392] grid2: pull out quoteQuantity --- pkg/strategy/grid2/strategy.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 46f570b826..aa55caed46 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -216,7 +216,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { newSide := types.SideTypeSell newPrice := o.Price newQuantity := o.Quantity - quoteQuantity := o.Quantity.Mul(o.Price) + orderQuoteQuantity := o.Quantity.Mul(o.Price) switch o.Side { case types.SideTypeSell: @@ -232,7 +232,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { // use the profit to buy more inventory in the grid if s.Compound || s.EarnBase { - newQuantity = quoteQuantity.Div(newPrice) + newQuantity = orderQuoteQuantity.Div(newPrice) } // calculate profit @@ -250,8 +250,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } if s.EarnBase { - quoteQuantity := o.Quantity.Mul(o.Price) - newQuantity = quoteQuantity.Div(newPrice) + newQuantity = orderQuoteQuantity.Div(newPrice) } } From a8fe55c284ddd201db18a5782aec6c75e83ffa97 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 15:24:13 +0800 Subject: [PATCH 0154/1392] grid2: push profit into stats --- pkg/strategy/grid2/profit.go | 45 ++++++++++++++++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 26 ++------------------ 2 files changed, 47 insertions(+), 24 deletions(-) create mode 100644 pkg/strategy/grid2/profit.go diff --git a/pkg/strategy/grid2/profit.go b/pkg/strategy/grid2/profit.go new file mode 100644 index 0000000000..f983086c62 --- /dev/null +++ b/pkg/strategy/grid2/profit.go @@ -0,0 +1,45 @@ +package grid2 + +import ( + "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type GridProfit struct { + Currency string + Profit fixedpoint.Value + Time time.Time +} + +type GridProfitStats struct { + Symbol string `json:"symbol"` + TotalBaseProfit fixedpoint.Value `json:"totalBaseProfit"` + TotalQuoteProfit fixedpoint.Value `json:"totalQuoteProfit"` + FloatProfit fixedpoint.Value `json:"floatProfit"` + GridProfit fixedpoint.Value `json:"gridProfit"` + ArbitrageCount int `json:"arbitrageCount"` + TotalFee fixedpoint.Value `json:"totalFee"` + Volume fixedpoint.Value `json:"volume"` + Market types.Market `json:"market"` + ProfitEntries []*GridProfit `json:"profitEntries"` +} + +func newGridProfitStats(market types.Market) *GridProfitStats { + return &GridProfitStats{ + Symbol: market.Symbol, + Market: market, + } +} + +func (s *GridProfitStats) AddProfit(profit *GridProfit) { + switch profit.Currency { + case s.Market.QuoteCurrency: + s.TotalQuoteProfit = s.TotalQuoteProfit.Add(profit.Profit) + case s.Market.BaseCurrency: + s.TotalBaseProfit = s.TotalBaseProfit.Add(profit.Profit) + } + + s.ProfitEntries = append(s.ProfitEntries, profit) +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index aa55caed46..bc4fef7ed0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "sync" - "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -26,28 +25,6 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } -type GridProfit struct { - Currency string - Profit fixedpoint.Value - Time time.Time -} - -type GridProfitStats struct { - Symbol string `json:"symbol"` - TotalProfit fixedpoint.Value `json:"totalProfit"` - FloatProfit fixedpoint.Value `json:"floatProfit"` - GridProfit fixedpoint.Value `json:"gridProfit"` - ArbitrageCount int `json:"arbitrageCount"` - TotalFee fixedpoint.Value `json:"totalFee"` - Volume fixedpoint.Value `json:"volume"` -} - -func newGridProfitStats(symbol string) *GridProfitStats { - return &GridProfitStats{ - Symbol: symbol, - } -} - type Strategy struct { Environment *bbgo.Environment @@ -238,6 +215,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { // calculate profit profit := s.calculateProfit(o, newPrice, newQuantity) s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) + s.GridProfitStats.AddProfit(profit) case types.SideTypeBuy: newSide = types.SideTypeSell @@ -743,7 +721,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) if s.GridProfitStats == nil { - s.GridProfitStats = newGridProfitStats(s.Symbol) + s.GridProfitStats = newGridProfitStats(s.Market) } if s.ProfitStats == nil { From 813f9c45a77d11291fa1313ddbe0aff2871998da Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 15:24:59 +0800 Subject: [PATCH 0155/1392] grid2: add order object into the profit structure --- pkg/strategy/grid2/profit.go | 7 ++++--- pkg/strategy/grid2/strategy.go | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/profit.go b/pkg/strategy/grid2/profit.go index f983086c62..941bee39f4 100644 --- a/pkg/strategy/grid2/profit.go +++ b/pkg/strategy/grid2/profit.go @@ -8,9 +8,10 @@ import ( ) type GridProfit struct { - Currency string - Profit fixedpoint.Value - Time time.Time + Currency string `json:"currency"` + Profit fixedpoint.Value `json:"profit"` + Time time.Time `json:"time"` + Order types.Order `json:"order"` } type GridProfitStats struct { diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index bc4fef7ed0..d661723f84 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -165,6 +165,7 @@ func (s *Strategy) calculateProfit(o types.Order, buyPrice, buyQuantity fixedpoi Currency: s.Market.BaseCurrency, Profit: profitQuantity, Time: o.UpdateTime.Time(), + Order: o, } return profit } @@ -176,6 +177,7 @@ func (s *Strategy) calculateProfit(o types.Order, buyPrice, buyQuantity fixedpoi Currency: s.Market.QuoteCurrency, Profit: profitQuantity, Time: o.UpdateTime.Time(), + Order: o, } return profit } From 92733411a36988d7f4d584f08e674f075c48bb07 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 15:34:24 +0800 Subject: [PATCH 0156/1392] config: add notification settings to grid2 --- config/grid2-max.yaml | 15 ++++++++++++--- config/grid2.yaml | 9 +++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 550953ead7..fd26f398af 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -1,4 +1,13 @@ --- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + switches: + trade: false + orderUpdate: false + submitOrder: false + sessions: max: exchange: max @@ -11,7 +20,7 @@ backtest: endTime: "2022-11-25" symbols: - BTCUSDT - sessions: [max] + sessions: [ max ] accounts: binance: balances: @@ -26,8 +35,8 @@ exchangeStrategies: grid2: symbol: BTCUSDT upperPrice: 18_000.0 - lowerPrice: 12_000.0 - gridNumber: 10 + lowerPrice: 13_000.0 + gridNumber: 100 ## compound is used for buying more inventory when the profit is made by the filled SELL order. ## when compound is disabled, fixed quantity is used for each grid order. diff --git a/config/grid2.yaml b/config/grid2.yaml index ad61b7e437..dafc18bcc8 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -1,4 +1,13 @@ --- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + switches: + trade: false + orderUpdate: false + submitOrder: false + sessions: binance: exchange: binance From bbab8728e366fdbaf80e1c0b2df106d278acd6df Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 15:43:27 +0800 Subject: [PATCH 0157/1392] grid2: add orderQueryService for querying order trades --- pkg/strategy/grid2/strategy.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index d661723f84..0877410421 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -83,8 +83,10 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` Position *types.Position `persistence:"position"` - grid *Grid - session *bbgo.ExchangeSession + grid *Grid + session *bbgo.ExchangeSession + orderQueryService types.ExchangeOrderQueryService + orderExecutor *bbgo.GeneralOrderExecutor // groupID is the group ID used for the strategy instance for canceling orders @@ -715,6 +717,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se instanceID := s.InstanceID() s.session = session + + if service, ok := session.Exchange.(types.ExchangeOrderQueryService); ok { + s.orderQueryService = service + } + s.logger = log.WithFields(logrus.Fields{ "symbol": s.Symbol, }) From 427daba6d0f1af9656da18b61d3f9467359329f2 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 15:56:35 +0800 Subject: [PATCH 0158/1392] grid2: change fee rate validation --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 0877410421..36d1558df5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -114,7 +114,7 @@ func (s *Strategy) Validate() error { if !s.ProfitSpread.IsZero() { percent := s.ProfitSpread.Div(s.LowerPrice) - feeRate := fixedpoint.NewFromFloat(0.075 * 0.01) + feeRate := fixedpoint.NewFromFloat(0.1 * 0.01) // 0.1%, 0.075% with BNB if percent.Compare(feeRate) < 0 { return fmt.Errorf("profitSpread %f %s is too small, less than the fee rate: %s", s.ProfitSpread.Float64(), percent.Percentage(), feeRate.Percentage()) } From 5344b3d768e4acc6afb2f7c72c2c7ee3bd786573 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 17:35:35 +0800 Subject: [PATCH 0159/1392] grid2: add TestStrategy_calculateProfit test --- config/grid2-max.yaml | 3 +- pkg/strategy/grid2/strategy_test.go | 67 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index fd26f398af..779ef5d761 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -47,6 +47,7 @@ exchangeStrategies: ## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair. # earnBase: true + triggerPrice: 17_000.0 stopLossPrice: 10_000.0 takeProfitPrice: 20_000.0 @@ -70,4 +71,4 @@ exchangeStrategies: ## quoteInvestment is required, and baseInvestment is optional (could be zero) ## if you have existing BTC position and want to reuse it you can set the baseInvestment. quoteInvestment: 10_000 - baseInvestment: 1.0 + # baseInvestment: 1.0 diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index fd7b8493fc..8c99f487c1 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -101,3 +101,70 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { assert.Equal(t, number(0.2).String(), quantity.String()) }) } + +func newTestStrategy() *Strategy { + market := types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + } + s := &Strategy{ + logger: logrus.NewEntry(logrus.New()), + Market: market, + GridProfitStats: newGridProfitStats(market), + } + return s +} + +func TestStrategy_calculateProfit(t *testing.T) { + t.Run("earn quote without compound", func(t *testing.T) { + s := newTestStrategy() + profit := s.calculateProfit(types.Order{ + SubmitOrder: types.SubmitOrder{ + Price: number(13_000), + Quantity: number(1.0), + }, + }, number(12_000), number(1.0)) + assert.NotNil(t, profit) + assert.Equal(t, "USDT", profit.Currency) + assert.InDelta(t, 1000.0, profit.Profit.Float64(), 0.1) + }) + + t.Run("earn quote with compound", func(t *testing.T) { + s := newTestStrategy() + s.Compound = true + + profit := s.calculateProfit(types.Order{ + SubmitOrder: types.SubmitOrder{ + Price: number(13_000), + Quantity: number(1.0), + }, + }, number(12_000), number(1.0)) + assert.NotNil(t, profit) + assert.Equal(t, "USDT", profit.Currency) + assert.InDelta(t, 1000.0, profit.Profit.Float64(), 0.1) + }) + + t.Run("earn base without compound", func(t *testing.T) { + s := newTestStrategy() + s.EarnBase = true + s.Compound = false + + quoteQuantity := number(12_000).Mul(number(1.0)) + sellQuantity := quoteQuantity.Div(number(13_000.0)) + + buyOrder := types.SubmitOrder{ + Price: number(12_000.0), + Quantity: number(1.0), + } + + profit := s.calculateProfit(types.Order{ + SubmitOrder: types.SubmitOrder{ + Price: number(13_000.0), + Quantity: sellQuantity, + }, + }, buyOrder.Price, buyOrder.Quantity) + assert.NotNil(t, profit) + assert.Equal(t, "BTC", profit.Currency) + assert.InDelta(t, sellQuantity.Float64()-buyOrder.Quantity.Float64(), profit.Profit.Float64(), 0.001) + }) +} From bec1103a643b5ef436b21de1d04107ed8ca16b8b Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 17:36:35 +0800 Subject: [PATCH 0160/1392] grid2: add more parameters to the test strategy --- pkg/strategy/grid2/strategy_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 8c99f487c1..1c1b68c24b 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -107,10 +107,14 @@ func newTestStrategy() *Strategy { BaseCurrency: "BTC", QuoteCurrency: "USDT", } + s := &Strategy{ logger: logrus.NewEntry(logrus.New()), Market: market, GridProfitStats: newGridProfitStats(market), + UpperPrice: number(20_000), + LowerPrice: number(10_000), + GridNum: 10, } return s } From e1f7d8b965bee0809e5a8ac7e1d43a75500be307 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 17:48:43 +0800 Subject: [PATCH 0161/1392] add data/bbgo_test.sql to git lfs --- config/grid2.yaml | 4 ++-- doc/topics/strategy-testing.md | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 doc/topics/strategy-testing.md diff --git a/config/grid2.yaml b/config/grid2.yaml index dafc18bcc8..32264b0536 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -16,8 +16,8 @@ sessions: # example command: # go run ./cmd/bbgo backtest --config config/grid2.yaml --base-asset-baseline backtest: - startTime: "2022-01-01" - endTime: "2022-11-25" + startTime: "2022-06-01" + endTime: "2022-06-30" symbols: - BTCUSDT sessions: [binance] diff --git a/doc/topics/strategy-testing.md b/doc/topics/strategy-testing.md new file mode 100644 index 0000000000..05f628c507 --- /dev/null +++ b/doc/topics/strategy-testing.md @@ -0,0 +1,18 @@ +## Strategy Testing + +A pre-built small backtest data db file is located at `data/bbgo_test.sql`. + +You can use this file for environments without networking to test your strategy. + +A small backtest data set is synchronized in the database: + +- exchange: binance +- symbol: BTCUSDT +- startDate: 2022-06-01 +- endDate: 2022-06-30 + +To import the database, you can do: + +```shell +mysql -uroot -pYOUR_PASSWORD < data/bbgo_test.sql +``` From 7abeb3c59e539fd1d3ae46acf5f7d2cfce7a0aa6 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 17:51:28 +0800 Subject: [PATCH 0162/1392] doc: update strategy testing doc --- doc/topics/strategy-testing.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/topics/strategy-testing.md b/doc/topics/strategy-testing.md index 05f628c507..7aaaa979cb 100644 --- a/doc/topics/strategy-testing.md +++ b/doc/topics/strategy-testing.md @@ -1,6 +1,6 @@ ## Strategy Testing -A pre-built small backtest data db file is located at `data/bbgo_test.sql`. +A pre-built small backtest data db file is located at `data/bbgo_test.sql`, which contains 30days BTCUSDT kline data from binance. You can use this file for environments without networking to test your strategy. @@ -11,6 +11,12 @@ A small backtest data set is synchronized in the database: - startDate: 2022-06-01 - endDate: 2022-06-30 +The SQL file is added via git-lfs, so you need to install git-lfs first: + +```shell +git lfs install +``` + To import the database, you can do: ```shell From 3d29c24e7b5c48f84b24a6b307dedc3a9f18f7e8 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 17:53:20 +0800 Subject: [PATCH 0163/1392] add bbgo_test.sqlite3 file From 43b6ef62430ff3f4b45db932554c72db25362714 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 17:56:10 +0800 Subject: [PATCH 0164/1392] doc: add doc for sqlite3 --- doc/topics/strategy-testing.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/doc/topics/strategy-testing.md b/doc/topics/strategy-testing.md index 7aaaa979cb..468125ef10 100644 --- a/doc/topics/strategy-testing.md +++ b/doc/topics/strategy-testing.md @@ -1,6 +1,8 @@ -## Strategy Testing +# Strategy Testing -A pre-built small backtest data db file is located at `data/bbgo_test.sql`, which contains 30days BTCUSDT kline data from binance. +A pre-built small backtest data mysql database file is located at `data/bbgo_test.sql`, which contains 30days BTCUSDT kline data from binance. + +for SQLite, it's `data/bbgo_test.sqlite3`. You can use this file for environments without networking to test your strategy. @@ -17,8 +19,32 @@ The SQL file is added via git-lfs, so you need to install git-lfs first: git lfs install ``` -To import the database, you can do: +## Testing with MySQL + +To import the SQL file into your MySQL database, you can do: ```shell mysql -uroot -pYOUR_PASSWORD < data/bbgo_test.sql ``` + +Setup your database correctly: + +```shell +DB_DRIVER=mysql +DB_DSN=root:123123@tcp(127.0.0.1:3306)/bbgo +``` + +## Testing with SQLite3 + +Create your own sqlite3 database copy in local: + +```shell +cp -v data/bbgo_test.sqlite3 bbgo_test.sqlite3 +``` + +Configure the environment variables to use SQLite3: + +```shell +DB_DRIVER="sqlite3" +DB_DSN="bbgo_test.sqlite3" +``` From 5f0c45093c0e7d20b7209ccc277a2599a5f26377 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 17:58:32 +0800 Subject: [PATCH 0165/1392] config: add triggerPrice doc to config --- config/grid2-max.yaml | 4 ++++ config/grid2.yaml | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 779ef5d761..58358b6299 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -47,7 +47,11 @@ exchangeStrategies: ## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair. # earnBase: true + ## triggerPrice is used for opening your grid only when the last price touches your pre-set price. + ## this is useful when you don't want to create a grid from a higher price. + ## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000 triggerPrice: 17_000.0 + stopLossPrice: 10_000.0 takeProfitPrice: 20_000.0 diff --git a/config/grid2.yaml b/config/grid2.yaml index 32264b0536..227ec222c0 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -45,6 +45,11 @@ exchangeStrategies: ## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair. # earnBase: true + ## triggerPrice is used for opening your grid only when the last price touches your pre-set price. + ## this is useful when you don't want to create a grid from a higher price. + ## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000 + triggerPrice: 17_000.0 + ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. ## you can set this instead of the default grid profit spread. From 4f3a160bbf8c3305aac11150eeea6cdb3a6e152a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 18:01:58 +0800 Subject: [PATCH 0166/1392] grid2: add stopLossPrice handler --- config/grid2.yaml | 2 +- pkg/strategy/grid2/strategy.go | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/config/grid2.yaml b/config/grid2.yaml index 227ec222c0..20d67e794d 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -49,7 +49,7 @@ exchangeStrategies: ## this is useful when you don't want to create a grid from a higher price. ## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000 triggerPrice: 17_000.0 - + ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. ## you can set this instead of the default grid profit spread. diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 36d1558df5..7769dcc6d0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -64,7 +64,8 @@ type Strategy struct { // BaseInvestment is the total base quantity you want to place as the sell order. BaseInvestment fixedpoint.Value `json:"baseInvestment"` - TriggerPrice fixedpoint.Value `json:"triggerPrice"` + TriggerPrice fixedpoint.Value `json:"triggerPrice"` + StopLossPrice fixedpoint.Value `json:"stopLossPrice"` TakeProfitPrice fixedpoint.Value `json:"takeProfitPrice"` @@ -498,6 +499,25 @@ func (s *Strategy) newTriggerPriceHandler(ctx context.Context, session *bbgo.Exc if err := s.openGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("failed to setup grid orders") + return + } + }) +} + +func (s *Strategy) newStopLossPriceHandler(ctx context.Context, session *bbgo.ExchangeSession) types.KLineCallback { + return types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { + if s.StopLossPrice.Compare(k.Low) < 0 { + return + } + + if err := s.closeGrid(ctx); err != nil { + s.logger.WithError(err).Errorf("can not close grid") + return + } + + if err := s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "grid2:stopLoss"); err != nil { + s.logger.WithError(err).Errorf("can not close position") + return } }) } @@ -772,6 +792,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se session.MarketDataStream.OnKLineClosed(s.newTriggerPriceHandler(ctx, session)) } + if !s.StopLossPrice.IsZero() { + session.MarketDataStream.OnKLineClosed(s.newStopLossPriceHandler(ctx, session)) + } + session.UserDataStream.OnStart(func() { if !s.TriggerPrice.IsZero() { return From e0a69a89c835781cfb928751a9ed721ebedf7958 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 18:02:30 +0800 Subject: [PATCH 0167/1392] config: add stopLossPrice config to the default grid2 config --- config/grid2.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/grid2.yaml b/config/grid2.yaml index 20d67e794d..963d782974 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -50,6 +50,8 @@ exchangeStrategies: ## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000 triggerPrice: 17_000.0 + stopLossPrice: 10_000.0 + ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. ## you can set this instead of the default grid profit spread. From 50f3ca80cfff414f8924df1114e336d2c126aba3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 18:12:01 +0800 Subject: [PATCH 0168/1392] config: add doc for stopLossPrice --- config/grid2.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/grid2.yaml b/config/grid2.yaml index 963d782974..c238a526a8 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -45,11 +45,13 @@ exchangeStrategies: ## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair. # earnBase: true - ## triggerPrice is used for opening your grid only when the last price touches your pre-set price. + ## triggerPrice is used for opening your grid only when the last price touches your trigger price. ## this is useful when you don't want to create a grid from a higher price. ## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000 triggerPrice: 17_000.0 + ## triggerPrice is used for closing your grid only when the last price touches your stop loss price. + ## for example, when the price drops to 17_000.0 then close the grid and sell all base inventory. stopLossPrice: 10_000.0 ## profitSpread is the profit spread of the arbitrage order (sell order) From 9d6272011166aa5ed8ecd61386107a0a1fe7759a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 18:17:05 +0800 Subject: [PATCH 0169/1392] grid2: add log for trigger price --- config/grid2-max.yaml | 5 +++-- pkg/strategy/grid2/strategy.go | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 58358b6299..91fe629354 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -50,9 +50,10 @@ exchangeStrategies: ## triggerPrice is used for opening your grid only when the last price touches your pre-set price. ## this is useful when you don't want to create a grid from a higher price. ## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000 - triggerPrice: 17_000.0 + triggerPrice: 16_900.0 + + stopLossPrice: 16_000.0 - stopLossPrice: 10_000.0 takeProfitPrice: 20_000.0 ## profitSpread is the profit spread of the arbitrage order (sell order) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7769dcc6d0..0898aab49d 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -497,6 +497,7 @@ func (s *Strategy) newTriggerPriceHandler(ctx context.Context, session *bbgo.Exc return } + s.logger.Infof("the last price %f hits triggerPrice %f, opening grid", k.Close.Float64(), s.TriggerPrice.Float64()) if err := s.openGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("failed to setup grid orders") return From bce004106c6db61840966197fa614c74c36e8089 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 18:21:43 +0800 Subject: [PATCH 0170/1392] grid2: check price --- config/grid2-max.yaml | 4 ++-- pkg/strategy/grid2/strategy.go | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 91fe629354..1d36a96dbd 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -35,8 +35,8 @@ exchangeStrategies: grid2: symbol: BTCUSDT upperPrice: 18_000.0 - lowerPrice: 13_000.0 - gridNumber: 100 + lowerPrice: 15_000.0 + gridNumber: 30 ## compound is used for buying more inventory when the profit is made by the filled SELL order. ## when compound is disabled, fixed quantity is used for each grid order. diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 0898aab49d..a7cf1e88bc 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -497,6 +497,10 @@ func (s *Strategy) newTriggerPriceHandler(ctx context.Context, session *bbgo.Exc return } + if s.grid != nil { + return + } + s.logger.Infof("the last price %f hits triggerPrice %f, opening grid", k.Close.Float64(), s.TriggerPrice.Float64()) if err := s.openGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("failed to setup grid orders") From 01b013fcc7839c7a05be5d6f49acf5236ce2f7a3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 18:27:21 +0800 Subject: [PATCH 0171/1392] grid2: fix trigger price check for onStart handler --- pkg/strategy/grid2/grid.go | 2 +- pkg/strategy/grid2/strategy.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index c533e7421b..ccdf34427c 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -188,5 +188,5 @@ func (g *Grid) updatePinsCache() { } func (g *Grid) String() string { - return fmt.Sprintf("grid: priceRange: %f <=> %f size: %f spread: %f", g.LowerPrice.Float64(), g.UpperPrice.Float64(), g.Size.Float64(), g.Spread.Float64()) + return fmt.Sprintf("GRID: priceRange: %f <=> %f size: %f spread: %f", g.LowerPrice.Float64(), g.UpperPrice.Float64(), g.Size.Float64(), g.Spread.Float64()) } diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index a7cf1e88bc..1dc1d98823 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -547,12 +547,14 @@ func (s *Strategy) closeGrid(ctx context.Context) error { // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) error { + // grid object guard if s.grid != nil { return nil } s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) s.grid.CalculateArithmeticPins() + s.logger.Info(s.grid.String()) lastPrice, err := s.getLastTradePrice(ctx, session) @@ -803,12 +805,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se session.UserDataStream.OnStart(func() { if !s.TriggerPrice.IsZero() { + if err := s.openGrid(ctx, session); err != nil { + s.logger.WithError(err).Errorf("failed to setup grid orders") + } return } - - if err := s.openGrid(ctx, session); err != nil { - s.logger.WithError(err).Errorf("failed to setup grid orders") - } }) return nil From ea34b3a9627d179a9161ebb6371c9373a79fa65a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 18:28:34 +0800 Subject: [PATCH 0172/1392] grid2: another fix --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 1dc1d98823..69b506d39b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -804,7 +804,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } session.UserDataStream.OnStart(func() { - if !s.TriggerPrice.IsZero() { + if s.TriggerPrice.IsZero() { if err := s.openGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("failed to setup grid orders") } From 943912f6bfe5ac9f2d1a4717d79487b1fe1a9552 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 18:32:17 +0800 Subject: [PATCH 0173/1392] grid2: add grid order debug logs --- pkg/strategy/grid2/strategy.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 69b506d39b..66c0ea3a25 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -690,8 +690,16 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) if err2 != nil { return err } + + // debug info + s.logger.Infof("GRID ORDERS: [") + for _, order := range submitOrders { + s.logger.Info(" - ", order.String()) + } + s.logger.Infof("] END OF GRID ORDERS") + for _, order := range createdOrders { - s.logger.Infof(order.String()) + s.logger.Info(order.String()) } } From a5e6173038d3eb13b0b231be3bce40847024dbe1 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 18:33:28 +0800 Subject: [PATCH 0174/1392] grid2: fix openGrid method --- pkg/strategy/grid2/strategy.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 66c0ea3a25..e4e9ded841 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -685,22 +685,22 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) quoteQuantity := quantity.Mul(price) usedQuote = usedQuote.Add(quoteQuantity) } + } - createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...) - if err2 != nil { - return err - } + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...) + if err2 != nil { + return err + } - // debug info - s.logger.Infof("GRID ORDERS: [") - for _, order := range submitOrders { - s.logger.Info(" - ", order.String()) - } - s.logger.Infof("] END OF GRID ORDERS") + // debug info + s.logger.Infof("GRID ORDERS: [") + for _, order := range submitOrders { + s.logger.Info(" - ", order.String()) + } + s.logger.Infof("] END OF GRID ORDERS") - for _, order := range createdOrders { - s.logger.Info(order.String()) - } + for _, order := range createdOrders { + s.logger.Info(order.String()) } return nil From efcfcf7c18f4a65a962d44476b74a039ddfdf100 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 18:42:03 +0800 Subject: [PATCH 0175/1392] grid2: add position reset --- pkg/strategy/grid2/strategy.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e4e9ded841..730eccae56 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -776,6 +776,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position = types.NewPositionFromMarket(s.Market) } + // make this an option + // s.Position.Reset() + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindProfitStats(s.ProfitStats) From 8d78399335e37656ea8e6e8853af9a7aee8fe930 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 19:48:12 +0800 Subject: [PATCH 0176/1392] grid2: fix order shifting --- pkg/strategy/grid2/strategy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 730eccae56..ed35125723 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -651,7 +651,8 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) usedBase = usedBase.Add(quantity) } else if i > 0 { // next price - nextPin := pins[i-1] + i-- + nextPin := pins[i] nextPrice := fixedpoint.Value(nextPin) submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, From 3b821c8b5825f507156aae753c545ad705b72986 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 21:06:52 +0800 Subject: [PATCH 0177/1392] grid2: fix order price shifting --- pkg/strategy/grid2/strategy.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index ed35125723..0be9600b9c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -283,7 +283,8 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, requiredBase = requiredBase.Add(quantity) } else if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote - nextLowerPin := pins[i-1] + i-- + nextLowerPin := pins[i] nextLowerPrice := fixedpoint.Value(nextLowerPin) requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) buyPlacedPrice = nextLowerPrice @@ -343,7 +344,8 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am requiredBase = requiredBase.Add(quantity) } else if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote - nextLowerPin := pins[i-1] + i-- + nextLowerPin := pins[i] nextLowerPrice := fixedpoint.Value(nextLowerPin) requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) buyPlacedPrice = nextLowerPrice @@ -400,7 +402,8 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f // quantity := amount.Div(lastPrice) if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote - nextLowerPin := pins[i-1] + i-- + nextLowerPin := pins[i] nextLowerPrice := fixedpoint.Value(nextLowerPin) // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) @@ -467,7 +470,8 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv // quantity := amount.Div(lastPrice) if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote - nextLowerPin := pins[i-1] + i-- + nextLowerPin := pins[i] nextLowerPrice := fixedpoint.Value(nextLowerPin) // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) @@ -520,6 +524,10 @@ func (s *Strategy) newStopLossPriceHandler(ctx context.Context, session *bbgo.Ex return } + if s.Position.GetBase().Sign() < 0 { + return + } + if err := s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "grid2:stopLoss"); err != nil { s.logger.WithError(err).Errorf("can not close position") return From ec6b170f01ff51239ba30a697b9a392a694766bf Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 21:09:39 +0800 Subject: [PATCH 0178/1392] grid2: add more log messages for stop loss --- pkg/strategy/grid2/strategy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 0be9600b9c..6bfdb37e8d 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -519,15 +519,19 @@ func (s *Strategy) newStopLossPriceHandler(ctx context.Context, session *bbgo.Ex return } + s.logger.Infof("last low price %f hits stopLossPrice %f, closing grid", k.Low.Float64(), s.StopLossPrice.Float64()) + if err := s.closeGrid(ctx); err != nil { s.logger.WithError(err).Errorf("can not close grid") return } - if s.Position.GetBase().Sign() < 0 { + base := s.Position.GetBase() + if base.Sign() < 0 { return } + s.logger.Infof("position base %f > 0, closing position...", base.Float64()) if err := s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "grid2:stopLoss"); err != nil { s.logger.WithError(err).Errorf("can not close position") return From 19e0a20c67c0fd013d0eba921236c18b840beee0 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 21:43:40 +0800 Subject: [PATCH 0179/1392] grid2: fill fixedpoint.Zero for stats --- config/grid2-max.yaml | 2 +- pkg/strategy/grid2/profit.go | 12 ++++++++++-- pkg/strategy/grid2/strategy.go | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 1d36a96dbd..0ea137c73d 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -50,7 +50,7 @@ exchangeStrategies: ## triggerPrice is used for opening your grid only when the last price touches your pre-set price. ## this is useful when you don't want to create a grid from a higher price. ## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000 - triggerPrice: 16_900.0 + # triggerPrice: 16_900.0 stopLossPrice: 16_000.0 diff --git a/pkg/strategy/grid2/profit.go b/pkg/strategy/grid2/profit.go index 941bee39f4..e9cc5e18cb 100644 --- a/pkg/strategy/grid2/profit.go +++ b/pkg/strategy/grid2/profit.go @@ -29,8 +29,16 @@ type GridProfitStats struct { func newGridProfitStats(market types.Market) *GridProfitStats { return &GridProfitStats{ - Symbol: market.Symbol, - Market: market, + Symbol: market.Symbol, + TotalBaseProfit: fixedpoint.Zero, + TotalQuoteProfit: fixedpoint.Zero, + FloatProfit: fixedpoint.Zero, + GridProfit: fixedpoint.Zero, + ArbitrageCount: 0, + TotalFee: fixedpoint.Zero, + Volume: fixedpoint.Zero, + Market: market, + ProfitEntries: nil, } } diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6bfdb37e8d..63fbf03b17 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -658,7 +658,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) Quantity: quantity, Market: s.Market, TimeInForce: types.TimeInForceGTC, - Tag: "grid", + Tag: "grid2", }) usedBase = usedBase.Add(quantity) } else if i > 0 { @@ -674,7 +674,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) Quantity: quantity, Market: s.Market, TimeInForce: types.TimeInForceGTC, - Tag: "grid", + Tag: "grid2", }) quoteQuantity := quantity.Mul(price) usedQuote = usedQuote.Add(quoteQuantity) From 002ce1958e63a93b9398728a03ce68819e519622 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Dec 2022 21:44:03 +0800 Subject: [PATCH 0180/1392] grid2: add omitempty to struct tag --- pkg/strategy/grid2/profit.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/grid2/profit.go b/pkg/strategy/grid2/profit.go index e9cc5e18cb..1d7ab1f793 100644 --- a/pkg/strategy/grid2/profit.go +++ b/pkg/strategy/grid2/profit.go @@ -16,15 +16,15 @@ type GridProfit struct { type GridProfitStats struct { Symbol string `json:"symbol"` - TotalBaseProfit fixedpoint.Value `json:"totalBaseProfit"` - TotalQuoteProfit fixedpoint.Value `json:"totalQuoteProfit"` - FloatProfit fixedpoint.Value `json:"floatProfit"` - GridProfit fixedpoint.Value `json:"gridProfit"` - ArbitrageCount int `json:"arbitrageCount"` - TotalFee fixedpoint.Value `json:"totalFee"` - Volume fixedpoint.Value `json:"volume"` - Market types.Market `json:"market"` - ProfitEntries []*GridProfit `json:"profitEntries"` + TotalBaseProfit fixedpoint.Value `json:"totalBaseProfit,omitempty"` + TotalQuoteProfit fixedpoint.Value `json:"totalQuoteProfit,omitempty"` + FloatProfit fixedpoint.Value `json:"floatProfit,omitempty"` + GridProfit fixedpoint.Value `json:"gridProfit,omitempty"` + ArbitrageCount int `json:"arbitrageCount,omitempty"` + TotalFee fixedpoint.Value `json:"totalFee,omitempty"` + Volume fixedpoint.Value `json:"volume,omitempty"` + Market types.Market `json:"market,omitempty"` + ProfitEntries []*GridProfit `json:"profitEntries,omitempty"` } func newGridProfitStats(market types.Market) *GridProfitStats { From 076ec3b3c2166b12ef7cf0385b706ee2f5b53db1 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 00:20:18 +0800 Subject: [PATCH 0181/1392] grid2: pull out grid order generation --- pkg/strategy/grid2/strategy.go | 53 +++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 63fbf03b17..3d74846378 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -634,11 +634,39 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) } } + submitOrders, err := s.generateGridOrders(totalQuote, totalBase, lastPrice) + if err != nil { + return err + } + + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...) + if err2 != nil { + return err + } + + // debug info + s.logger.Infof("GRID ORDERS: [") + for _, order := range submitOrders { + s.logger.Info(" - ", order.String()) + } + s.logger.Infof("] END OF GRID ORDERS") + + for _, order := range createdOrders { + s.logger.Info(order.String()) + } + + return nil +} + +func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoint.Value) ([]types.SubmitOrder, error) { var buyPlacedPrice = fixedpoint.Zero var pins = s.grid.Pins var usedBase = fixedpoint.Zero var usedQuote = fixedpoint.Zero var submitOrders []types.SubmitOrder + + // si is for sell order price index + var si = len(pins) - 1 for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) @@ -649,6 +677,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... if price.Compare(lastPrice) >= 0 { + si = i if usedBase.Add(quantity).Compare(totalBase) < 0 { submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, @@ -679,8 +708,14 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) quoteQuantity := quantity.Mul(price) usedQuote = usedQuote.Add(quoteQuantity) buyPlacedPrice = nextPrice + } else if i == 0 { + // skip i == 0 } } else { + if i+1 == si { + continue + } + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) >= 0 { continue } @@ -700,23 +735,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) } } - createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...) - if err2 != nil { - return err - } - - // debug info - s.logger.Infof("GRID ORDERS: [") - for _, order := range submitOrders { - s.logger.Info(" - ", order.String()) - } - s.logger.Infof("] END OF GRID ORDERS") - - for _, order := range createdOrders { - s.logger.Info(order.String()) - } - - return nil + return submitOrders, nil } func (s *Strategy) clearOpenOrders(ctx context.Context, session *bbgo.ExchangeSession) error { From 0b824a09fcba59b222693249fb5ae71fc6b92495 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 00:47:08 +0800 Subject: [PATCH 0182/1392] grid2: fix tests --- pkg/strategy/grid2/strategy_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 1c1b68c24b..7057a47e5d 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -70,8 +70,8 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { Pin(number(14_000.0)), Pin(number(15_000.0)), }) - assert.EqualError(t, err, "quote balance (3000.000000 USDT) is not enough, required = quote 4999.999890") - assert.InDelta(t, 4999.99989, requiredQuote.Float64(), number(0.001).Float64()) + assert.EqualError(t, err, "quote balance (3000.000000 USDT) is not enough, required = quote 5037.036980") + assert.InDelta(t, 5037.036980, requiredQuote.Float64(), number(0.001).Float64()) }) } From 6df4a3c31941aa8955c9603d5750921b2c448ea1 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 11:21:07 +0800 Subject: [PATCH 0183/1392] grid2: add TestStrategy_generateGridOrders --- pkg/strategy/grid2/strategy.go | 21 ++------ pkg/strategy/grid2/strategy_test.go | 79 +++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3d74846378..1630bf25a1 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -283,8 +283,7 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, requiredBase = requiredBase.Add(quantity) } else if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote - i-- - nextLowerPin := pins[i] + nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) buyPlacedPrice = nextLowerPrice @@ -344,8 +343,7 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am requiredBase = requiredBase.Add(quantity) } else if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote - i-- - nextLowerPin := pins[i] + nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) buyPlacedPrice = nextLowerPrice @@ -402,8 +400,7 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f // quantity := amount.Div(lastPrice) if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote - i-- - nextLowerPin := pins[i] + nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) @@ -470,8 +467,7 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv // quantity := amount.Div(lastPrice) if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote - i-- - nextLowerPin := pins[i] + nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) @@ -659,7 +655,6 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) } func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoint.Value) ([]types.SubmitOrder, error) { - var buyPlacedPrice = fixedpoint.Zero var pins = s.grid.Pins var usedBase = fixedpoint.Zero var usedQuote = fixedpoint.Zero @@ -692,8 +687,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin usedBase = usedBase.Add(quantity) } else if i > 0 { // next price - i-- - nextPin := pins[i] + nextPin := pins[i-1] nextPrice := fixedpoint.Value(nextPin) submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, @@ -707,7 +701,6 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin }) quoteQuantity := quantity.Mul(price) usedQuote = usedQuote.Add(quoteQuantity) - buyPlacedPrice = nextPrice } else if i == 0 { // skip i == 0 } @@ -716,10 +709,6 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin continue } - if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) >= 0 { - continue - } - submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, Type: types.OrderTypeLimit, diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 7057a47e5d..4ecde64008 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -8,6 +8,7 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -48,9 +49,80 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { }) } +type PriceSideAssert struct { + Price fixedpoint.Value + Side types.SideType +} + +func assertPriceSide(t *testing.T, priceSideAsserts []PriceSideAssert, orders []types.SubmitOrder) { + for i, a := range priceSideAsserts { + assert.Equal(t, a.Side, orders[i].Side) + assert.Equal(t, a.Price, orders[i].Price) + } +} + +func TestStrategy_generateGridOrders(t *testing.T) { + t.Run("quote only", func(t *testing.T) { + s := newTestStrategy() + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + s.QuantityOrAmount.Quantity = number(0.01) + + lastPrice := number(15300) + orders, err := s.generateGridOrders(number(10000.0), number(0), lastPrice) + assert.NoError(t, err) + if !assert.Equal(t, 10, len(orders)) { + for _, o := range orders { + t.Logf("- %s %s", o.Price.String(), o.Side) + } + } + + assertPriceSide(t, []PriceSideAssert{ + {number(19000.0), types.SideTypeBuy}, + {number(18000.0), types.SideTypeBuy}, + {number(17000.0), types.SideTypeBuy}, + {number(16000.0), types.SideTypeBuy}, + {number(15000.0), types.SideTypeBuy}, + {number(14000.0), types.SideTypeBuy}, + {number(13000.0), types.SideTypeBuy}, + {number(12000.0), types.SideTypeBuy}, + {number(11000.0), types.SideTypeBuy}, + {number(10000.0), types.SideTypeBuy}, + }, orders) + }) + + t.Run("base + quote", func(t *testing.T) { + s := newTestStrategy() + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + s.QuantityOrAmount.Quantity = number(0.01) + + lastPrice := number(15300) + orders, err := s.generateGridOrders(number(10000.0), number(0.021), lastPrice) + assert.NoError(t, err) + if !assert.Equal(t, 10, len(orders)) { + for _, o := range orders { + t.Logf("- %s %s", o.Price.String(), o.Side) + } + } + + assertPriceSide(t, []PriceSideAssert{ + {number(20000.0), types.SideTypeSell}, + {number(19000.0), types.SideTypeSell}, + {number(17000.0), types.SideTypeBuy}, + {number(16000.0), types.SideTypeBuy}, + {number(15000.0), types.SideTypeBuy}, + {number(14000.0), types.SideTypeBuy}, + {number(13000.0), types.SideTypeBuy}, + {number(12000.0), types.SideTypeBuy}, + {number(11000.0), types.SideTypeBuy}, + {number(10000.0), types.SideTypeBuy}, + }, orders) + }) +} + func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { s := &Strategy{ - logger: logrus.NewEntry(logrus.New()), Market: types.Market{ BaseCurrency: "BTC", @@ -70,8 +142,8 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { Pin(number(14_000.0)), Pin(number(15_000.0)), }) - assert.EqualError(t, err, "quote balance (3000.000000 USDT) is not enough, required = quote 5037.036980") - assert.InDelta(t, 5037.036980, requiredQuote.Float64(), number(0.001).Float64()) + assert.EqualError(t, err, "quote balance (3000.000000 USDT) is not enough, required = quote 4999.999890") + assert.InDelta(t, 4999.999890, requiredQuote.Float64(), number(0.001).Float64()) }) } @@ -106,6 +178,7 @@ func newTestStrategy() *Strategy { market := types.Market{ BaseCurrency: "BTC", QuoteCurrency: "USDT", + TickSize: number(0.01), } s := &Strategy{ From 27b42db3d78f2c0dd8834f88c95d39b4ecf39ce6 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 11:23:21 +0800 Subject: [PATCH 0184/1392] grid2: add test case for enough base investment --- pkg/strategy/grid2/strategy_test.go | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 4ecde64008..427c24c419 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -119,6 +119,36 @@ func TestStrategy_generateGridOrders(t *testing.T) { {number(10000.0), types.SideTypeBuy}, }, orders) }) + + t.Run("enough base + quote", func(t *testing.T) { + s := newTestStrategy() + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + s.QuantityOrAmount.Quantity = number(0.01) + + lastPrice := number(15300) + orders, err := s.generateGridOrders(number(10000.0), number(1.0), lastPrice) + assert.NoError(t, err) + if !assert.Equal(t, 10, len(orders)) { + for _, o := range orders { + t.Logf("- %s %s", o.Price.String(), o.Side) + } + } + + assertPriceSide(t, []PriceSideAssert{ + {number(20000.0), types.SideTypeSell}, + {number(19000.0), types.SideTypeSell}, + {number(18000.0), types.SideTypeSell}, + {number(17000.0), types.SideTypeSell}, + {number(16000.0), types.SideTypeSell}, + {number(14000.0), types.SideTypeBuy}, + {number(13000.0), types.SideTypeBuy}, + {number(12000.0), types.SideTypeBuy}, + {number(11000.0), types.SideTypeBuy}, + {number(10000.0), types.SideTypeBuy}, + }, orders) + }) + } func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { From 5be140de0e09a35d7ea2c47fa87b5d7e740d2f85 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 15:19:24 +0800 Subject: [PATCH 0185/1392] grid2: improve sell,buy price calculation --- pkg/strategy/grid2/strategy.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 1630bf25a1..d49a69cf1b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -270,13 +270,14 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, requiredQuote = fixedpoint.Zero // when we need to place a buy-to-sell conversion order, we need to mark the price - buyPlacedPrice := fixedpoint.Zero + si := len(pins) - 1 for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... if price.Compare(lastPrice) >= 0 { + si = i // for orders that sell // if we still have the base balance if requiredBase.Add(quantity).Compare(baseBalance) <= 0 { @@ -286,11 +287,10 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) - buyPlacedPrice = nextLowerPrice } } else { // for orders that buy - if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { + if i+1 == si { continue } requiredQuote = requiredQuote.Add(quantity.Mul(price)) @@ -329,13 +329,14 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am requiredQuote = fixedpoint.Zero // when we need to place a buy-to-sell conversion order, we need to mark the price - buyPlacedPrice := fixedpoint.Zero + si := len(pins) - 1 for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... if price.Compare(lastPrice) >= 0 { + si = i // for orders that sell // if we still have the base balance quantity := amount.Div(lastPrice) @@ -346,11 +347,10 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) - buyPlacedPrice = nextLowerPrice } } else { // for orders that buy - if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { + if i+1 == si { continue } requiredQuote = requiredQuote.Add(amount) @@ -383,18 +383,19 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am } func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { - buyPlacedPrice := fixedpoint.Zero // quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + .... // => // quoteInvestment = (p1 + p2 + p3) * q // q = quoteInvestment / (p1 + p2 + p3) totalQuotePrice := fixedpoint.Zero + si := len(pins) - 1 for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) if price.Compare(lastPrice) >= 0 { + si = i // for orders that sell // if we still have the base balance // quantity := amount.Div(lastPrice) @@ -404,11 +405,10 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f nextLowerPrice := fixedpoint.Value(nextLowerPin) // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) - buyPlacedPrice = nextLowerPrice } } else { // for orders that buy - if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { + if i+1 == si { continue } From 4bba5510dd18096c793f7f94286ab995c909e181 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 18:11:44 +0800 Subject: [PATCH 0186/1392] grid2: position reset should reset the total fee --- pkg/strategy/grid2/strategy.go | 2 ++ pkg/types/position.go | 1 + 2 files changed, 3 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index d49a69cf1b..57f6a5ff72 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -809,6 +809,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) + // TODO: detect if there are previous grid orders on the order book if s.ClearOpenOrdersWhenStart { if err := s.clearOpenOrders(ctx, session); err != nil { return err @@ -819,6 +820,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se defer wg.Done() if s.KeepOrdersWhenShutdown { + s.logger.Infof("KeepOrdersWhenShutdown is set, will keep the orders on the exchange") return } diff --git a/pkg/types/position.go b/pkg/types/position.go index 3fd5c85a14..bd428fe438 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -296,6 +296,7 @@ func (p *Position) Reset() { p.Base = fixedpoint.Zero p.Quote = fixedpoint.Zero p.AverageCost = fixedpoint.Zero + p.TotalFee = make(map[string]fixedpoint.Value) } func (p *Position) SetFeeRate(exchangeFee ExchangeFee) { From c5fb15dc225d7400ffe43a39e964a600ef5ad046 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 18:12:38 +0800 Subject: [PATCH 0187/1392] grid2: update test grid configuration --- config/grid2-max.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 0ea137c73d..e159bd5412 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -35,8 +35,8 @@ exchangeStrategies: grid2: symbol: BTCUSDT upperPrice: 18_000.0 - lowerPrice: 15_000.0 - gridNumber: 30 + lowerPrice: 16_000.0 + gridNumber: 200 ## compound is used for buying more inventory when the profit is made by the filled SELL order. ## when compound is disabled, fixed quantity is used for each grid order. From f727f314e6e00df8577c26974b73420a3a70e7c5 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 18:15:30 +0800 Subject: [PATCH 0188/1392] grid2: add FeeRate configuration for checking profit spread --- pkg/strategy/grid2/strategy.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 57f6a5ff72..c9db9ab09d 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -80,6 +80,10 @@ type Strategy struct { // If this is set, when bbgo started, it will clear the open orders in the same market (by symbol) ClearOpenOrdersWhenStart bool `json:"clearOpenOrdersWhenStart"` + // FeeRate is used for calculating the minimal profit spread. + // it makes sure that your grid configuration is profitable. + FeeRate fixedpoint.Value `json:"feeRate"` + GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"` ProfitStats *types.ProfitStats `persistence:"profit_stats"` Position *types.Position `persistence:"position"` @@ -113,10 +117,16 @@ func (s *Strategy) Validate() error { return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) } + if s.FeeRate.IsZero() { + s.FeeRate = fixedpoint.NewFromFloat(0.1 * 0.01) // 0.1%, 0.075% with BNB + } + if !s.ProfitSpread.IsZero() { percent := s.ProfitSpread.Div(s.LowerPrice) - feeRate := fixedpoint.NewFromFloat(0.1 * 0.01) // 0.1%, 0.075% with BNB - if percent.Compare(feeRate) < 0 { + + // the min fee rate from 2 maker/taker orders + minProfitSpread := s.FeeRate.Mul(fixedpoint.NewFromInt(2)) + if percent.Compare(minProfitSpread) < 0 { return fmt.Errorf("profitSpread %f %s is too small, less than the fee rate: %s", s.ProfitSpread.Float64(), percent.Percentage(), feeRate.Percentage()) } } From fcf86133193571ff6da5028424076f87752d9b37 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 18:15:54 +0800 Subject: [PATCH 0189/1392] grid2: fix feeRate var --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c9db9ab09d..3cafaeb1b0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -127,7 +127,7 @@ func (s *Strategy) Validate() error { // the min fee rate from 2 maker/taker orders minProfitSpread := s.FeeRate.Mul(fixedpoint.NewFromInt(2)) if percent.Compare(minProfitSpread) < 0 { - return fmt.Errorf("profitSpread %f %s is too small, less than the fee rate: %s", s.ProfitSpread.Float64(), percent.Percentage(), feeRate.Percentage()) + return fmt.Errorf("profitSpread %f %s is too small, less than the fee rate: %s", s.ProfitSpread.Float64(), percent.Percentage(), s.FeeRate.Percentage()) } } From 537e9e14ecaec56d3b808f51e019e6c04ff4ca0f Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 19:00:39 +0800 Subject: [PATCH 0190/1392] add GetOrderTrades method to TradeStore Signed-off-by: c9s --- pkg/bbgo/trade_store.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/trade_store.go b/pkg/bbgo/trade_store.go index a5ea2db380..ebf74c4168 100644 --- a/pkg/bbgo/trade_store.go +++ b/pkg/bbgo/trade_store.go @@ -69,10 +69,21 @@ func (s *TradeStore) Filter(filter TradeFilter) { s.Unlock() } +func (s *TradeStore) GetOrderTrades(o types.Order) (trades []types.Trade) { + s.Lock() + for _, t := range s.trades { + if t.OrderID == o.OrderID { + trades = append(trades, t) + } + } + s.Unlock() + return trades +} + func (s *TradeStore) GetAndClear() (trades []types.Trade) { s.Lock() - for _, o := range s.trades { - trades = append(trades, o) + for _, t := range s.trades { + trades = append(trades, t) } s.trades = make(map[uint64]types.Trade) s.Unlock() From 5c8304429728c2ef8180c0fea9251583903e33d8 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 19:23:12 +0800 Subject: [PATCH 0191/1392] bbgo: let tradeStore be able to collect trades from stream --- pkg/bbgo/trade_store.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/trade_store.go b/pkg/bbgo/trade_store.go index ebf74c4168..e16ab1c3c6 100644 --- a/pkg/bbgo/trade_store.go +++ b/pkg/bbgo/trade_store.go @@ -11,10 +11,6 @@ type TradeStore struct { sync.Mutex trades map[uint64]types.Trade - - RemoveCancelled bool - RemoveFilled bool - AddOrderUpdate bool } func NewTradeStore() *TradeStore { @@ -99,3 +95,9 @@ func (s *TradeStore) Add(trades ...types.Trade) { s.trades[trade.ID] = trade } } + +func (s *TradeStore) BindStream(stream types.Stream) { + stream.OnTradeUpdate(func(trade types.Trade) { + s.Add(trade) + }) +} From 16224583ff3d811cdfa88e092f842d529f562123 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 19:23:39 +0800 Subject: [PATCH 0192/1392] grid2: add historicalTrades store --- pkg/strategy/grid2/strategy.go | 54 ++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3cafaeb1b0..3a23d8149a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -3,6 +3,7 @@ package grid2 import ( "context" "fmt" + "strconv" "sync" "github.com/pkg/errors" @@ -92,7 +93,8 @@ type Strategy struct { session *bbgo.ExchangeSession orderQueryService types.ExchangeOrderQueryService - orderExecutor *bbgo.GeneralOrderExecutor + orderExecutor *bbgo.GeneralOrderExecutor + historicalTrades *bbgo.TradeStore // groupID is the group ID used for the strategy instance for canceling orders groupID uint32 @@ -195,6 +197,28 @@ func (s *Strategy) calculateProfit(o types.Order, buyPrice, buyQuantity fixedpoi return profit } +func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { + tq := fixedpoint.Zero + for _, t := range trades { + if t.Fee.IsZero() && t.FeeCurrency == "" { + s.logger.Warnf("trade fee and feeCurrency is zero: %+v", t) + return false + } + + tq = tq.Add(t.Quantity) + } + + if tq.Compare(o.Quantity) != 0 { + s.logger.Warnf("order trades missing. expected: %f actual: %f", + o.Quantity.Float64(), + tq.Float64()) + return false + } + + return true +} + +// handleOrderFilled is called when a order status is FILLED func (s *Strategy) handleOrderFilled(o types.Order) { if s.grid == nil { return @@ -202,7 +226,29 @@ func (s *Strategy) handleOrderFilled(o types.Order) { s.logger.Infof("GRID ORDER FILLED: %s", o.String()) - // var profit *GridProfit = nil + // collect trades + if s.orderQueryService != nil { + tradeCollector := s.orderExecutor.TradeCollector() + // tradeCollector.Process() + + orderTrades := tradeCollector.TradeStore().GetOrderTrades(o) + s.logger.Infof("FILLED ORDER TRADES: %+v", orderTrades) + + // TODO: check if there is no missing trades + if !s.verifyOrderTrades(o, orderTrades) { + s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") + + apiOrderTrades, err := s.orderQueryService.QueryOrderTrades(context.Background(), types.OrderQuery{ + Symbol: o.Symbol, + OrderID: strconv.FormatUint(o.OrderID, 10), + }) + if err != nil { + s.logger.WithError(err).Errorf("query order trades error") + } else { + orderTrades = apiOrderTrades + } + } + } // check order fee newSide := types.SideTypeSell @@ -228,6 +274,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } // calculate profit + // TODO: send profit notification profit := s.calculateProfit(o, newPrice, newQuantity) s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) s.GridProfitStats.AddProfit(profit) @@ -564,6 +611,7 @@ func (s *Strategy) closeGrid(ctx context.Context) error { // openGrid // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. +// TODO: fix sell order placement for profitSpread func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) error { // grid object guard if s.grid != nil { @@ -809,6 +857,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // make this an option // s.Position.Reset() + s.historicalTrades = bbgo.NewTradeStore() + s.historicalTrades.BindStream(session.UserDataStream) s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) s.orderExecutor.BindEnvironment(s.Environment) From 5d441e3efedf1ac9e2ebc96518e901fa1b90b7f7 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 19:30:06 +0800 Subject: [PATCH 0193/1392] grid2: collect fees and check if we need to reduce the quantity for sell --- pkg/strategy/grid2/strategy.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3a23d8149a..f2c8360509 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -197,6 +197,19 @@ func (s *Strategy) calculateProfit(o types.Order, buyPrice, buyQuantity fixedpoi return profit } +// collectTradeFee collects the fee from the given trade slice +func collectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { + fees := make(map[string]fixedpoint.Value) + for _, t := range trades { + if fee, ok := fees[t.FeeCurrency]; ok { + fees[t.FeeCurrency] = fee.Add(t.Fee) + } else { + fees[t.FeeCurrency] = t.Fee + } + } + return fees +} + func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { tq := fixedpoint.Zero for _, t := range trades { @@ -227,11 +240,9 @@ func (s *Strategy) handleOrderFilled(o types.Order) { s.logger.Infof("GRID ORDER FILLED: %s", o.String()) // collect trades + baseSellQuantityReduction := fixedpoint.Zero if s.orderQueryService != nil { - tradeCollector := s.orderExecutor.TradeCollector() - // tradeCollector.Process() - - orderTrades := tradeCollector.TradeStore().GetOrderTrades(o) + orderTrades := s.historicalTrades.GetOrderTrades(o) s.logger.Infof("FILLED ORDER TRADES: %+v", orderTrades) // TODO: check if there is no missing trades @@ -248,6 +259,15 @@ func (s *Strategy) handleOrderFilled(o types.Order) { orderTrades = apiOrderTrades } } + + if s.verifyOrderTrades(o, orderTrades) { + // check if there is a BaseCurrency fee collected + fees := collectTradeFee(orderTrades) + if fee, ok := fees[s.Market.BaseCurrency]; ok { + baseSellQuantityReduction = fee + s.logger.Infof("baseSellQuantityReduction: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) + } + } } // check order fee From fae61bd91f03271b078e72f9a6c11ff01c6b22a7 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 19:31:44 +0800 Subject: [PATCH 0194/1392] grid2: narrow down orderQueryService support checking --- pkg/strategy/grid2/strategy.go | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f2c8360509..df9e26a110 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -241,14 +241,17 @@ func (s *Strategy) handleOrderFilled(o types.Order) { // collect trades baseSellQuantityReduction := fixedpoint.Zero - if s.orderQueryService != nil { - orderTrades := s.historicalTrades.GetOrderTrades(o) - s.logger.Infof("FILLED ORDER TRADES: %+v", orderTrades) + orderTrades := s.historicalTrades.GetOrderTrades(o) + if len(orderTrades) > 0 { + s.logger.Infof("FOUND FILLED ORDER TRADES: %+v", orderTrades) + } - // TODO: check if there is no missing trades - if !s.verifyOrderTrades(o, orderTrades) { - s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") + // TODO: should be only for BUY order + if !s.verifyOrderTrades(o, orderTrades) { + s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") + // if orderQueryService is supported, use it to query the trades of the filled order + if s.orderQueryService != nil { apiOrderTrades, err := s.orderQueryService.QueryOrderTrades(context.Background(), types.OrderQuery{ Symbol: o.Symbol, OrderID: strconv.FormatUint(o.OrderID, 10), @@ -259,14 +262,14 @@ func (s *Strategy) handleOrderFilled(o types.Order) { orderTrades = apiOrderTrades } } + } - if s.verifyOrderTrades(o, orderTrades) { - // check if there is a BaseCurrency fee collected - fees := collectTradeFee(orderTrades) - if fee, ok := fees[s.Market.BaseCurrency]; ok { - baseSellQuantityReduction = fee - s.logger.Infof("baseSellQuantityReduction: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) - } + if s.verifyOrderTrades(o, orderTrades) { + // check if there is a BaseCurrency fee collected + fees := collectTradeFee(orderTrades) + if fee, ok := fees[s.Market.BaseCurrency]; ok { + baseSellQuantityReduction = fee + s.logger.Infof("baseSellQuantityReduction: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) } } From 8e3bfe8499e1b147dc6ff7a868984bbd6c09ce21 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 19:37:42 +0800 Subject: [PATCH 0195/1392] grid2: consider base sell quantity reduction --- config/grid2-max.yaml | 6 +++ pkg/strategy/grid2/strategy.go | 77 +++++++++++++++++++--------------- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index e159bd5412..fc22b3d357 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -77,3 +77,9 @@ exchangeStrategies: ## if you have existing BTC position and want to reuse it you can set the baseInvestment. quoteInvestment: 10_000 # baseInvestment: 1.0 + + feeRate: 0.075% + closeWhenCancelOrder: false + resetPositionWhenStart: true + clearOpenOrdersWhenStart: false + keepOrdersWhenShutdown: false diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index df9e26a110..cabf74036a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -81,6 +81,8 @@ type Strategy struct { // If this is set, when bbgo started, it will clear the open orders in the same market (by symbol) ClearOpenOrdersWhenStart bool `json:"clearOpenOrdersWhenStart"` + ResetPositionWhenStart bool `json:"resetPositionWhenStart"` + // FeeRate is used for calculating the minimal profit spread. // it makes sure that your grid configuration is profitable. FeeRate fixedpoint.Value `json:"feeRate"` @@ -239,46 +241,53 @@ func (s *Strategy) handleOrderFilled(o types.Order) { s.logger.Infof("GRID ORDER FILLED: %s", o.String()) + // check order fee + newSide := types.SideTypeSell + newPrice := o.Price + newQuantity := o.Quantity + orderQuoteQuantity := o.Quantity.Mul(o.Price) + // collect trades baseSellQuantityReduction := fixedpoint.Zero - orderTrades := s.historicalTrades.GetOrderTrades(o) - if len(orderTrades) > 0 { - s.logger.Infof("FOUND FILLED ORDER TRADES: %+v", orderTrades) - } - // TODO: should be only for BUY order - if !s.verifyOrderTrades(o, orderTrades) { - s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") + // baseSellQuantityReduction calculation should be only for BUY order + // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC + // if we don't reduce the sell quantity, than we might fail to place the sell order + if o.Side == types.SideTypeBuy { + orderTrades := s.historicalTrades.GetOrderTrades(o) + if len(orderTrades) > 0 { + s.logger.Infof("FOUND FILLED ORDER TRADES: %+v", orderTrades) + } + + if !s.verifyOrderTrades(o, orderTrades) { + s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") - // if orderQueryService is supported, use it to query the trades of the filled order - if s.orderQueryService != nil { - apiOrderTrades, err := s.orderQueryService.QueryOrderTrades(context.Background(), types.OrderQuery{ - Symbol: o.Symbol, - OrderID: strconv.FormatUint(o.OrderID, 10), - }) - if err != nil { - s.logger.WithError(err).Errorf("query order trades error") - } else { - orderTrades = apiOrderTrades + // if orderQueryService is supported, use it to query the trades of the filled order + if s.orderQueryService != nil { + apiOrderTrades, err := s.orderQueryService.QueryOrderTrades(context.Background(), types.OrderQuery{ + Symbol: o.Symbol, + OrderID: strconv.FormatUint(o.OrderID, 10), + }) + if err != nil { + s.logger.WithError(err).Errorf("query order trades error") + } else { + orderTrades = apiOrderTrades + } } } - } - if s.verifyOrderTrades(o, orderTrades) { - // check if there is a BaseCurrency fee collected - fees := collectTradeFee(orderTrades) - if fee, ok := fees[s.Market.BaseCurrency]; ok { - baseSellQuantityReduction = fee - s.logger.Infof("baseSellQuantityReduction: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) + if s.verifyOrderTrades(o, orderTrades) { + // check if there is a BaseCurrency fee collected + fees := collectTradeFee(orderTrades) + if fee, ok := fees[s.Market.BaseCurrency]; ok { + baseSellQuantityReduction = fee + s.logger.Infof("baseSellQuantityReduction: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) + + newQuantity = newQuantity.Sub(baseSellQuantityReduction) + } } } - // check order fee - newSide := types.SideTypeSell - newPrice := o.Price - newQuantity := o.Quantity - orderQuoteQuantity := o.Quantity.Mul(o.Price) - switch o.Side { case types.SideTypeSell: newSide = types.SideTypeBuy @@ -313,7 +322,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } if s.EarnBase { - newQuantity = orderQuoteQuantity.Div(newPrice) + newQuantity = orderQuoteQuantity.Div(newPrice).Sub(baseSellQuantityReduction) } } @@ -878,8 +887,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position = types.NewPositionFromMarket(s.Market) } - // make this an option - // s.Position.Reset() + if s.ResetPositionWhenStart { + s.Position.Reset() + } + s.historicalTrades = bbgo.NewTradeStore() s.historicalTrades.BindStream(session.UserDataStream) From c4544cf8b24e17601dd0edc7cda28accf74c8aeb Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 19:42:36 +0800 Subject: [PATCH 0196/1392] grid2: improve debug logging --- config/grid2-max.yaml | 2 +- pkg/strategy/grid2/strategy.go | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index fc22b3d357..f9c751cb42 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -79,7 +79,7 @@ exchangeStrategies: # baseInvestment: 1.0 feeRate: 0.075% - closeWhenCancelOrder: false + closeWhenCancelOrder: true resetPositionWhenStart: true clearOpenOrdersWhenStart: false keepOrdersWhenShutdown: false diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index cabf74036a..22c20604a1 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -653,7 +653,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) s.grid.CalculateArithmeticPins() - s.logger.Info(s.grid.String()) + s.logger.Info("OPENING GRID: ", s.grid.String()) lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { @@ -732,7 +732,12 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) // debug info s.logger.Infof("GRID ORDERS: [") - for _, order := range submitOrders { + for i, order := range submitOrders { + + if i > 0 && lastPrice.Compare(order.Price) >= 0 && lastPrice.Compare(submitOrders[i-1].Price) <= 0 { + s.logger.Info(" - LAST PRICE: %f", lastPrice.Float64()) + } + s.logger.Info(" - ", order.String()) } s.logger.Infof("] END OF GRID ORDERS") From a67d01e821dfb7cfd44ada037c3245a11d67c7cc Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 19:43:58 +0800 Subject: [PATCH 0197/1392] grid2: fix log format --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 22c20604a1..eb87140f7f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -735,7 +735,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) for i, order := range submitOrders { if i > 0 && lastPrice.Compare(order.Price) >= 0 && lastPrice.Compare(submitOrders[i-1].Price) <= 0 { - s.logger.Info(" - LAST PRICE: %f", lastPrice.Float64()) + s.logger.Infof(" - LAST PRICE: %f", lastPrice.Float64()) } s.logger.Info(" - ", order.String()) From 9be3c79f8a8b0a5952c3097ba852583d62453aae Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 19:46:08 +0800 Subject: [PATCH 0198/1392] grid2: handle take profit --- pkg/strategy/grid2/strategy.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index eb87140f7f..dbfa9217b6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -624,6 +624,32 @@ func (s *Strategy) newStopLossPriceHandler(ctx context.Context, session *bbgo.Ex }) } +func (s *Strategy) newTakeProfitHandler(ctx context.Context, session *bbgo.ExchangeSession) types.KLineCallback { + return types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { + if s.TakeProfitPrice.Compare(k.High) < 0 { + return + } + + s.logger.Infof("last high price %f hits takeProfitPrice %f, closing grid", k.High.Float64(), s.TakeProfitPrice.Float64()) + + if err := s.closeGrid(ctx); err != nil { + s.logger.WithError(err).Errorf("can not close grid") + return + } + + base := s.Position.GetBase() + if base.Sign() < 0 { + return + } + + s.logger.Infof("position base %f > 0, closing position...", base.Float64()) + if err := s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "grid2:takeProfit"); err != nil { + s.logger.WithError(err).Errorf("can not close position") + return + } + }) +} + // closeGrid closes the grid orders func (s *Strategy) closeGrid(ctx context.Context) error { bbgo.Sync(ctx, s) @@ -936,6 +962,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se session.MarketDataStream.OnKLineClosed(s.newStopLossPriceHandler(ctx, session)) } + if !s.TakeProfitPrice.IsZero() { + session.MarketDataStream.OnKLineClosed(s.newTakeProfitHandler(ctx, session)) + } + session.UserDataStream.OnStart(func() { if s.TriggerPrice.IsZero() { if err := s.openGrid(ctx, session); err != nil { From fda7abc1f26549aba724b40e7ae60864c5b4ebf3 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 19:46:48 +0800 Subject: [PATCH 0199/1392] config: update grid2 config sample --- config/grid2.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/config/grid2.yaml b/config/grid2.yaml index c238a526a8..242e2c914b 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -74,4 +74,11 @@ exchangeStrategies: ## quoteInvestment is required, and baseInvestment is optional (could be zero) ## if you have existing BTC position and want to reuse it you can set the baseInvestment. quoteInvestment: 10_000 - baseInvestment: 1.0 + + ## baseInvestment is optional + baseInvestment: 0.0 + + closeWhenCancelOrder: true + resetPositionWhenStart: false + clearOpenOrdersWhenStart: false + keepOrdersWhenShutdown: false From 79733b963b8ba336aee5f4c2835f7678e1fc6aa0 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 23:42:03 +0800 Subject: [PATCH 0200/1392] grid2: fix take profit handler --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index dbfa9217b6..ee0741baab 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -626,7 +626,7 @@ func (s *Strategy) newStopLossPriceHandler(ctx context.Context, session *bbgo.Ex func (s *Strategy) newTakeProfitHandler(ctx context.Context, session *bbgo.ExchangeSession) types.KLineCallback { return types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { - if s.TakeProfitPrice.Compare(k.High) < 0 { + if s.TakeProfitPrice.Compare(k.High) > 0 { return } From 64082246633e3dfe9cae35ec9af0e13c3379b5e9 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 5 Dec 2022 23:54:20 +0800 Subject: [PATCH 0201/1392] bbgo: add TradeStore prune --- pkg/bbgo/trade_store.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/bbgo/trade_store.go b/pkg/bbgo/trade_store.go index e16ab1c3c6..a19c63f2df 100644 --- a/pkg/bbgo/trade_store.go +++ b/pkg/bbgo/trade_store.go @@ -2,10 +2,13 @@ package bbgo import ( "sync" + "time" "github.com/c9s/bbgo/pkg/types" ) +const TradeExpiryTime = 24 * time.Hour + type TradeStore struct { // any created trades for tracking trades sync.Mutex @@ -96,6 +99,29 @@ func (s *TradeStore) Add(trades ...types.Trade) { } } +// pruneExpiredTrades prunes trades that are older than the expiry time +// see TradeExpiryTime +func (s *TradeStore) pruneExpiredTrades(curTime time.Time) { + s.Lock() + defer s.Unlock() + + var trades = make(map[uint64]types.Trade) + var cutOffTime = curTime.Add(-TradeExpiryTime) + for _, trade := range s.trades { + if trade.Time.Before(cutOffTime) { + continue + } + + trades[trade.ID] = trade + } + + s.trades = trades +} + +func (s *TradeStore) Prune(curTime time.Time) { + s.pruneExpiredTrades(curTime) +} + func (s *TradeStore) BindStream(stream types.Stream) { stream.OnTradeUpdate(func(trade types.Trade) { s.Add(trade) From beb862be44e6f5a56f8c64aa670a5c96f2577a72 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 00:15:09 +0800 Subject: [PATCH 0202/1392] bbgo: add TradeStore prune func and its tests --- pkg/bbgo/trade_store.go | 19 +++++++++++++++++- pkg/bbgo/trade_store_test.go | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 pkg/bbgo/trade_store_test.go diff --git a/pkg/bbgo/trade_store.go b/pkg/bbgo/trade_store.go index a19c63f2df..6056318684 100644 --- a/pkg/bbgo/trade_store.go +++ b/pkg/bbgo/trade_store.go @@ -8,12 +8,14 @@ import ( ) const TradeExpiryTime = 24 * time.Hour +const PruneTriggerNumOfTrades = 10_000 type TradeStore struct { // any created trades for tracking trades sync.Mutex - trades map[uint64]types.Trade + trades map[uint64]types.Trade + lastTradeTime time.Time } func NewTradeStore() *TradeStore { @@ -96,6 +98,13 @@ func (s *TradeStore) Add(trades ...types.Trade) { for _, trade := range trades { s.trades[trade.ID] = trade + s.touchLastTradeTime(trade) + } +} + +func (s *TradeStore) touchLastTradeTime(trade types.Trade) { + if trade.Time.Time().After(s.lastTradeTime) { + s.lastTradeTime = trade.Time.Time() } } @@ -122,8 +131,16 @@ func (s *TradeStore) Prune(curTime time.Time) { s.pruneExpiredTrades(curTime) } +func (s *TradeStore) isCoolTrade(trade types.Trade) bool { + // if the time of last trade is over 1 hour, we call it's cool trade + return s.lastTradeTime != (time.Time{}) && time.Time(trade.Time).Sub(s.lastTradeTime) > time.Hour +} + func (s *TradeStore) BindStream(stream types.Stream) { stream.OnTradeUpdate(func(trade types.Trade) { s.Add(trade) + if s.isCoolTrade(trade) { + s.Prune(time.Time(trade.Time)) + } }) } diff --git a/pkg/bbgo/trade_store_test.go b/pkg/bbgo/trade_store_test.go new file mode 100644 index 0000000000..f3a5a1ed9e --- /dev/null +++ b/pkg/bbgo/trade_store_test.go @@ -0,0 +1,39 @@ +package bbgo + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" +) + +func TestTradeStore_isCoolTrade(t *testing.T) { + now := time.Now() + store := NewTradeStore() + store.lastTradeTime = now.Add(-2 * time.Hour) + ok := store.isCoolTrade(types.Trade{ + Time: types.Time(now), + }) + assert.True(t, ok) + + store.lastTradeTime = now.Add(-2 * time.Minute) + ok = store.isCoolTrade(types.Trade{ + Time: types.Time(now), + }) + assert.False(t, ok) +} + +func TestTradeStore_Prune(t *testing.T) { + now := time.Now() + store := NewTradeStore() + store.Add( + types.Trade{ID: 1, Time: types.Time(now.Add(-25 * time.Hour))}, + types.Trade{ID: 2, Time: types.Time(now.Add(-23 * time.Hour))}, + types.Trade{ID: 3, Time: types.Time(now.Add(-2 * time.Minute))}, + types.Trade{ID: 4, Time: types.Time(now.Add(-1 * time.Minute))}, + ) + store.Prune(now) + assert.Equal(t, 3, len(store.trades)) +} From a6205e0d1d0084669f3517c2738982765c0bdfc1 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 00:28:38 +0800 Subject: [PATCH 0203/1392] bbgo: add EnablePrune option --- pkg/bbgo/trade_store.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/bbgo/trade_store.go b/pkg/bbgo/trade_store.go index 6056318684..ffe9011f9d 100644 --- a/pkg/bbgo/trade_store.go +++ b/pkg/bbgo/trade_store.go @@ -14,6 +14,8 @@ type TradeStore struct { // any created trades for tracking trades sync.Mutex + EnablePrune bool + trades map[uint64]types.Trade lastTradeTime time.Time } @@ -139,8 +141,13 @@ func (s *TradeStore) isCoolTrade(trade types.Trade) bool { func (s *TradeStore) BindStream(stream types.Stream) { stream.OnTradeUpdate(func(trade types.Trade) { s.Add(trade) - if s.isCoolTrade(trade) { - s.Prune(time.Time(trade.Time)) - } }) + + if s.EnablePrune { + stream.OnTradeUpdate(func(trade types.Trade) { + if s.isCoolTrade(trade) { + s.Prune(time.Time(trade.Time)) + } + }) + } } From bee528c7c56a5c91fefd32c41fb7dd7c9ca92d1c Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 00:55:08 +0800 Subject: [PATCH 0204/1392] grid2: set enable prune for trade history --- pkg/strategy/grid2/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index ee0741baab..f8280af5aa 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -923,6 +923,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } s.historicalTrades = bbgo.NewTradeStore() + s.historicalTrades.EnablePrune = true s.historicalTrades.BindStream(session.UserDataStream) s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) From a8c957fc8d75f9b0b850f9471f96452c6cc89435 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 01:17:29 +0800 Subject: [PATCH 0205/1392] grid2: fix profit spread behavior and tests --- pkg/strategy/grid2/strategy.go | 16 +++++++++++-- pkg/strategy/grid2/strategy_test.go | 36 +++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f8280af5aa..9ea25a2dc1 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -37,6 +37,11 @@ type Strategy struct { Symbol string `json:"symbol"` // ProfitSpread is the fixed profit spread you want to submit the sell order + // When ProfitSpread is enabled, the grid will shift up, e.g., + // If you opened a grid with the price range 10_000 to 20_000 + // With profit spread set to 3_000 + // The sell orders will be placed in the range 13_000 to 23_000 + // And the buy orders will be placed in the original price range 10_000 to 20_000 ProfitSpread fixedpoint.Value `json:"profitSpread"` // GridNum is the grid number, how many orders you want to post on the orderbook. @@ -786,6 +791,13 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) + sellPrice := price + + // when profitSpread is set, the sell price is shift upper with the given spread + if s.ProfitSpread.Sign() > 0 { + sellPrice = sellPrice.Add(s.ProfitSpread) + } + quantity := s.QuantityOrAmount.Quantity if quantity.IsZero() { quantity = s.QuantityOrAmount.Amount.Div(price) @@ -799,7 +811,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin Symbol: s.Symbol, Type: types.OrderTypeLimit, Side: types.SideTypeSell, - Price: price, + Price: sellPrice, Quantity: quantity, Market: s.Market, TimeInForce: types.TimeInForceGTC, @@ -826,7 +838,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin // skip i == 0 } } else { - if i+1 == si { + if s.ProfitSpread.IsZero() && i+1 == si { continue } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 427c24c419..611cf832e3 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -56,8 +56,8 @@ type PriceSideAssert struct { func assertPriceSide(t *testing.T, priceSideAsserts []PriceSideAssert, orders []types.SubmitOrder) { for i, a := range priceSideAsserts { - assert.Equal(t, a.Side, orders[i].Side) - assert.Equal(t, a.Price, orders[i].Price) + assert.Equalf(t, a.Price, orders[i].Price, "order #%d price should be %f", i+1, a.Price.Float64()) + assert.Equalf(t, a.Side, orders[i].Side, "order at price %f should be %s", a.Price.Float64(), a.Side) } } @@ -149,6 +149,38 @@ func TestStrategy_generateGridOrders(t *testing.T) { }, orders) }) + t.Run("enough base + quote + profitSpread", func(t *testing.T) { + s := newTestStrategy() + s.ProfitSpread = number(1_000) + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + s.QuantityOrAmount.Quantity = number(0.01) + + lastPrice := number(15300) + orders, err := s.generateGridOrders(number(10000.0), number(1.0), lastPrice) + assert.NoError(t, err) + if !assert.Equal(t, 11, len(orders)) { + for _, o := range orders { + t.Logf("- %s %s", o.Price.String(), o.Side) + } + } + + assertPriceSide(t, []PriceSideAssert{ + {number(21000.0), types.SideTypeSell}, + {number(20000.0), types.SideTypeSell}, + {number(19000.0), types.SideTypeSell}, + {number(18000.0), types.SideTypeSell}, + {number(17000.0), types.SideTypeSell}, + + {number(15000.0), types.SideTypeBuy}, + {number(14000.0), types.SideTypeBuy}, + {number(13000.0), types.SideTypeBuy}, + {number(12000.0), types.SideTypeBuy}, + {number(11000.0), types.SideTypeBuy}, + {number(10000.0), types.SideTypeBuy}, + }, orders) + }) + } func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { From 541c0e76b52839bb608a44b40b4a3ba046e755d7 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 01:19:24 +0800 Subject: [PATCH 0206/1392] grid2: consider profitSpread in calculateQuoteBaseInvestmentQuantity --- pkg/strategy/grid2/strategy.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 9ea25a2dc1..c636dfcc81 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -444,9 +444,10 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am } } else { // for orders that buy - if i+1 == si { + if s.ProfitSpread.IsZero() && i+1 == si { continue } + requiredQuote = requiredQuote.Add(amount) } } @@ -502,7 +503,7 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f } } else { // for orders that buy - if i+1 == si { + if s.ProfitSpread.IsZero() && i+1 == si { continue } @@ -525,9 +526,15 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) - if price.Compare(lastPrice) < 0 { + sellPrice := price + if s.ProfitSpread.Sign() > 0 { + sellPrice = sellPrice.Add(s.ProfitSpread) + } + + if sellPrice.Compare(lastPrice) < 0 { break } + numberOfSellOrders++ } From 7e0ac66ea1a0e14c74b039f7fa4e3206a91da373 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 01:21:41 +0800 Subject: [PATCH 0207/1392] grid2: fix calculateQuoteBaseInvestmentQuantity grid calculation --- pkg/strategy/grid2/strategy.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c636dfcc81..555db5ed50 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -534,7 +534,7 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv if sellPrice.Compare(lastPrice) < 0 { break } - + numberOfSellOrders++ } @@ -552,17 +552,25 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv s.logger.Infof("grid base investment quantity range: %f <=> %f", minBaseQuantity.Float64(), maxBaseQuantity.Float64()) } - buyPlacedPrice := fixedpoint.Zero totalQuotePrice := fixedpoint.Zero // quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + .... // => // quoteInvestment = (p1 + p2 + p3) * q // maxBuyQuantity = quoteInvestment / (p1 + p2 + p3) + si := len(pins) - 1 for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) + sellPrice := price + + // when profitSpread is set, the sell price is shift upper with the given spread + if s.ProfitSpread.Sign() > 0 { + sellPrice = sellPrice.Add(s.ProfitSpread) + } if price.Compare(lastPrice) >= 0 { + si = i + // for orders that sell // if we still have the base balance // quantity := amount.Div(lastPrice) @@ -572,11 +580,10 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv nextLowerPrice := fixedpoint.Value(nextLowerPin) // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) - buyPlacedPrice = nextLowerPrice } } else { // for orders that buy - if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { + if s.ProfitSpread.IsZero() && i+1 == si { continue } From e7ff7a49db30ccd1618eea4258593158c4d01c8a Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 01:51:50 +0800 Subject: [PATCH 0208/1392] grid2: fix calculateQuoteInvestmentQuantity for profitSpread --- pkg/strategy/grid2/strategy.go | 17 ++++++++------ pkg/strategy/grid2/strategy_test.go | 35 ++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 555db5ed50..b00803e914 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -364,7 +364,7 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, requiredQuote = fixedpoint.Zero // when we need to place a buy-to-sell conversion order, we need to mark the price - si := len(pins) - 1 + si := -1 for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) @@ -423,7 +423,7 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am requiredQuote = fixedpoint.Zero // when we need to place a buy-to-sell conversion order, we need to mark the price - si := len(pins) - 1 + si := -1 for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) @@ -484,7 +484,7 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f // quoteInvestment = (p1 + p2 + p3) * q // q = quoteInvestment / (p1 + p2 + p3) totalQuotePrice := fixedpoint.Zero - si := len(pins) - 1 + si := -1 for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) @@ -494,11 +494,12 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f // for orders that sell // if we still have the base balance // quantity := amount.Div(lastPrice) - if i > 0 { // we do not want to sell at i == 0 + if s.ProfitSpread.Sign() > 0 { + totalQuotePrice = totalQuotePrice.Add(price) + } else if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) - // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) } } else { @@ -557,7 +558,7 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv // => // quoteInvestment = (p1 + p2 + p3) * q // maxBuyQuantity = quoteInvestment / (p1 + p2 + p3) - si := len(pins) - 1 + si := -1 for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) @@ -568,13 +569,15 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv sellPrice = sellPrice.Add(s.ProfitSpread) } + // buy price greater than the last price will trigger taker order. if price.Compare(lastPrice) >= 0 { si = i // for orders that sell // if we still have the base balance // quantity := amount.Div(lastPrice) - if i > 0 { // we do not want to sell at i == 0 + if i > 0 { + // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 611cf832e3..12ebdbec9f 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -210,20 +210,14 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { } func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { - s := &Strategy{ - logger: logrus.NewEntry(logrus.New()), - Market: types.Market{ - BaseCurrency: "BTC", - QuoteCurrency: "USDT", - }, - } - - t.Run("calculate quote quantity from quote investment", func(t *testing.T) { + t.Run("quote quantity", func(t *testing.T) { // quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000) * q // q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000) // q = 12_000 / (10,000 + 11,000 + 12,000 + 13,000 + 14,000) // q = 0.2 - quantity, err := s.calculateQuoteInvestmentQuantity(number(12_000.0), number(13_500.0), []Pin{ + s := newTestStrategy() + lastPrice := number(13_500.0) + quantity, err := s.calculateQuoteInvestmentQuantity(number(12_000.0), lastPrice, []Pin{ Pin(number(10_000.0)), // buy Pin(number(11_000.0)), // buy Pin(number(12_000.0)), // buy @@ -234,6 +228,27 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { assert.NoError(t, err) assert.Equal(t, number(0.2).String(), quantity.String()) }) + + t.Run("profit spread", func(t *testing.T) { + // quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000) * q + // q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000) + // q = 7500 / (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000) + // q = 0.1 + s := newTestStrategy() + s.ProfitSpread = number(2000.0) + lastPrice := number(13_500.0) + quantity, err := s.calculateQuoteInvestmentQuantity(number(7500.0), lastPrice, []Pin{ + Pin(number(10_000.0)), // sell order @ 12_000 + Pin(number(11_000.0)), // sell order @ 13_000 + Pin(number(12_000.0)), // sell order @ 14_000 + Pin(number(13_000.0)), // sell order @ 15_000 + Pin(number(14_000.0)), // sell order @ 16_000 + Pin(number(15_000.0)), // sell order @ 17_000 + }) + assert.NoError(t, err) + assert.Equal(t, number(0.1).String(), quantity.String()) + }) + } func newTestStrategy() *Strategy { From fc80cfb714ddb6d9eaad3feac12fcd18fbde61c4 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 01:57:33 +0800 Subject: [PATCH 0209/1392] grid2: fix quote investment calculation for profit spread --- pkg/strategy/grid2/strategy.go | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b00803e914..a38d52ccca 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -553,32 +553,28 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv s.logger.Infof("grid base investment quantity range: %f <=> %f", minBaseQuantity.Float64(), maxBaseQuantity.Float64()) } + // calculate quantity with quote investment totalQuotePrice := fixedpoint.Zero // quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + .... // => // quoteInvestment = (p1 + p2 + p3) * q // maxBuyQuantity = quoteInvestment / (p1 + p2 + p3) si := -1 - for i := len(pins) - 1; i >= 0; i-- { + for i := len(pins) - 1 - maxNumberOfSellOrders; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) - sellPrice := price - - // when profitSpread is set, the sell price is shift upper with the given spread - if s.ProfitSpread.Sign() > 0 { - sellPrice = sellPrice.Add(s.ProfitSpread) - } // buy price greater than the last price will trigger taker order. if price.Compare(lastPrice) >= 0 { si = i - // for orders that sell - // if we still have the base balance - // quantity := amount.Div(lastPrice) - if i > 0 { - // we do not want to sell at i == 0 - // convert sell to buy quote and add to requiredQuote + // when profit spread is set, we count all the grid prices as buy prices + if s.ProfitSpread.Sign() > 0 { + totalQuotePrice = totalQuotePrice.Add(price) + } else if i > 0 { + // when profit spread is not set + // we do not want to place sell order at i == 0 + // here we submit an order to convert a buy order into a sell order nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) From dd591c936f86f50f9e82106937b1ee50308bfab0 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 02:07:05 +0800 Subject: [PATCH 0210/1392] grid2: add min order quantity protection --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index a38d52ccca..e410453526 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -307,7 +307,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { // use the profit to buy more inventory in the grid if s.Compound || s.EarnBase { - newQuantity = orderQuoteQuantity.Div(newPrice) + newQuantity = fixedpoint.Max(orderQuoteQuantity.Div(newPrice), s.Market.MinQuantity) } // calculate profit @@ -327,7 +327,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } if s.EarnBase { - newQuantity = orderQuoteQuantity.Div(newPrice).Sub(baseSellQuantityReduction) + newQuantity = fixedpoint.Max(orderQuoteQuantity.Div(newPrice).Sub(baseSellQuantityReduction), s.Market.MinQuantity) } } From aa5f2a032a49aebe58093b8b06249ed630737b4e Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 02:13:32 +0800 Subject: [PATCH 0211/1392] grid2: call TruncatePrice on profitSpread --- config/grid2.yaml | 32 ++++++++++++++++++++------------ pkg/strategy/grid2/strategy.go | 4 ++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/config/grid2.yaml b/config/grid2.yaml index 242e2c914b..21a3c49952 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -16,8 +16,8 @@ sessions: # example command: # go run ./cmd/bbgo backtest --config config/grid2.yaml --base-asset-baseline backtest: - startTime: "2022-06-01" - endTime: "2022-06-30" + startTime: "2021-06-01" + endTime: "2021-12-31" symbols: - BTCUSDT sessions: [binance] @@ -25,16 +25,16 @@ backtest: binance: balances: BTC: 0.0 - USDT: 10000.0 + USDT: 21_000.0 exchangeStrategies: - on: binance grid2: symbol: BTCUSDT - upperPrice: 15_000.0 - lowerPrice: 10_000.0 - gridNumber: 10 + upperPrice: 60_000.0 + lowerPrice: 28_000.0 + gridNumber: 1000 ## compound is used for buying more inventory when the profit is made by the filled SELL order. ## when compound is disabled, fixed quantity is used for each grid order. @@ -45,14 +45,14 @@ exchangeStrategies: ## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair. # earnBase: true - ## triggerPrice is used for opening your grid only when the last price touches your trigger price. + ## triggerPrice (optional) is used for opening your grid only when the last price touches your trigger price. ## this is useful when you don't want to create a grid from a higher price. ## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000 - triggerPrice: 17_000.0 + # triggerPrice: 17_000.0 - ## triggerPrice is used for closing your grid only when the last price touches your stop loss price. + ## triggerPrice (optional) is used for closing your grid only when the last price touches your stop loss price. ## for example, when the price drops to 17_000.0 then close the grid and sell all base inventory. - stopLossPrice: 10_000.0 + # stopLossPrice: 10_000.0 ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. @@ -73,12 +73,20 @@ exchangeStrategies: ## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid. ## quoteInvestment is required, and baseInvestment is optional (could be zero) ## if you have existing BTC position and want to reuse it you can set the baseInvestment. - quoteInvestment: 10_000 + quoteInvestment: 20_000 - ## baseInvestment is optional + ## baseInvestment (optional) can be useful when you have existing inventory, maybe bought at much lower price baseInvestment: 0.0 + ## closeWhenCancelOrder (optional) + ## default to false closeWhenCancelOrder: true + + ## resetPositionWhenStart (optional) + ## default to false resetPositionWhenStart: false + + ## clearOpenOrdersWhenStart (optional) + ## default to false clearOpenOrdersWhenStart: false keepOrdersWhenShutdown: false diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e410453526..6ead2e953d 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -931,6 +931,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.groupID = util.FNV32(instanceID) s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + if s.ProfitSpread.Sign() > 0 { + s.ProfitSpread = s.Market.TruncatePrice(s.ProfitSpread) + } + if s.GridProfitStats == nil { s.GridProfitStats = newGridProfitStats(s.Market) } From 8482ad47939e4ac597d70e8d9de40a843b0aac8f Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 02:39:59 +0800 Subject: [PATCH 0212/1392] github: pull git lfs --- .github/workflows/go.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b18cdf76a8..1c89f9d3f4 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -26,6 +26,9 @@ jobs: steps: - uses: actions/checkout@v2 + with: + lfs: 'true' + ssh-key: ${{ secrets.git_ssh_key }} - uses: actions/cache@v2 with: From 35297b9bbfb2ff41e9a1c2e0837e1d9ceda1df84 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 02:30:04 +0800 Subject: [PATCH 0213/1392] bbgo: fix backtesting flag setter --- pkg/bbgo/environment.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index 9f57420dbc..9ac8ccca60 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -43,7 +43,7 @@ var BackTestService *service.BacktestService func SetBackTesting(s *service.BacktestService) { BackTestService = s - IsBackTesting = true + IsBackTesting = s != nil } var LoadedExchangeStrategies = make(map[string]SingleExchangeStrategy) From d9e230a433080db03d8b46a5dcc3112cd2879621 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 02:30:18 +0800 Subject: [PATCH 0214/1392] grid2: add TestBacktestStrategy skeleton for backtesting in unit test --- config/grid2.yaml | 2 +- pkg/strategy/grid2/strategy_test.go | 85 +++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/config/grid2.yaml b/config/grid2.yaml index 21a3c49952..b6f22190fb 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -17,7 +17,7 @@ sessions: # go run ./cmd/bbgo backtest --config config/grid2.yaml --base-asset-baseline backtest: startTime: "2021-06-01" - endTime: "2021-12-31" + endTime: "2021-06-30" symbols: - BTCUSDT sessions: [binance] diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 12ebdbec9f..039ada42e4 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -3,12 +3,17 @@ package grid2 import ( + "context" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/backtest" + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" ) @@ -322,3 +327,83 @@ func TestStrategy_calculateProfit(t *testing.T) { assert.InDelta(t, sellQuantity.Float64()-buyOrder.Quantity.Float64(), profit.Profit.Float64(), 0.001) }) } + +func TestBacktestStrategy(t *testing.T) { + startTime, err := types.ParseLooseFormatTime("2021-06-01") + assert.NoError(t, err) + + endTime, err := types.ParseLooseFormatTime("2021-06-30") + assert.NoError(t, err) + + backtestConfig := &bbgo.Backtest{ + StartTime: startTime, + EndTime: &endTime, + RecordTrades: false, + FeeMode: bbgo.BacktestFeeModeToken, + Accounts: map[string]bbgo.BacktestAccount{ + "binance": { + MakerFeeRate: number(0.075 * 0.01), + TakerFeeRate: number(0.075 * 0.01), + Balances: bbgo.BacktestAccountBalanceMap{ + "USDT": number(10_000.0), + "BTC": number(1.0), + }, + }, + }, + Symbols: []string{"BTCUSDT"}, + Sessions: []string{"binance"}, + SyncSecKLines: false, + } + + t.Logf("backtestConfig: %+v", backtestConfig) + + ctx := context.Background() + environ := bbgo.NewEnvironment() + err = environ.ConfigureDatabaseDriver(ctx, "sqlite3", "../../../data/bbgo_test.sqlite3") + assert.NoError(t, err) + + backtestService := &service.BacktestService{DB: environ.DatabaseService.DB} + defer func() { + err := environ.DatabaseService.DB.Close() + assert.NoError(t, err) + }() + + environ.BacktestService = backtestService + bbgo.SetBackTesting(backtestService) + defer bbgo.SetBackTesting(nil) + + exName, err := types.ValidExchangeName("binance") + if !assert.NoError(t, err) { + return + } + + publicExchange, err := exchange.NewPublic(exName) + if !assert.NoError(t, err) { + return + } + + backtestExchange, err := backtest.NewExchange(publicExchange.Name(), publicExchange, backtestService, backtestConfig) + if !assert.NoError(t, err) { + return + } + + session := environ.AddExchange(exName.String(), backtestExchange) + assert.NotNil(t, session) + + err = environ.Init(ctx) + assert.NoError(t, err) + + for _, ses := range environ.Sessions() { + userDataStream := ses.UserDataStream.(types.StandardStreamEmitter) + backtestEx := ses.Exchange.(*backtest.Exchange) + backtestEx.MarketDataStream = ses.MarketDataStream.(types.StandardStreamEmitter) + backtestEx.BindUserData(userDataStream) + } + + trader := bbgo.NewTrader(environ) + if assert.NotNil(t, trader) { + trader.DisableLogging() + } + + // TODO: add grid2 to the user config and run backtest +} From 2a22866d55e6eb888b5db1cef610c1af93f9f6eb Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 02:37:33 +0800 Subject: [PATCH 0215/1392] grid2: inject strategy into user config and run backtest --- pkg/strategy/grid2/strategy_test.go | 46 +++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 039ada42e4..d9d4cb04f3 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -258,9 +258,11 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { func newTestStrategy() *Strategy { market := types.Market{ - BaseCurrency: "BTC", - QuoteCurrency: "USDT", - TickSize: number(0.01), + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + TickSize: number(0.01), + PricePrecision: 2, + VolumePrecision: 8, } s := &Strategy{ @@ -270,6 +272,8 @@ func newTestStrategy() *Strategy { UpperPrice: number(20_000), LowerPrice: number(10_000), GridNum: 10, + + // QuoteInvestment: number(9000.0), } return s } @@ -329,6 +333,25 @@ func TestStrategy_calculateProfit(t *testing.T) { } func TestBacktestStrategy(t *testing.T) { + market := types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + TickSize: number(0.01), + PricePrecision: 2, + VolumePrecision: 8, + } + strategy := &Strategy{ + logger: logrus.NewEntry(logrus.New()), + Symbol: "BTCUSDT", + Market: market, + GridProfitStats: newGridProfitStats(market), + UpperPrice: number(60_000), + LowerPrice: number(28_000), + GridNum: 100, + QuoteInvestment: number(9000.0), + } + + // TEMPLATE {{{ start backtest startTime, err := types.ParseLooseFormatTime("2021-06-01") assert.NoError(t, err) @@ -359,6 +382,8 @@ func TestBacktestStrategy(t *testing.T) { ctx := context.Background() environ := bbgo.NewEnvironment() + environ.SetStartTime(startTime.Time()) + err = environ.ConfigureDatabaseDriver(ctx, "sqlite3", "../../../data/bbgo_test.sqlite3") assert.NoError(t, err) @@ -406,4 +431,19 @@ func TestBacktestStrategy(t *testing.T) { } // TODO: add grid2 to the user config and run backtest + userConfig := &bbgo.Config{ + ExchangeStrategies: []bbgo.ExchangeStrategyMount{ + { + Mounts: []string{"binance"}, + Strategy: strategy, + }, + }, + } + + err = trader.Configure(userConfig) + assert.NoError(t, err) + + err = trader.Run(ctx) + assert.NoError(t, err) + // }}} } From d1f3d201ef5fd71115e1af586de5be84467ca8c9 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 02:37:55 +0800 Subject: [PATCH 0216/1392] grid2: add todo in the test --- pkg/strategy/grid2/strategy_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index d9d4cb04f3..f14e867ceb 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -445,5 +445,8 @@ func TestBacktestStrategy(t *testing.T) { err = trader.Run(ctx) assert.NoError(t, err) + + // TODO: feed data + // }}} } From ca594e7883e099766b72cdd9c47f5adfcc8393f1 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 02:48:16 +0800 Subject: [PATCH 0217/1392] add data/binance-markets.json --- data/binance-markets.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 data/binance-markets.json diff --git a/data/binance-markets.json b/data/binance-markets.json new file mode 100644 index 0000000000..38b9f8b37e --- /dev/null +++ b/data/binance-markets.json @@ -0,0 +1 @@ +{"1INCHBTC":{"symbol":"1INCHBTC","localSymbol":"1INCHBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"1INCH","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"1INCHBUSD":{"symbol":"1INCHBUSD","localSymbol":"1INCHBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"1INCH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"1INCHDOWNUSDT":{"symbol":"1INCHDOWNUSDT","localSymbol":"1INCHDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"1INCHDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":2999958.00000000,"stepSize":0.01000000,"minPrice":0.00062699,"maxPrice":0.01190300,"tickSize":0.00000100},"1INCHUPUSDT":{"symbol":"1INCHUPUSDT","localSymbol":"1INCHUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"1INCHUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":300000.00000000,"stepSize":0.01000000,"minPrice":0.00633000,"maxPrice":0.12025500,"tickSize":0.00000100},"1INCHUSDT":{"symbol":"1INCHUSDT","localSymbol":"1INCHUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"1INCH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"AAVEBKRW":{"symbol":"AAVEBKRW","localSymbol":"AAVEBKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"AAVE","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.00100000,"maxQuantity":9000.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":5000000.00000000,"tickSize":1.00000000},"AAVEBNB":{"symbol":"AAVEBNB","localSymbol":"AAVEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AAVE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"AAVEBRL":{"symbol":"AAVEBRL","localSymbol":"AAVEBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"AAVE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":92233.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":999996.00000000,"tickSize":1.00000000},"AAVEBTC":{"symbol":"AAVEBTC","localSymbol":"AAVEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AAVE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"AAVEBUSD":{"symbol":"AAVEBUSD","localSymbol":"AAVEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AAVE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"AAVEDOWNUSDT":{"symbol":"AAVEDOWNUSDT","localSymbol":"AAVEDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AAVEDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":100000000.00000000,"stepSize":0.01000000,"minPrice":0.00047100,"maxPrice":0.00893399,"tickSize":0.00000100},"AAVEETH":{"symbol":"AAVEETH","localSymbol":"AAVEETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"AAVE","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":9000000.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AAVEUPUSDT":{"symbol":"AAVEUPUSDT","localSymbol":"AAVEUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AAVEUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.05970000,"maxPrice":1.13320000,"tickSize":0.00010000},"AAVEUSDT":{"symbol":"AAVEUSDT","localSymbol":"AAVEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AAVE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"ACABTC":{"symbol":"ACABTC","localSymbol":"ACABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ACA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ACABUSD":{"symbol":"ACABUSD","localSymbol":"ACABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ACA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ACAUSDT":{"symbol":"ACAUSDT","localSymbol":"ACAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ACA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ACHBTC":{"symbol":"ACHBTC","localSymbol":"ACHBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ACH","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ACHBUSD":{"symbol":"ACHBUSD","localSymbol":"ACHBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ACH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ACHUSDT":{"symbol":"ACHUSDT","localSymbol":"ACHUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ACH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ACMBTC":{"symbol":"ACMBTC","localSymbol":"ACMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ACM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ACMBUSD":{"symbol":"ACMBUSD","localSymbol":"ACMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ACM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ACMUSDT":{"symbol":"ACMUSDT","localSymbol":"ACMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ACM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ADAAUD":{"symbol":"ADAAUD","localSymbol":"ADAAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"ADA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"ADABIDR":{"symbol":"ADABIDR","localSymbol":"ADABIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"ADA","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.10000000,"maxQuantity":184467.00000000,"stepSize":0.10000000,"minPrice":1.00000000,"maxPrice":500000.00000000,"tickSize":1.00000000},"ADABKRW":{"symbol":"ADABKRW","localSymbol":"ADABKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"ADA","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ADABNB":{"symbol":"ADABNB","localSymbol":"ADABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ADA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ADABRL":{"symbol":"ADABRL","localSymbol":"ADABRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"ADA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ADABTC":{"symbol":"ADABTC","localSymbol":"ADABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ADA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ADABUSD":{"symbol":"ADABUSD","localSymbol":"ADABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ADA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ADADOWNUSDT":{"symbol":"ADADOWNUSDT","localSymbol":"ADADOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ADADOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":100000000.00000000,"stepSize":0.01000000,"minPrice":0.00058600,"maxPrice":0.01112700,"tickSize":0.00000100},"ADAETH":{"symbol":"ADAETH","localSymbol":"ADAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ADA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ADAEUR":{"symbol":"ADAEUR","localSymbol":"ADAEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"ADA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ADAGBP":{"symbol":"ADAGBP","localSymbol":"ADAGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"ADA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ADAPAX":{"symbol":"ADAPAX","localSymbol":"ADAPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"ADA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ADARUB":{"symbol":"ADARUB","localSymbol":"ADARUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"ADA","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ADATRY":{"symbol":"ADATRY","localSymbol":"ADATRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ADA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ADATUSD":{"symbol":"ADATUSD","localSymbol":"ADATUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"ADA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ADAUPUSDT":{"symbol":"ADAUPUSDT","localSymbol":"ADAUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ADAUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.03300000,"maxPrice":0.62500000,"tickSize":0.00100000},"ADAUSDC":{"symbol":"ADAUSDC","localSymbol":"ADAUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"ADA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ADAUSDT":{"symbol":"ADAUSDT","localSymbol":"ADAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ADA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ADXBNB":{"symbol":"ADXBNB","localSymbol":"ADXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ADX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ADXBTC":{"symbol":"ADXBTC","localSymbol":"ADXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ADX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ADXBUSD":{"symbol":"ADXBUSD","localSymbol":"ADXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ADX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ADXETH":{"symbol":"ADXETH","localSymbol":"ADXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ADX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ADXUSDT":{"symbol":"ADXUSDT","localSymbol":"ADXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ADX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AEBNB":{"symbol":"AEBNB","localSymbol":"AEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"AEBTC":{"symbol":"AEBTC","localSymbol":"AEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AEETH":{"symbol":"AEETH","localSymbol":"AEETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"AE","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"AERGOBTC":{"symbol":"AERGOBTC","localSymbol":"AERGOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AERGO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AERGOBUSD":{"symbol":"AERGOBUSD","localSymbol":"AERGOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AERGO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AGIBNB":{"symbol":"AGIBNB","localSymbol":"AGIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AGI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"AGIBTC":{"symbol":"AGIBTC","localSymbol":"AGIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AGI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AGIETH":{"symbol":"AGIETH","localSymbol":"AGIETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"AGI","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AGIXBTC":{"symbol":"AGIXBTC","localSymbol":"AGIXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AGIX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AGIXBUSD":{"symbol":"AGIXBUSD","localSymbol":"AGIXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AGIX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"AGLDBNB":{"symbol":"AGLDBNB","localSymbol":"AGLDBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AGLD","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"AGLDBTC":{"symbol":"AGLDBTC","localSymbol":"AGLDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AGLD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AGLDBUSD":{"symbol":"AGLDBUSD","localSymbol":"AGLDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AGLD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"AGLDUSDT":{"symbol":"AGLDUSDT","localSymbol":"AGLDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AGLD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"AIONBNB":{"symbol":"AIONBNB","localSymbol":"AIONBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AION","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"AIONBTC":{"symbol":"AIONBTC","localSymbol":"AIONBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AION","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AIONBUSD":{"symbol":"AIONBUSD","localSymbol":"AIONBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AION","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AIONETH":{"symbol":"AIONETH","localSymbol":"AIONETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"AION","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AIONUSDT":{"symbol":"AIONUSDT","localSymbol":"AIONUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AION","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"AKROBTC":{"symbol":"AKROBTC","localSymbol":"AKROBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AKRO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AKROBUSD":{"symbol":"AKROBUSD","localSymbol":"AKROBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AKRO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"AKROUSDT":{"symbol":"AKROUSDT","localSymbol":"AKROUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AKRO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ALCXBTC":{"symbol":"ALCXBTC","localSymbol":"ALCXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ALCX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00010000,"maxQuantity":92141578.00000000,"stepSize":0.00010000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ALCXBUSD":{"symbol":"ALCXBUSD","localSymbol":"ALCXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ALCX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":92233.00000000,"stepSize":0.00010000,"minPrice":0.10000000,"maxPrice":999996.00000000,"tickSize":0.10000000},"ALCXUSDT":{"symbol":"ALCXUSDT","localSymbol":"ALCXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ALCX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":92233.00000000,"stepSize":0.00010000,"minPrice":0.10000000,"maxPrice":999996.00000000,"tickSize":0.10000000},"ALGOBIDR":{"symbol":"ALGOBIDR","localSymbol":"ALGOBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"ALGO","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.01000000,"maxQuantity":9223371114.00000000,"stepSize":0.01000000,"minPrice":1.00000000,"maxPrice":10000000.00000000,"tickSize":1.00000000},"ALGOBNB":{"symbol":"ALGOBNB","localSymbol":"ALGOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ALGO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ALGOBTC":{"symbol":"ALGOBTC","localSymbol":"ALGOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ALGO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ALGOBUSD":{"symbol":"ALGOBUSD","localSymbol":"ALGOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ALGO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ALGOETH":{"symbol":"ALGOETH","localSymbol":"ALGOETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ALGO","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ALGOPAX":{"symbol":"ALGOPAX","localSymbol":"ALGOPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"ALGO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ALGORUB":{"symbol":"ALGORUB","localSymbol":"ALGORUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"ALGO","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ALGOTRY":{"symbol":"ALGOTRY","localSymbol":"ALGOTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ALGO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ALGOTUSD":{"symbol":"ALGOTUSD","localSymbol":"ALGOTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"ALGO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ALGOUSDC":{"symbol":"ALGOUSDC","localSymbol":"ALGOUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"ALGO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ALGOUSDT":{"symbol":"ALGOUSDT","localSymbol":"ALGOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ALGO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":100000.00000000,"tickSize":0.00010000},"ALICEBIDR":{"symbol":"ALICEBIDR","localSymbol":"ALICEBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"ALICE","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.01000000,"maxQuantity":922337194.00000000,"stepSize":0.01000000,"minPrice":1.00000000,"maxPrice":100000000.00000000,"tickSize":1.00000000},"ALICEBNB":{"symbol":"ALICEBNB","localSymbol":"ALICEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ALICE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ALICEBTC":{"symbol":"ALICEBTC","localSymbol":"ALICEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ALICE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ALICEBUSD":{"symbol":"ALICEBUSD","localSymbol":"ALICEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ALICE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ALICETRY":{"symbol":"ALICETRY","localSymbol":"ALICETRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ALICE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ALICEUSDT":{"symbol":"ALICEUSDT","localSymbol":"ALICEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ALICE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ALPACABNB":{"symbol":"ALPACABNB","localSymbol":"ALPACABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ALPACA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ALPACABTC":{"symbol":"ALPACABTC","localSymbol":"ALPACABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ALPACA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ALPACABUSD":{"symbol":"ALPACABUSD","localSymbol":"ALPACABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ALPACA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ALPACAUSDT":{"symbol":"ALPACAUSDT","localSymbol":"ALPACAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ALPACA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ALPHABNB":{"symbol":"ALPHABNB","localSymbol":"ALPHABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ALPHA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ALPHABTC":{"symbol":"ALPHABTC","localSymbol":"ALPHABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ALPHA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ALPHABUSD":{"symbol":"ALPHABUSD","localSymbol":"ALPHABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ALPHA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ALPHAUSDT":{"symbol":"ALPHAUSDT","localSymbol":"ALPHAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ALPHA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ALPINEBTC":{"symbol":"ALPINEBTC","localSymbol":"ALPINEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ALPINE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ALPINEBUSD":{"symbol":"ALPINEBUSD","localSymbol":"ALPINEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ALPINE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ALPINEEUR":{"symbol":"ALPINEEUR","localSymbol":"ALPINEEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"ALPINE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ALPINETRY":{"symbol":"ALPINETRY","localSymbol":"ALPINETRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ALPINE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"ALPINEUSDT":{"symbol":"ALPINEUSDT","localSymbol":"ALPINEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ALPINE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AMBBNB":{"symbol":"AMBBNB","localSymbol":"AMBBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AMB","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"AMBBTC":{"symbol":"AMBBTC","localSymbol":"AMBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AMB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AMBBUSD":{"symbol":"AMBBUSD","localSymbol":"AMBBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AMB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":100.00000000,"tickSize":0.00001000},"AMBETH":{"symbol":"AMBETH","localSymbol":"AMBETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"AMB","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AMPBNB":{"symbol":"AMPBNB","localSymbol":"AMPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AMP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AMPBTC":{"symbol":"AMPBTC","localSymbol":"AMPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AMP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AMPBUSD":{"symbol":"AMPBUSD","localSymbol":"AMPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AMP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"AMPUSDT":{"symbol":"AMPUSDT","localSymbol":"AMPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AMP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ANCBNB":{"symbol":"ANCBNB","localSymbol":"ANCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ANC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ANCBTC":{"symbol":"ANCBTC","localSymbol":"ANCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ANC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ANCBUSD":{"symbol":"ANCBUSD","localSymbol":"ANCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ANC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ANCUSDT":{"symbol":"ANCUSDT","localSymbol":"ANCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ANC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ANKRBNB":{"symbol":"ANKRBNB","localSymbol":"ANKRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ANKR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ANKRBTC":{"symbol":"ANKRBTC","localSymbol":"ANKRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ANKR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ANKRBUSD":{"symbol":"ANKRBUSD","localSymbol":"ANKRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ANKR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ANKRPAX":{"symbol":"ANKRPAX","localSymbol":"ANKRPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"ANKR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ANKRTRY":{"symbol":"ANKRTRY","localSymbol":"ANKRTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ANKR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ANKRTUSD":{"symbol":"ANKRTUSD","localSymbol":"ANKRTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"ANKR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ANKRUSDC":{"symbol":"ANKRUSDC","localSymbol":"ANKRUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"ANKR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ANKRUSDT":{"symbol":"ANKRUSDT","localSymbol":"ANKRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ANKR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ANTBNB":{"symbol":"ANTBNB","localSymbol":"ANTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ANT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ANTBTC":{"symbol":"ANTBTC","localSymbol":"ANTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ANT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ANTBUSD":{"symbol":"ANTBUSD","localSymbol":"ANTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ANT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ANTUSDT":{"symbol":"ANTUSDT","localSymbol":"ANTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ANT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ANYBTC":{"symbol":"ANYBTC","localSymbol":"ANYBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ANY","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ANYBUSD":{"symbol":"ANYBUSD","localSymbol":"ANYBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ANY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ANYUSDT":{"symbol":"ANYUSDT","localSymbol":"ANYUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ANY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"APEAUD":{"symbol":"APEAUD","localSymbol":"APEAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"APE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"APEBNB":{"symbol":"APEBNB","localSymbol":"APEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"APE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"APEBRL":{"symbol":"APEBRL","localSymbol":"APEBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"APE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"APEBTC":{"symbol":"APEBTC","localSymbol":"APEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"APE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"APEBUSD":{"symbol":"APEBUSD","localSymbol":"APEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"APE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"APEETH":{"symbol":"APEETH","localSymbol":"APEETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"APE","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"APEEUR":{"symbol":"APEEUR","localSymbol":"APEEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"APE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"APEGBP":{"symbol":"APEGBP","localSymbol":"APEGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"APE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"APETRY":{"symbol":"APETRY","localSymbol":"APETRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"APE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"APEUSDT":{"symbol":"APEUSDT","localSymbol":"APEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"APE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"API3BNB":{"symbol":"API3BNB","localSymbol":"API3BNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"API3","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"API3BTC":{"symbol":"API3BTC","localSymbol":"API3BTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"API3","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"API3BUSD":{"symbol":"API3BUSD","localSymbol":"API3BUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"API3","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"API3TRY":{"symbol":"API3TRY","localSymbol":"API3TRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"API3","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"API3USDT":{"symbol":"API3USDT","localSymbol":"API3USDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"API3","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"APPCBNB":{"symbol":"APPCBNB","localSymbol":"APPCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"APPC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"APPCBTC":{"symbol":"APPCBTC","localSymbol":"APPCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"APPC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"APPCETH":{"symbol":"APPCETH","localSymbol":"APPCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"APPC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"APTBRL":{"symbol":"APTBRL","localSymbol":"APTBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"APT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"APTBTC":{"symbol":"APTBTC","localSymbol":"APTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"APT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":8384883677.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":10.00000000,"tickSize":0.00000001},"APTBUSD":{"symbol":"APTBUSD","localSymbol":"APTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"APT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"APTEUR":{"symbol":"APTEUR","localSymbol":"APTEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"APT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"APTTRY":{"symbol":"APTTRY","localSymbol":"APTTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"APT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"APTUSDT":{"symbol":"APTUSDT","localSymbol":"APTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"APT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ARBNB":{"symbol":"ARBNB","localSymbol":"ARBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":100000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ARBTC":{"symbol":"ARBTC","localSymbol":"ARBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":100000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ARBUSD":{"symbol":"ARBUSD","localSymbol":"ARBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":100000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ARDRBNB":{"symbol":"ARDRBNB","localSymbol":"ARDRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ARDR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ARDRBTC":{"symbol":"ARDRBTC","localSymbol":"ARDRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ARDR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ARDRETH":{"symbol":"ARDRETH","localSymbol":"ARDRETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ARDR","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ARDRUSDT":{"symbol":"ARDRUSDT","localSymbol":"ARDRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ARDR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ARKBTC":{"symbol":"ARKBTC","localSymbol":"ARKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ARK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ARKBUSD":{"symbol":"ARKBUSD","localSymbol":"ARKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ARK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ARKETH":{"symbol":"ARKETH","localSymbol":"ARKETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ARK","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ARNBTC":{"symbol":"ARNBTC","localSymbol":"ARNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ARN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ARNETH":{"symbol":"ARNETH","localSymbol":"ARNETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ARN","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ARPABNB":{"symbol":"ARPABNB","localSymbol":"ARPABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ARPA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ARPABTC":{"symbol":"ARPABTC","localSymbol":"ARPABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ARPA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ARPABUSD":{"symbol":"ARPABUSD","localSymbol":"ARPABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ARPA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ARPAETH":{"symbol":"ARPAETH","localSymbol":"ARPAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ARPA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":100.00000000,"tickSize":0.00000001},"ARPARUB":{"symbol":"ARPARUB","localSymbol":"ARPARUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"ARPA","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ARPATRY":{"symbol":"ARPATRY","localSymbol":"ARPATRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ARPA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ARPAUSDT":{"symbol":"ARPAUSDT","localSymbol":"ARPAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ARPA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ARUSDT":{"symbol":"ARUSDT","localSymbol":"ARUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":100000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ASRBTC":{"symbol":"ASRBTC","localSymbol":"ASRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ASR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ASRBUSD":{"symbol":"ASRBUSD","localSymbol":"ASRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ASR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ASRUSDT":{"symbol":"ASRUSDT","localSymbol":"ASRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ASR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ASTBTC":{"symbol":"ASTBTC","localSymbol":"ASTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AST","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ASTETH":{"symbol":"ASTETH","localSymbol":"ASTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"AST","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ASTRBTC":{"symbol":"ASTRBTC","localSymbol":"ASTRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ASTR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ASTRBUSD":{"symbol":"ASTRBUSD","localSymbol":"ASTRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ASTR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ASTRETH":{"symbol":"ASTRETH","localSymbol":"ASTRETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ASTR","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ASTRUSDT":{"symbol":"ASTRUSDT","localSymbol":"ASTRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ASTR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ATABNB":{"symbol":"ATABNB","localSymbol":"ATABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ATA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ATABTC":{"symbol":"ATABTC","localSymbol":"ATABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ATA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ATABUSD":{"symbol":"ATABUSD","localSymbol":"ATABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ATA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"ATAUSDT":{"symbol":"ATAUSDT","localSymbol":"ATAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ATA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"ATMBTC":{"symbol":"ATMBTC","localSymbol":"ATMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ATM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ATMBUSD":{"symbol":"ATMBUSD","localSymbol":"ATMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ATM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ATMUSDT":{"symbol":"ATMUSDT","localSymbol":"ATMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ATM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ATOMBIDR":{"symbol":"ATOMBIDR","localSymbol":"ATOMBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"ATOM","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.00100000,"maxQuantity":922337194.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":100000000.00000000,"tickSize":1.00000000},"ATOMBNB":{"symbol":"ATOMBNB","localSymbol":"ATOMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ATOM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ATOMBRL":{"symbol":"ATOMBRL","localSymbol":"ATOMBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"ATOM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ATOMBTC":{"symbol":"ATOMBTC","localSymbol":"ATOMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ATOM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ATOMBUSD":{"symbol":"ATOMBUSD","localSymbol":"ATOMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ATOM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ATOMETH":{"symbol":"ATOMETH","localSymbol":"ATOMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ATOM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ATOMEUR":{"symbol":"ATOMEUR","localSymbol":"ATOMEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"ATOM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ATOMPAX":{"symbol":"ATOMPAX","localSymbol":"ATOMPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"ATOM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ATOMTRY":{"symbol":"ATOMTRY","localSymbol":"ATOMTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ATOM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"ATOMTUSD":{"symbol":"ATOMTUSD","localSymbol":"ATOMTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"ATOM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ATOMUSDC":{"symbol":"ATOMUSDC","localSymbol":"ATOMUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"ATOM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ATOMUSDT":{"symbol":"ATOMUSDT","localSymbol":"ATOMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ATOM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"AUCTIONBTC":{"symbol":"AUCTIONBTC","localSymbol":"AUCTIONBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AUCTION","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"AUCTIONBUSD":{"symbol":"AUCTIONBUSD","localSymbol":"AUCTIONBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AUCTION","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"AUCTIONUSDT":{"symbol":"AUCTIONUSDT","localSymbol":"AUCTIONUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AUCTION","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"AUDBUSD":{"symbol":"AUDBUSD","localSymbol":"AUDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AUD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AUDIOBTC":{"symbol":"AUDIOBTC","localSymbol":"AUDIOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AUDIO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AUDIOBUSD":{"symbol":"AUDIOBUSD","localSymbol":"AUDIOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AUDIO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AUDIOTRY":{"symbol":"AUDIOTRY","localSymbol":"AUDIOTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"AUDIO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"AUDIOUSDT":{"symbol":"AUDIOUSDT","localSymbol":"AUDIOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AUDIO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AUDUSDC":{"symbol":"AUDUSDC","localSymbol":"AUDUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"AUD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AUDUSDT":{"symbol":"AUDUSDT","localSymbol":"AUDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AUD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AUTOBTC":{"symbol":"AUTOBTC","localSymbol":"AUTOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AUTO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"AUTOBUSD":{"symbol":"AUTOBUSD","localSymbol":"AUTOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AUTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":1000000.00000000,"tickSize":0.10000000},"AUTOUSDT":{"symbol":"AUTOUSDT","localSymbol":"AUTOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AUTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":1000000.00000000,"tickSize":0.10000000},"AVABNB":{"symbol":"AVABNB","localSymbol":"AVABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AVA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"AVABTC":{"symbol":"AVABTC","localSymbol":"AVABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AVA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"AVABUSD":{"symbol":"AVABUSD","localSymbol":"AVABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AVA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"AVAUSDT":{"symbol":"AVAUSDT","localSymbol":"AVAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AVA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"AVAXAUD":{"symbol":"AVAXAUD","localSymbol":"AVAXAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"AVAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"AVAXBIDR":{"symbol":"AVAXBIDR","localSymbol":"AVAXBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"AVAX","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.01000000,"maxQuantity":922337194.00000000,"stepSize":0.01000000,"minPrice":1.00000000,"maxPrice":100000000.00000000,"tickSize":1.00000000},"AVAXBNB":{"symbol":"AVAXBNB","localSymbol":"AVAXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AVAX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"AVAXBRL":{"symbol":"AVAXBRL","localSymbol":"AVAXBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"AVAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"AVAXBTC":{"symbol":"AVAXBTC","localSymbol":"AVAXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AVAX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"AVAXBUSD":{"symbol":"AVAXBUSD","localSymbol":"AVAXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AVAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"AVAXETH":{"symbol":"AVAXETH","localSymbol":"AVAXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"AVAX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"AVAXEUR":{"symbol":"AVAXEUR","localSymbol":"AVAXEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"AVAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"AVAXGBP":{"symbol":"AVAXGBP","localSymbol":"AVAXGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"AVAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"AVAXTRY":{"symbol":"AVAXTRY","localSymbol":"AVAXTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"AVAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"AVAXUSDT":{"symbol":"AVAXUSDT","localSymbol":"AVAXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AVAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"AXSAUD":{"symbol":"AXSAUD","localSymbol":"AXSAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"AXS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"AXSBNB":{"symbol":"AXSBNB","localSymbol":"AXSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"AXS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"AXSBRL":{"symbol":"AXSBRL","localSymbol":"AXSBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"AXS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"AXSBTC":{"symbol":"AXSBTC","localSymbol":"AXSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"AXS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"AXSBUSD":{"symbol":"AXSBUSD","localSymbol":"AXSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"AXS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"AXSETH":{"symbol":"AXSETH","localSymbol":"AXSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"AXS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"AXSTRY":{"symbol":"AXSTRY","localSymbol":"AXSTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"AXS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"AXSUSDT":{"symbol":"AXSUSDT","localSymbol":"AXSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"AXS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"BADGERBTC":{"symbol":"BADGERBTC","localSymbol":"BADGERBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BADGER","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BADGERBUSD":{"symbol":"BADGERBUSD","localSymbol":"BADGERBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BADGER","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"BADGERUSDT":{"symbol":"BADGERUSDT","localSymbol":"BADGERUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BADGER","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"BAKEBNB":{"symbol":"BAKEBNB","localSymbol":"BAKEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BAKE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BAKEBTC":{"symbol":"BAKEBTC","localSymbol":"BAKEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BAKE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BAKEBUSD":{"symbol":"BAKEBUSD","localSymbol":"BAKEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BAKE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BAKEUSDT":{"symbol":"BAKEUSDT","localSymbol":"BAKEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BAKE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"BALBNB":{"symbol":"BALBNB","localSymbol":"BALBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BAL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"BALBTC":{"symbol":"BALBTC","localSymbol":"BALBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BAL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BALBUSD":{"symbol":"BALBUSD","localSymbol":"BALBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BAL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"BALUSDT":{"symbol":"BALUSDT","localSymbol":"BALUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BAL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"BANDBNB":{"symbol":"BANDBNB","localSymbol":"BANDBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BAND","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BANDBTC":{"symbol":"BANDBTC","localSymbol":"BANDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BAND","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BANDBUSD":{"symbol":"BANDBUSD","localSymbol":"BANDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BAND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"BANDUSDT":{"symbol":"BANDUSDT","localSymbol":"BANDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BAND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"BARBTC":{"symbol":"BARBTC","localSymbol":"BARBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BAR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BARBUSD":{"symbol":"BARBUSD","localSymbol":"BARBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"BARUSDT":{"symbol":"BARUSDT","localSymbol":"BARUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"BATBNB":{"symbol":"BATBNB","localSymbol":"BATBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BAT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BATBTC":{"symbol":"BATBTC","localSymbol":"BATBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BAT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BATBUSD":{"symbol":"BATBUSD","localSymbol":"BATBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BAT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BATETH":{"symbol":"BATETH","localSymbol":"BATETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BAT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BATPAX":{"symbol":"BATPAX","localSymbol":"BATPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"BAT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BATTUSD":{"symbol":"BATTUSD","localSymbol":"BATTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"BAT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BATUSDC":{"symbol":"BATUSDC","localSymbol":"BATUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"BAT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BATUSDT":{"symbol":"BATUSDT","localSymbol":"BATUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BAT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BCCBNB":{"symbol":"BCCBNB","localSymbol":"BCCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BCC","minNotional":0.10000000,"minAmount":0.10000000,"minQuantity":0.00001000,"maxQuantity":900000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCCBTC":{"symbol":"BCCBTC","localSymbol":"BCCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BCC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BCCETH":{"symbol":"BCCETH","localSymbol":"BCCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BCC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":9000000.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BCCUSDT":{"symbol":"BCCUSDT","localSymbol":"BCCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BCC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCDBTC":{"symbol":"BCDBTC","localSymbol":"BCDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BCD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BCDETH":{"symbol":"BCDETH","localSymbol":"BCDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BCD","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":9000000.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BCHABCBTC":{"symbol":"BCHABCBTC","localSymbol":"BCHABCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BCHABC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BCHABCBUSD":{"symbol":"BCHABCBUSD","localSymbol":"BCHABCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BCHABC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCHABCPAX":{"symbol":"BCHABCPAX","localSymbol":"BCHABCPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"BCHABC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCHABCTUSD":{"symbol":"BCHABCTUSD","localSymbol":"BCHABCTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"BCHABC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCHABCUSDC":{"symbol":"BCHABCUSDC","localSymbol":"BCHABCUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"BCHABC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCHABCUSDT":{"symbol":"BCHABCUSDT","localSymbol":"BCHABCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BCHABC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCHABUSD":{"symbol":"BCHABUSD","localSymbol":"BCHABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BCHA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"BCHBNB":{"symbol":"BCHBNB","localSymbol":"BCHBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BCH","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BCHBTC":{"symbol":"BCHBTC","localSymbol":"BCHBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BCH","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BCHBUSD":{"symbol":"BCHBUSD","localSymbol":"BCHBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BCH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BCHDOWNUSDT":{"symbol":"BCHDOWNUSDT","localSymbol":"BCHDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BCHDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":100000.00000000,"tickSize":0.00010000},"BCHEUR":{"symbol":"BCHEUR","localSymbol":"BCHEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"BCH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BCHPAX":{"symbol":"BCHPAX","localSymbol":"BCHPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"BCH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCHSVBTC":{"symbol":"BCHSVBTC","localSymbol":"BCHSVBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BCHSV","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BCHSVPAX":{"symbol":"BCHSVPAX","localSymbol":"BCHSVPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"BCHSV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCHSVTUSD":{"symbol":"BCHSVTUSD","localSymbol":"BCHSVTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"BCHSV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCHSVUSDC":{"symbol":"BCHSVUSDC","localSymbol":"BCHSVUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"BCHSV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BCHSVUSDT":{"symbol":"BCHSVUSDT","localSymbol":"BCHSVUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BCHSV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":92141578.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"BCHTUSD":{"symbol":"BCHTUSD","localSymbol":"BCHTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"BCH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BCHUPUSDT":{"symbol":"BCHUPUSDT","localSymbol":"BCHUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BCHUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":100000.00000000,"tickSize":0.00001000},"BCHUSDC":{"symbol":"BCHUSDC","localSymbol":"BCHUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"BCH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BCHUSDT":{"symbol":"BCHUSDT","localSymbol":"BCHUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BCH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BCNBNB":{"symbol":"BCNBNB","localSymbol":"BCNBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BCN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BCNBTC":{"symbol":"BCNBTC","localSymbol":"BCNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BCN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BCNETH":{"symbol":"BCNETH","localSymbol":"BCNETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BCN","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BCPTBNB":{"symbol":"BCPTBNB","localSymbol":"BCPTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BCPT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BCPTBTC":{"symbol":"BCPTBTC","localSymbol":"BCPTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BCPT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BCPTETH":{"symbol":"BCPTETH","localSymbol":"BCPTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BCPT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BCPTPAX":{"symbol":"BCPTPAX","localSymbol":"BCPTPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"BCPT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BCPTTUSD":{"symbol":"BCPTTUSD","localSymbol":"BCPTTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"BCPT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BCPTUSDC":{"symbol":"BCPTUSDC","localSymbol":"BCPTUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"BCPT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BDOTDOT":{"symbol":"BDOTDOT","localSymbol":"BDOTDOT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"DOT","baseCurrency":"BDOT","minNotional":0.30000000,"minAmount":0.30000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BEAMBNB":{"symbol":"BEAMBNB","localSymbol":"BEAMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BEAM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BEAMBTC":{"symbol":"BEAMBTC","localSymbol":"BEAMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BEAM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BEAMUSDT":{"symbol":"BEAMUSDT","localSymbol":"BEAMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BEAM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BEARBUSD":{"symbol":"BEARBUSD","localSymbol":"BEARBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BEARUSDT":{"symbol":"BEARUSDT","localSymbol":"BEARUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BELBNB":{"symbol":"BELBNB","localSymbol":"BELBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BEL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BELBTC":{"symbol":"BELBTC","localSymbol":"BELBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BEL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BELBUSD":{"symbol":"BELBUSD","localSymbol":"BELBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BEL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BELETH":{"symbol":"BELETH","localSymbol":"BELETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BEL","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BELTRY":{"symbol":"BELTRY","localSymbol":"BELTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"BEL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"BELUSDT":{"symbol":"BELUSDT","localSymbol":"BELUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BEL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BETABNB":{"symbol":"BETABNB","localSymbol":"BETABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BETA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BETABTC":{"symbol":"BETABTC","localSymbol":"BETABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BETA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BETABUSD":{"symbol":"BETABUSD","localSymbol":"BETABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BETA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BETAETH":{"symbol":"BETAETH","localSymbol":"BETAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BETA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BETAUSDT":{"symbol":"BETAUSDT","localSymbol":"BETAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BETA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BETHBUSD":{"symbol":"BETHBUSD","localSymbol":"BETHBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":9000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BETHETH":{"symbol":"BETHETH","localSymbol":"BETHETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BETH","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00010000,"maxQuantity":92141578.00000000,"stepSize":0.00010000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BGBPUSDC":{"symbol":"BGBPUSDC","localSymbol":"BGBPUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"BGBP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BICOBTC":{"symbol":"BICOBTC","localSymbol":"BICOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BICO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BICOBUSD":{"symbol":"BICOBUSD","localSymbol":"BICOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BICO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BICOUSDT":{"symbol":"BICOUSDT","localSymbol":"BICOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BICO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BIFIBNB":{"symbol":"BIFIBNB","localSymbol":"BIFIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BIFI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BIFIBUSD":{"symbol":"BIFIBUSD","localSymbol":"BIFIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BIFI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BIFIUSDT":{"symbol":"BIFIUSDT","localSymbol":"BIFIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BIFI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BKRWBUSD":{"symbol":"BKRWBUSD","localSymbol":"BKRWBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BKRW","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BKRWUSDT":{"symbol":"BKRWUSDT","localSymbol":"BKRWUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BKRW","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BLZBNB":{"symbol":"BLZBNB","localSymbol":"BLZBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BLZ","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BLZBTC":{"symbol":"BLZBTC","localSymbol":"BLZBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BLZ","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BLZBUSD":{"symbol":"BLZBUSD","localSymbol":"BLZBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BLZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BLZETH":{"symbol":"BLZETH","localSymbol":"BLZETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BLZ","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BLZUSDT":{"symbol":"BLZUSDT","localSymbol":"BLZUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BLZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BNBAUD":{"symbol":"BNBAUD","localSymbol":"BNBAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BNBBEARBUSD":{"symbol":"BNBBEARBUSD","localSymbol":"BNBBEARBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BNBBEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BNBBEARUSDT":{"symbol":"BNBBEARUSDT","localSymbol":"BNBBEARUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BNBBEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BNBBIDR":{"symbol":"BNBBIDR","localSymbol":"BNBBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"BNB","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.00100000,"maxQuantity":100000.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":50000000.00000000,"tickSize":1.00000000},"BNBBKRW":{"symbol":"BNBBKRW","localSymbol":"BNBBKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"BNB","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.00100000,"maxQuantity":9000.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":5000000.00000000,"tickSize":1.00000000},"BNBBRL":{"symbol":"BNBBRL","localSymbol":"BNBBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":45000.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":100000.00000000,"tickSize":1.00000000},"BNBBTC":{"symbol":"BNBBTC","localSymbol":"BNBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BNB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":100000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":100000.00000000,"tickSize":0.00000100},"BNBBULLBUSD":{"symbol":"BNBBULLBUSD","localSymbol":"BNBBULLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BNBBULL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BNBBULLUSDT":{"symbol":"BNBBULLUSDT","localSymbol":"BNBBULLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BNBBULL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BNBBUSD":{"symbol":"BNBBUSD","localSymbol":"BNBBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BNBDAI":{"symbol":"BNBDAI","localSymbol":"BNBDAI","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"DAI","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BNBDOWNUSDT":{"symbol":"BNBDOWNUSDT","localSymbol":"BNBDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BNBDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.00197000,"maxPrice":0.03725000,"tickSize":0.00001000},"BNBETH":{"symbol":"BNBETH","localSymbol":"BNBETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BNB","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":9000000.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BNBEUR":{"symbol":"BNBEUR","localSymbol":"BNBEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":10000.00000000,"tickSize":0.10000000},"BNBGBP":{"symbol":"BNBGBP","localSymbol":"BNBGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BNBIDRT":{"symbol":"BNBIDRT","localSymbol":"BNBIDRT","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"IDRT","baseCurrency":"BNB","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.00100000,"maxQuantity":100000.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":50000000.00000000,"tickSize":1.00000000},"BNBNGN":{"symbol":"BNBNGN","localSymbol":"BNBNGN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"NGN","baseCurrency":"BNB","minNotional":500.00000000,"minAmount":500.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"BNBPAX":{"symbol":"BNBPAX","localSymbol":"BNBPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":10000.00000000,"tickSize":0.10000000},"BNBRUB":{"symbol":"BNBRUB","localSymbol":"BNBRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"BNB","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.00100000,"maxQuantity":184466.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":500000.00000000,"tickSize":0.01000000},"BNBTRY":{"symbol":"BNBTRY","localSymbol":"BNBTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":92232.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"BNBTUSD":{"symbol":"BNBTUSD","localSymbol":"BNBTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":10000.00000000,"tickSize":0.10000000},"BNBUAH":{"symbol":"BNBUAH","localSymbol":"BNBUAH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"UAH","baseCurrency":"BNB","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"BNBUPUSDT":{"symbol":"BNBUPUSDT","localSymbol":"BNBUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BNBUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":500.00000000,"stepSize":0.01000000,"minPrice":3.74000000,"maxPrice":70.97000000,"tickSize":0.01000000},"BNBUSDC":{"symbol":"BNBUSDC","localSymbol":"BNBUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BNBUSDP":{"symbol":"BNBUSDP","localSymbol":"BNBUSDP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDP","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BNBUSDS":{"symbol":"BNBUSDS","localSymbol":"BNBUSDS","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDS","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BNBUSDT":{"symbol":"BNBUSDT","localSymbol":"BNBUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BNBUST":{"symbol":"BNBUST","localSymbol":"BNBUST","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"UST","baseCurrency":"BNB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BNBZAR":{"symbol":"BNBZAR","localSymbol":"BNBZAR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ZAR","baseCurrency":"BNB","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.01000000,"maxQuantity":100000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BNTBTC":{"symbol":"BNTBTC","localSymbol":"BNTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BNT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BNTBUSD":{"symbol":"BNTBUSD","localSymbol":"BNTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BNT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BNTETH":{"symbol":"BNTETH","localSymbol":"BNTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BNT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BNTUSDT":{"symbol":"BNTUSDT","localSymbol":"BNTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BNT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BNXBNB":{"symbol":"BNXBNB","localSymbol":"BNXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BNX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BNXBTC":{"symbol":"BNXBTC","localSymbol":"BNXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BNX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BNXBUSD":{"symbol":"BNXBUSD","localSymbol":"BNXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BNX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BNXUSDT":{"symbol":"BNXUSDT","localSymbol":"BNXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BNX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"BONDBNB":{"symbol":"BONDBNB","localSymbol":"BONDBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BOND","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BONDBTC":{"symbol":"BONDBTC","localSymbol":"BONDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BOND","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BONDBUSD":{"symbol":"BONDBUSD","localSymbol":"BONDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BOND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"BONDETH":{"symbol":"BONDETH","localSymbol":"BONDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BOND","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BONDUSDT":{"symbol":"BONDUSDT","localSymbol":"BONDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BOND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"BOTBTC":{"symbol":"BOTBTC","localSymbol":"BOTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BOT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00010000,"maxQuantity":92141578.00000000,"stepSize":0.00010000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BOTBUSD":{"symbol":"BOTBUSD","localSymbol":"BOTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BQXBTC":{"symbol":"BQXBTC","localSymbol":"BQXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BQX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BQXETH":{"symbol":"BQXETH","localSymbol":"BQXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BQX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":10.00000000,"maxQuantity":90000000.00000000,"stepSize":10.00000000,"minPrice":10.00000000,"maxPrice":1000.00000000,"tickSize":10.00000000},"BRDBNB":{"symbol":"BRDBNB","localSymbol":"BRDBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BRD","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BRDBTC":{"symbol":"BRDBTC","localSymbol":"BRDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BRD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BRDETH":{"symbol":"BRDETH","localSymbol":"BRDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BRD","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BSWBNB":{"symbol":"BSWBNB","localSymbol":"BSWBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BSW","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BSWBUSD":{"symbol":"BSWBUSD","localSymbol":"BSWBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BSW","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BSWETH":{"symbol":"BSWETH","localSymbol":"BSWETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BSW","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BSWTRY":{"symbol":"BSWTRY","localSymbol":"BSWTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"BSW","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"BSWUSDT":{"symbol":"BSWUSDT","localSymbol":"BSWUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BSW","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BTCAUD":{"symbol":"BTCAUD","localSymbol":"BTCAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCBBTC":{"symbol":"BTCBBTC","localSymbol":"BTCBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BTCB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BTCBIDR":{"symbol":"BTCBIDR","localSymbol":"BTCBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"BTC","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.00001000,"maxQuantity":100000.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":9000000000.00000000,"tickSize":1.00000000},"BTCBKRW":{"symbol":"BTCBKRW","localSymbol":"BTCBKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"BTC","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.00000100,"maxQuantity":100.00000000,"stepSize":0.00000100,"minPrice":1.00000000,"maxPrice":700000000.00000000,"tickSize":1.00000000},"BTCBRL":{"symbol":"BTCBRL","localSymbol":"BTCBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":10000000.00000000,"tickSize":1.00000000},"BTCBUSD":{"symbol":"BTCBUSD","localSymbol":"BTCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCDAI":{"symbol":"BTCDAI","localSymbol":"BTCDAI","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"DAI","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCDOWNUSDT":{"symbol":"BTCDOWNUSDT","localSymbol":"BTCDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BTCDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":921415.00000000,"stepSize":0.01000000,"minPrice":0.00425199,"maxPrice":0.08078700,"tickSize":0.00000100},"BTCEUR":{"symbol":"BTCEUR","localSymbol":"BTCEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCGBP":{"symbol":"BTCGBP","localSymbol":"BTCGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCIDRT":{"symbol":"BTCIDRT","localSymbol":"BTCIDRT","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"IDRT","baseCurrency":"BTC","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.00001000,"maxQuantity":100000.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":9000000000.00000000,"tickSize":1.00000000},"BTCNGN":{"symbol":"BTCNGN","localSymbol":"BTCNGN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"NGN","baseCurrency":"BTC","minNotional":500.00000000,"minAmount":500.00000000,"minQuantity":0.00001000,"maxQuantity":305.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":300109718.00000000,"tickSize":1.00000000},"BTCPAX":{"symbol":"BTCPAX","localSymbol":"BTCPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCPLN":{"symbol":"BTCPLN","localSymbol":"BTCPLN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PLN","baseCurrency":"BTC","minNotional":50.00000000,"minAmount":50.00000000,"minQuantity":0.00001000,"maxQuantity":9223.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":9999319.00000000,"tickSize":1.00000000},"BTCRUB":{"symbol":"BTCRUB","localSymbol":"BTCRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"BTC","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.00001000,"maxQuantity":900.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":100000000.00000000,"tickSize":1.00000000},"BTCSTBTC":{"symbol":"BTCSTBTC","localSymbol":"BTCSTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BTCST","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BTCSTBUSD":{"symbol":"BTCSTBUSD","localSymbol":"BTCSTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BTCST","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"BTCSTUSDT":{"symbol":"BTCSTUSDT","localSymbol":"BTCSTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BTCST","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"BTCTRY":{"symbol":"BTCTRY","localSymbol":"BTCTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":10000000.00000000,"tickSize":1.00000000},"BTCTUSD":{"symbol":"BTCTUSD","localSymbol":"BTCTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCUAH":{"symbol":"BTCUAH","localSymbol":"BTCUAH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"UAH","baseCurrency":"BTC","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.00001000,"maxQuantity":1800.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":51212504.00000000,"tickSize":1.00000000},"BTCUPUSDT":{"symbol":"BTCUPUSDT","localSymbol":"BTCUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BTCUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":3000.00000000,"stepSize":0.01000000,"minPrice":0.28699999,"maxPrice":5.43500000,"tickSize":0.00100000},"BTCUSDC":{"symbol":"BTCUSDC","localSymbol":"BTCUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCUSDP":{"symbol":"BTCUSDP","localSymbol":"BTCUSDP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDP","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9223.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":9999319.00000000,"tickSize":0.01000000},"BTCUSDS":{"symbol":"BTCUSDS","localSymbol":"BTCUSDS","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDS","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00000100,"maxQuantity":9000.00000000,"stepSize":0.00000100,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCUSDT":{"symbol":"BTCUSDT","localSymbol":"BTCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCUST":{"symbol":"BTCUST","localSymbol":"BTCUST","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"UST","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCVAI":{"symbol":"BTCVAI","localSymbol":"BTCVAI","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"VAI","baseCurrency":"BTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BTCZAR":{"symbol":"BTCZAR","localSymbol":"BTCZAR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ZAR","baseCurrency":"BTC","minNotional":200.00000000,"minAmount":200.00000000,"minQuantity":0.00001000,"maxQuantity":922.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":99928191.00000000,"tickSize":1.00000000},"BTGBTC":{"symbol":"BTGBTC","localSymbol":"BTGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BTG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BTGBUSD":{"symbol":"BTGBUSD","localSymbol":"BTGBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BTG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"BTGETH":{"symbol":"BTGETH","localSymbol":"BTGETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BTG","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BTGUSDT":{"symbol":"BTGUSDT","localSymbol":"BTGUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BTG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"BTSBNB":{"symbol":"BTSBNB","localSymbol":"BTSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BTS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BTSBTC":{"symbol":"BTSBTC","localSymbol":"BTSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BTS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BTSBUSD":{"symbol":"BTSBUSD","localSymbol":"BTSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BTS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BTSETH":{"symbol":"BTSETH","localSymbol":"BTSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BTS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BTSUSDT":{"symbol":"BTSUSDT","localSymbol":"BTSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BTS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BTTBNB":{"symbol":"BTTBNB","localSymbol":"BTTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BTT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BTTBRL":{"symbol":"BTTBRL","localSymbol":"BTTBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"BTT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BTTBTC":{"symbol":"BTTBTC","localSymbol":"BTTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BTT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BTTBUSD":{"symbol":"BTTBUSD","localSymbol":"BTTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BTT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BTTCBUSD":{"symbol":"BTTCBUSD","localSymbol":"BTTCBUSD","pricePrecision":8,"volumePrecision":1,"quoteCurrency":"BUSD","baseCurrency":"BTTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92233720368.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":0.10000000,"tickSize":0.00000001},"BTTCTRY":{"symbol":"BTTCTRY","localSymbol":"BTTCTRY","pricePrecision":8,"volumePrecision":1,"quoteCurrency":"TRY","baseCurrency":"BTTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92233720368.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":0.10000000,"tickSize":0.00000001},"BTTCUSDC":{"symbol":"BTTCUSDC","localSymbol":"BTTCUSDC","pricePrecision":8,"volumePrecision":1,"quoteCurrency":"USDC","baseCurrency":"BTTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92233720368.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":0.10000000,"tickSize":0.00000001},"BTTCUSDT":{"symbol":"BTTCUSDT","localSymbol":"BTTCUSDT","pricePrecision":8,"volumePrecision":1,"quoteCurrency":"USDT","baseCurrency":"BTTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92233720368.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":0.10000000,"tickSize":0.00000001},"BTTEUR":{"symbol":"BTTEUR","localSymbol":"BTTEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"BTT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BTTPAX":{"symbol":"BTTPAX","localSymbol":"BTTPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"BTT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"BTTTRX":{"symbol":"BTTTRX","localSymbol":"BTTTRX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRX","baseCurrency":"BTT","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BTTTRY":{"symbol":"BTTTRY","localSymbol":"BTTTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"BTT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BTTTUSD":{"symbol":"BTTTUSD","localSymbol":"BTTTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"BTT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BTTUSDC":{"symbol":"BTTUSDC","localSymbol":"BTTUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"BTT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BTTUSDT":{"symbol":"BTTUSDT","localSymbol":"BTTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BTT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BULLBUSD":{"symbol":"BULLBUSD","localSymbol":"BULLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BULL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00000100,"maxQuantity":9000.00000000,"stepSize":0.00000100,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BULLUSDT":{"symbol":"BULLUSDT","localSymbol":"BULLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BULL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00000100,"maxQuantity":9000.00000000,"stepSize":0.00000100,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"BURGERBNB":{"symbol":"BURGERBNB","localSymbol":"BURGERBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BURGER","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BURGERBUSD":{"symbol":"BURGERBUSD","localSymbol":"BURGERBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BURGER","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"BURGERETH":{"symbol":"BURGERETH","localSymbol":"BURGERETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"BURGER","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"BURGERUSDT":{"symbol":"BURGERUSDT","localSymbol":"BURGERUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BURGER","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"BUSDBIDR":{"symbol":"BUSDBIDR","localSymbol":"BUSDBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"BUSD","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.10000000,"maxQuantity":1000000.00000000,"stepSize":0.10000000,"minPrice":1.00000000,"maxPrice":100099999.00000000,"tickSize":1.00000000},"BUSDBKRW":{"symbol":"BUSDBKRW","localSymbol":"BUSDBKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"BUSD","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BUSDBRL":{"symbol":"BUSDBRL","localSymbol":"BUSDBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"BUSD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BUSDBVND":{"symbol":"BUSDBVND","localSymbol":"BUSDBVND","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BVND","baseCurrency":"BUSD","minNotional":30000.00000000,"minAmount":30000.00000000,"minQuantity":1.00000000,"maxQuantity":1000000.00000000,"stepSize":1.00000000,"minPrice":11981.00000000,"maxPrice":35943.00000000,"tickSize":1.00000000},"BUSDDAI":{"symbol":"BUSDDAI","localSymbol":"BUSDDAI","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"DAI","baseCurrency":"BUSD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BUSDIDRT":{"symbol":"BUSDIDRT","localSymbol":"BUSDIDRT","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"IDRT","baseCurrency":"BUSD","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.01000000,"maxQuantity":1000000.00000000,"stepSize":0.01000000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"BUSDNGN":{"symbol":"BUSDNGN","localSymbol":"BUSDNGN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"NGN","baseCurrency":"BUSD","minNotional":500.00000000,"minAmount":500.00000000,"minQuantity":0.01000000,"maxQuantity":922320.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BUSDPLN":{"symbol":"BUSDPLN","localSymbol":"BUSDPLN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PLN","baseCurrency":"BUSD","minNotional":50.00000000,"minAmount":50.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BUSDRUB":{"symbol":"BUSDRUB","localSymbol":"BUSDRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"BUSD","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"BUSDTRY":{"symbol":"BUSDTRY","localSymbol":"BUSDTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"BUSD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"BUSDUAH":{"symbol":"BUSDUAH","localSymbol":"BUSDUAH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"UAH","baseCurrency":"BUSD","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"BUSDUSDT":{"symbol":"BUSDUSDT","localSymbol":"BUSDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BUSD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":15000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BUSDVAI":{"symbol":"BUSDVAI","localSymbol":"BUSDVAI","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"VAI","baseCurrency":"BUSD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"BUSDZAR":{"symbol":"BUSDZAR","localSymbol":"BUSDZAR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ZAR","baseCurrency":"BUSD","minNotional":200.00000000,"minAmount":200.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"BZRXBNB":{"symbol":"BZRXBNB","localSymbol":"BZRXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"BZRX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"BZRXBTC":{"symbol":"BZRXBTC","localSymbol":"BZRXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"BZRX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"BZRXBUSD":{"symbol":"BZRXBUSD","localSymbol":"BZRXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"BZRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"BZRXUSDT":{"symbol":"BZRXUSDT","localSymbol":"BZRXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"BZRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"C98BNB":{"symbol":"C98BNB","localSymbol":"C98BNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"C98","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"C98BRL":{"symbol":"C98BRL","localSymbol":"C98BRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"C98","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"C98BTC":{"symbol":"C98BTC","localSymbol":"C98BTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"C98","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"C98BUSD":{"symbol":"C98BUSD","localSymbol":"C98BUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"C98","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"C98USDT":{"symbol":"C98USDT","localSymbol":"C98USDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"C98","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CAKEAUD":{"symbol":"CAKEAUD","localSymbol":"CAKEAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"CAKE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"CAKEBNB":{"symbol":"CAKEBNB","localSymbol":"CAKEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CAKE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"CAKEBRL":{"symbol":"CAKEBRL","localSymbol":"CAKEBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"CAKE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"CAKEBTC":{"symbol":"CAKEBTC","localSymbol":"CAKEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CAKE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CAKEBUSD":{"symbol":"CAKEBUSD","localSymbol":"CAKEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CAKE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"CAKEGBP":{"symbol":"CAKEGBP","localSymbol":"CAKEGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"CAKE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"CAKEUSDT":{"symbol":"CAKEUSDT","localSymbol":"CAKEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CAKE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"CDTBTC":{"symbol":"CDTBTC","localSymbol":"CDTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CDT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CDTETH":{"symbol":"CDTETH","localSymbol":"CDTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"CDT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CELOBTC":{"symbol":"CELOBTC","localSymbol":"CELOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CELO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CELOBUSD":{"symbol":"CELOBUSD","localSymbol":"CELOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CELO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"CELOUSDT":{"symbol":"CELOUSDT","localSymbol":"CELOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CELO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"CELRBNB":{"symbol":"CELRBNB","localSymbol":"CELRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CELR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CELRBTC":{"symbol":"CELRBTC","localSymbol":"CELRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CELR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CELRBUSD":{"symbol":"CELRBUSD","localSymbol":"CELRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CELR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"CELRETH":{"symbol":"CELRETH","localSymbol":"CELRETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"CELR","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CELRUSDT":{"symbol":"CELRUSDT","localSymbol":"CELRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CELR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"CFXBTC":{"symbol":"CFXBTC","localSymbol":"CFXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CFX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CFXBUSD":{"symbol":"CFXBUSD","localSymbol":"CFXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CFX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"CFXUSDT":{"symbol":"CFXUSDT","localSymbol":"CFXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CFX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"CHATBTC":{"symbol":"CHATBTC","localSymbol":"CHATBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CHAT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CHATETH":{"symbol":"CHATETH","localSymbol":"CHATETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"CHAT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CHESSBNB":{"symbol":"CHESSBNB","localSymbol":"CHESSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CHESS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"CHESSBTC":{"symbol":"CHESSBTC","localSymbol":"CHESSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CHESS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CHESSBUSD":{"symbol":"CHESSBUSD","localSymbol":"CHESSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CHESS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"CHESSUSDT":{"symbol":"CHESSUSDT","localSymbol":"CHESSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CHESS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"CHRBNB":{"symbol":"CHRBNB","localSymbol":"CHRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CHR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CHRBTC":{"symbol":"CHRBTC","localSymbol":"CHRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CHR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CHRBUSD":{"symbol":"CHRBUSD","localSymbol":"CHRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CHR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":100000.00000000,"tickSize":0.00010000},"CHRETH":{"symbol":"CHRETH","localSymbol":"CHRETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"CHR","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CHRUSDT":{"symbol":"CHRUSDT","localSymbol":"CHRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CHR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CHZBNB":{"symbol":"CHZBNB","localSymbol":"CHZBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CHZ","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CHZBRL":{"symbol":"CHZBRL","localSymbol":"CHZBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"CHZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"CHZBTC":{"symbol":"CHZBTC","localSymbol":"CHZBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CHZ","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CHZBUSD":{"symbol":"CHZBUSD","localSymbol":"CHZBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CHZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CHZEUR":{"symbol":"CHZEUR","localSymbol":"CHZEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"CHZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CHZGBP":{"symbol":"CHZGBP","localSymbol":"CHZGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"CHZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CHZTRY":{"symbol":"CHZTRY","localSymbol":"CHZTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"CHZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"CHZUSDT":{"symbol":"CHZUSDT","localSymbol":"CHZUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CHZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CITYBNB":{"symbol":"CITYBNB","localSymbol":"CITYBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CITY","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"CITYBTC":{"symbol":"CITYBTC","localSymbol":"CITYBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CITY","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CITYBUSD":{"symbol":"CITYBUSD","localSymbol":"CITYBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CITY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"CITYUSDT":{"symbol":"CITYUSDT","localSymbol":"CITYUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CITY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"CKBBTC":{"symbol":"CKBBTC","localSymbol":"CKBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CKB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CKBBUSD":{"symbol":"CKBBUSD","localSymbol":"CKBBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CKB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"CKBUSDT":{"symbol":"CKBUSDT","localSymbol":"CKBUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CKB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"CLOAKBTC":{"symbol":"CLOAKBTC","localSymbol":"CLOAKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CLOAK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CLOAKETH":{"symbol":"CLOAKETH","localSymbol":"CLOAKETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"CLOAK","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"CLVBNB":{"symbol":"CLVBNB","localSymbol":"CLVBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CLV","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CLVBTC":{"symbol":"CLVBTC","localSymbol":"CLVBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CLV","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CLVBUSD":{"symbol":"CLVBUSD","localSymbol":"CLVBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CLV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"CLVUSDT":{"symbol":"CLVUSDT","localSymbol":"CLVUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CLV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"CMTBNB":{"symbol":"CMTBNB","localSymbol":"CMTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CMT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"CMTBTC":{"symbol":"CMTBTC","localSymbol":"CMTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CMT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CMTETH":{"symbol":"CMTETH","localSymbol":"CMTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"CMT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CNDBNB":{"symbol":"CNDBNB","localSymbol":"CNDBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CND","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CNDBTC":{"symbol":"CNDBTC","localSymbol":"CNDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CND","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CNDETH":{"symbol":"CNDETH","localSymbol":"CNDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"CND","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"COCOSBNB":{"symbol":"COCOSBNB","localSymbol":"COCOSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"COCOS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"COCOSBTC":{"symbol":"COCOSBTC","localSymbol":"COCOSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"COCOS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"COCOSBUSD":{"symbol":"COCOSBUSD","localSymbol":"COCOSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"COCOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"COCOSTRY":{"symbol":"COCOSTRY","localSymbol":"COCOSTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"COCOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"COCOSUSDT":{"symbol":"COCOSUSDT","localSymbol":"COCOSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"COCOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"COMPBNB":{"symbol":"COMPBNB","localSymbol":"COMPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"COMP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"COMPBTC":{"symbol":"COMPBTC","localSymbol":"COMPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"COMP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"COMPBUSD":{"symbol":"COMPBUSD","localSymbol":"COMPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"COMP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"COMPUSDT":{"symbol":"COMPUSDT","localSymbol":"COMPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"COMP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"COSBNB":{"symbol":"COSBNB","localSymbol":"COSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"COS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"COSBTC":{"symbol":"COSBTC","localSymbol":"COSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"COS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"COSBUSD":{"symbol":"COSBUSD","localSymbol":"COSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"COS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"COSTRY":{"symbol":"COSTRY","localSymbol":"COSTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"COS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"COSUSDT":{"symbol":"COSUSDT","localSymbol":"COSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"COS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":18443055.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":5000.00000000,"tickSize":0.00001000},"COTIBNB":{"symbol":"COTIBNB","localSymbol":"COTIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"COTI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"COTIBTC":{"symbol":"COTIBTC","localSymbol":"COTIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"COTI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"COTIBUSD":{"symbol":"COTIBUSD","localSymbol":"COTIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"COTI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"COTIUSDT":{"symbol":"COTIUSDT","localSymbol":"COTIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"COTI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"COVERBUSD":{"symbol":"COVERBUSD","localSymbol":"COVERBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"COVER","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"COVERETH":{"symbol":"COVERETH","localSymbol":"COVERETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"COVER","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"CREAMBNB":{"symbol":"CREAMBNB","localSymbol":"CREAMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CREAM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"CREAMBUSD":{"symbol":"CREAMBUSD","localSymbol":"CREAMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CREAM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"CRVBNB":{"symbol":"CRVBNB","localSymbol":"CRVBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CRV","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"CRVBTC":{"symbol":"CRVBTC","localSymbol":"CRVBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CRV","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CRVBUSD":{"symbol":"CRVBUSD","localSymbol":"CRVBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CRV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"CRVETH":{"symbol":"CRVETH","localSymbol":"CRVETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"CRV","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"CRVUSDT":{"symbol":"CRVUSDT","localSymbol":"CRVUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CRV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"CTKBNB":{"symbol":"CTKBNB","localSymbol":"CTKBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CTK","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"CTKBTC":{"symbol":"CTKBTC","localSymbol":"CTKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CTK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CTKBUSD":{"symbol":"CTKBUSD","localSymbol":"CTKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CTK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"CTKUSDT":{"symbol":"CTKUSDT","localSymbol":"CTKUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CTK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"CTSIBNB":{"symbol":"CTSIBNB","localSymbol":"CTSIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CTSI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"CTSIBTC":{"symbol":"CTSIBTC","localSymbol":"CTSIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CTSI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CTSIBUSD":{"symbol":"CTSIBUSD","localSymbol":"CTSIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CTSI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CTSIUSDT":{"symbol":"CTSIUSDT","localSymbol":"CTSIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CTSI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CTXCBNB":{"symbol":"CTXCBNB","localSymbol":"CTXCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CTXC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"CTXCBTC":{"symbol":"CTXCBTC","localSymbol":"CTXCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CTXC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CTXCBUSD":{"symbol":"CTXCBUSD","localSymbol":"CTXCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CTXC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CTXCUSDT":{"symbol":"CTXCUSDT","localSymbol":"CTXCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CTXC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CVCBNB":{"symbol":"CVCBNB","localSymbol":"CVCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"CVC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"CVCBTC":{"symbol":"CVCBTC","localSymbol":"CVCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CVC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"CVCBUSD":{"symbol":"CVCBUSD","localSymbol":"CVCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CVC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CVCETH":{"symbol":"CVCETH","localSymbol":"CVCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"CVC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CVCUSDT":{"symbol":"CVCUSDT","localSymbol":"CVCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CVC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CVPBUSD":{"symbol":"CVPBUSD","localSymbol":"CVPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CVP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CVPETH":{"symbol":"CVPETH","localSymbol":"CVPETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"CVP","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CVPUSDT":{"symbol":"CVPUSDT","localSymbol":"CVPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CVP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"CVXBTC":{"symbol":"CVXBTC","localSymbol":"CVXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"CVX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"CVXBUSD":{"symbol":"CVXBUSD","localSymbol":"CVXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"CVX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"CVXUSDT":{"symbol":"CVXUSDT","localSymbol":"CVXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"CVX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"DAIBNB":{"symbol":"DAIBNB","localSymbol":"DAIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DAI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DAIBTC":{"symbol":"DAIBTC","localSymbol":"DAIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DAI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DAIBUSD":{"symbol":"DAIBUSD","localSymbol":"DAIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DAI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DAIUSDT":{"symbol":"DAIUSDT","localSymbol":"DAIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DAI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DARBNB":{"symbol":"DARBNB","localSymbol":"DARBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DAR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DARBTC":{"symbol":"DARBTC","localSymbol":"DARBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DAR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DARBUSD":{"symbol":"DARBUSD","localSymbol":"DARBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DARETH":{"symbol":"DARETH","localSymbol":"DARETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DAR","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"DAREUR":{"symbol":"DAREUR","localSymbol":"DAREUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"DAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DARTRY":{"symbol":"DARTRY","localSymbol":"DARTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"DAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"DARUSDT":{"symbol":"DARUSDT","localSymbol":"DARUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DASHBNB":{"symbol":"DASHBNB","localSymbol":"DASHBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DASH","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DASHBTC":{"symbol":"DASHBTC","localSymbol":"DASHBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DASH","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"DASHBUSD":{"symbol":"DASHBUSD","localSymbol":"DASHBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DASH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"DASHETH":{"symbol":"DASHETH","localSymbol":"DASHETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DASH","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":9000000.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DASHUSDT":{"symbol":"DASHUSDT","localSymbol":"DASHUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DASH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"DATABTC":{"symbol":"DATABTC","localSymbol":"DATABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DATA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DATABUSD":{"symbol":"DATABUSD","localSymbol":"DATABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DATA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DATAETH":{"symbol":"DATAETH","localSymbol":"DATAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DATA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DATAUSDT":{"symbol":"DATAUSDT","localSymbol":"DATAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DATA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DCRBNB":{"symbol":"DCRBNB","localSymbol":"DCRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DCR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"DCRBTC":{"symbol":"DCRBTC","localSymbol":"DCRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DCR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"DCRBUSD":{"symbol":"DCRBUSD","localSymbol":"DCRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DCR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"DCRUSDT":{"symbol":"DCRUSDT","localSymbol":"DCRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DCR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"DEGOBTC":{"symbol":"DEGOBTC","localSymbol":"DEGOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DEGO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DEGOBUSD":{"symbol":"DEGOBUSD","localSymbol":"DEGOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DEGO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"DEGOUSDT":{"symbol":"DEGOUSDT","localSymbol":"DEGOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DEGO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"DENTBTC":{"symbol":"DENTBTC","localSymbol":"DENTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DENT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DENTBUSD":{"symbol":"DENTBUSD","localSymbol":"DENTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DENT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"DENTETH":{"symbol":"DENTETH","localSymbol":"DENTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DENT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DENTTRY":{"symbol":"DENTTRY","localSymbol":"DENTTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"DENT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DENTUSDT":{"symbol":"DENTUSDT","localSymbol":"DENTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DENT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"DEXEBUSD":{"symbol":"DEXEBUSD","localSymbol":"DEXEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DEXE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"DEXEETH":{"symbol":"DEXEETH","localSymbol":"DEXEETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DEXE","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"DEXEUSDT":{"symbol":"DEXEUSDT","localSymbol":"DEXEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DEXE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"DFBUSD":{"symbol":"DFBUSD","localSymbol":"DFBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DF","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DFETH":{"symbol":"DFETH","localSymbol":"DFETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DF","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"DFUSDT":{"symbol":"DFUSDT","localSymbol":"DFUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DF","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DGBBTC":{"symbol":"DGBBTC","localSymbol":"DGBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DGB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DGBBUSD":{"symbol":"DGBBUSD","localSymbol":"DGBBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DGB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DGBUSDT":{"symbol":"DGBUSDT","localSymbol":"DGBUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DGB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DGDBTC":{"symbol":"DGDBTC","localSymbol":"DGDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DGD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"DGDETH":{"symbol":"DGDETH","localSymbol":"DGDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DGD","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":9000000.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DIABNB":{"symbol":"DIABNB","localSymbol":"DIABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DIA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"DIABTC":{"symbol":"DIABTC","localSymbol":"DIABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DIA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DIABUSD":{"symbol":"DIABUSD","localSymbol":"DIABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DIA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"DIAUSDT":{"symbol":"DIAUSDT","localSymbol":"DIAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DIA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"DLTBNB":{"symbol":"DLTBNB","localSymbol":"DLTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DLT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"DLTBTC":{"symbol":"DLTBTC","localSymbol":"DLTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DLT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DLTETH":{"symbol":"DLTETH","localSymbol":"DLTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DLT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DNTBTC":{"symbol":"DNTBTC","localSymbol":"DNTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DNT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DNTBUSD":{"symbol":"DNTBUSD","localSymbol":"DNTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DNT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DNTETH":{"symbol":"DNTETH","localSymbol":"DNTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DNT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DNTUSDT":{"symbol":"DNTUSDT","localSymbol":"DNTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DNT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DOCKBTC":{"symbol":"DOCKBTC","localSymbol":"DOCKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DOCK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DOCKBUSD":{"symbol":"DOCKBUSD","localSymbol":"DOCKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DOCK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"DOCKETH":{"symbol":"DOCKETH","localSymbol":"DOCKETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DOCK","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DOCKUSDT":{"symbol":"DOCKUSDT","localSymbol":"DOCKUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DOCK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DODOBTC":{"symbol":"DODOBTC","localSymbol":"DODOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DODO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DODOBUSD":{"symbol":"DODOBUSD","localSymbol":"DODOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DODO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"DODOUSDT":{"symbol":"DODOUSDT","localSymbol":"DODOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DODO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"DOGEAUD":{"symbol":"DOGEAUD","localSymbol":"DOGEAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"DOGE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DOGEBIDR":{"symbol":"DOGEBIDR","localSymbol":"DOGEBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"DOGE","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":1.00000000,"maxPrice":100000.00000000,"tickSize":1.00000000},"DOGEBNB":{"symbol":"DOGEBNB","localSymbol":"DOGEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DOGE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DOGEBRL":{"symbol":"DOGEBRL","localSymbol":"DOGEBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"DOGE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DOGEBTC":{"symbol":"DOGEBTC","localSymbol":"DOGEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DOGE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DOGEBUSD":{"symbol":"DOGEBUSD","localSymbol":"DOGEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DOGE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DOGEEUR":{"symbol":"DOGEEUR","localSymbol":"DOGEEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"DOGE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DOGEGBP":{"symbol":"DOGEGBP","localSymbol":"DOGEGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"DOGE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DOGEPAX":{"symbol":"DOGEPAX","localSymbol":"DOGEPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"DOGE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"DOGERUB":{"symbol":"DOGERUB","localSymbol":"DOGERUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"DOGE","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":100000.00000000,"tickSize":0.00100000},"DOGETRY":{"symbol":"DOGETRY","localSymbol":"DOGETRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"DOGE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"DOGEUSDC":{"symbol":"DOGEUSDC","localSymbol":"DOGEUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"DOGE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"DOGEUSDT":{"symbol":"DOGEUSDT","localSymbol":"DOGEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DOGE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DOTAUD":{"symbol":"DOTAUD","localSymbol":"DOTAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"DOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"DOTBIDR":{"symbol":"DOTBIDR","localSymbol":"DOTBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"DOT","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.01000000,"maxQuantity":100000.00000000,"stepSize":0.01000000,"minPrice":1.00000000,"maxPrice":50000000.00000000,"tickSize":1.00000000},"DOTBKRW":{"symbol":"DOTBKRW","localSymbol":"DOTBKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"DOT","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.10000000,"maxQuantity":9200.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"DOTBNB":{"symbol":"DOTBNB","localSymbol":"DOTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DOT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DOTBRL":{"symbol":"DOTBRL","localSymbol":"DOTBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"DOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"DOTBTC":{"symbol":"DOTBTC","localSymbol":"DOTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DOT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"DOTBUSD":{"symbol":"DOTBUSD","localSymbol":"DOTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"DOTDOWNUSDT":{"symbol":"DOTDOWNUSDT","localSymbol":"DOTDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DOTDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":100000000.00000000,"stepSize":0.01000000,"minPrice":1.66800000,"maxPrice":31.67400000,"tickSize":0.00100000},"DOTETH":{"symbol":"DOTETH","localSymbol":"DOTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DOT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"DOTEUR":{"symbol":"DOTEUR","localSymbol":"DOTEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"DOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"DOTGBP":{"symbol":"DOTGBP","localSymbol":"DOTGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"DOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"DOTNGN":{"symbol":"DOTNGN","localSymbol":"DOTNGN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"NGN","baseCurrency":"DOT","minNotional":500.00000000,"minAmount":500.00000000,"minQuantity":0.00001000,"maxQuantity":92232.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":1000007.00000000,"tickSize":1.00000000},"DOTRUB":{"symbol":"DOTRUB","localSymbol":"DOTRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"DOT","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.01000000,"maxQuantity":92233.00000000,"stepSize":0.01000000,"minPrice":1.00000000,"maxPrice":999996.00000000,"tickSize":1.00000000},"DOTTRY":{"symbol":"DOTTRY","localSymbol":"DOTTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"DOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"DOTUPUSDT":{"symbol":"DOTUPUSDT","localSymbol":"DOTUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DOTUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.00970000,"maxPrice":0.18320000,"tickSize":0.00010000},"DOTUSDT":{"symbol":"DOTUSDT","localSymbol":"DOTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"DREPBNB":{"symbol":"DREPBNB","localSymbol":"DREPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DREP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"DREPBTC":{"symbol":"DREPBTC","localSymbol":"DREPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DREP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DREPBUSD":{"symbol":"DREPBUSD","localSymbol":"DREPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DREP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DREPUSDT":{"symbol":"DREPUSDT","localSymbol":"DREPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DREP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DUSKBNB":{"symbol":"DUSKBNB","localSymbol":"DUSKBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DUSK","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"DUSKBTC":{"symbol":"DUSKBTC","localSymbol":"DUSKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DUSK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"DUSKBUSD":{"symbol":"DUSKBUSD","localSymbol":"DUSKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DUSK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DUSKPAX":{"symbol":"DUSKPAX","localSymbol":"DUSKPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"DUSK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DUSKUSDC":{"symbol":"DUSKUSDC","localSymbol":"DUSKUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"DUSK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DUSKUSDT":{"symbol":"DUSKUSDT","localSymbol":"DUSKUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DUSK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"DYDXBNB":{"symbol":"DYDXBNB","localSymbol":"DYDXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"DYDX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":8384883677.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":10.00000000,"tickSize":0.00000100},"DYDXBTC":{"symbol":"DYDXBTC","localSymbol":"DYDXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"DYDX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":46116860414.00000763,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"DYDXBUSD":{"symbol":"DYDXBUSD","localSymbol":"DYDXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"DYDX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"DYDXETH":{"symbol":"DYDXETH","localSymbol":"DYDXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"DYDX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"DYDXUSDT":{"symbol":"DYDXUSDT","localSymbol":"DYDXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"DYDX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"EASYBTC":{"symbol":"EASYBTC","localSymbol":"EASYBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"EASY","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"EASYETH":{"symbol":"EASYETH","localSymbol":"EASYETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"EASY","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"EDOBTC":{"symbol":"EDOBTC","localSymbol":"EDOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"EDO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"EDOETH":{"symbol":"EDOETH","localSymbol":"EDOETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"EDO","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"EGLDBNB":{"symbol":"EGLDBNB","localSymbol":"EGLDBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"EGLD","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"EGLDBTC":{"symbol":"EGLDBTC","localSymbol":"EGLDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"EGLD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":10000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"EGLDBUSD":{"symbol":"EGLDBUSD","localSymbol":"EGLDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"EGLD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"EGLDETH":{"symbol":"EGLDETH","localSymbol":"EGLDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"EGLD","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00010000,"maxQuantity":92141578.00000000,"stepSize":0.00010000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"EGLDEUR":{"symbol":"EGLDEUR","localSymbol":"EGLDEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"EGLD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"EGLDUSDT":{"symbol":"EGLDUSDT","localSymbol":"EGLDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"EGLD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ELFBTC":{"symbol":"ELFBTC","localSymbol":"ELFBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ELF","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ELFBUSD":{"symbol":"ELFBUSD","localSymbol":"ELFBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ELF","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":913205152.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":100.00000000,"tickSize":0.00010000},"ELFETH":{"symbol":"ELFETH","localSymbol":"ELFETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ELF","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ELFUSDT":{"symbol":"ELFUSDT","localSymbol":"ELFUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ELF","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ENGBTC":{"symbol":"ENGBTC","localSymbol":"ENGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ENG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ENGETH":{"symbol":"ENGETH","localSymbol":"ENGETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ENG","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ENJBNB":{"symbol":"ENJBNB","localSymbol":"ENJBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ENJ","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ENJBRL":{"symbol":"ENJBRL","localSymbol":"ENJBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"ENJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ENJBTC":{"symbol":"ENJBTC","localSymbol":"ENJBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ENJ","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ENJBUSD":{"symbol":"ENJBUSD","localSymbol":"ENJBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ENJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ENJETH":{"symbol":"ENJETH","localSymbol":"ENJETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ENJ","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ENJEUR":{"symbol":"ENJEUR","localSymbol":"ENJEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"ENJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"ENJGBP":{"symbol":"ENJGBP","localSymbol":"ENJGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"ENJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ENJTRY":{"symbol":"ENJTRY","localSymbol":"ENJTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ENJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ENJUSDT":{"symbol":"ENJUSDT","localSymbol":"ENJUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ENJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ENSBNB":{"symbol":"ENSBNB","localSymbol":"ENSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ENS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ENSBTC":{"symbol":"ENSBTC","localSymbol":"ENSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ENS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ENSBUSD":{"symbol":"ENSBUSD","localSymbol":"ENSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ENS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ENSTRY":{"symbol":"ENSTRY","localSymbol":"ENSTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ENS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"ENSUSDT":{"symbol":"ENSUSDT","localSymbol":"ENSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ENS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"EOSAUD":{"symbol":"EOSAUD","localSymbol":"EOSAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"EOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"EOSBEARBUSD":{"symbol":"EOSBEARBUSD","localSymbol":"EOSBEARBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"EOSBEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"EOSBEARUSDT":{"symbol":"EOSBEARUSDT","localSymbol":"EOSBEARUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"EOSBEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"EOSBNB":{"symbol":"EOSBNB","localSymbol":"EOSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"EOS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"EOSBTC":{"symbol":"EOSBTC","localSymbol":"EOSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"EOS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"EOSBULLBUSD":{"symbol":"EOSBULLBUSD","localSymbol":"EOSBULLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"EOSBULL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"EOSBULLUSDT":{"symbol":"EOSBULLUSDT","localSymbol":"EOSBULLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"EOSBULL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":92232.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000007.00000000,"tickSize":0.01000000},"EOSBUSD":{"symbol":"EOSBUSD","localSymbol":"EOSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"EOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"EOSDOWNUSDT":{"symbol":"EOSDOWNUSDT","localSymbol":"EOSDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"EOSDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":69980060.00000000,"stepSize":0.01000000,"minPrice":0.00003760,"maxPrice":0.00071410,"tickSize":0.00000010},"EOSETH":{"symbol":"EOSETH","localSymbol":"EOSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"EOS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"EOSEUR":{"symbol":"EOSEUR","localSymbol":"EOSEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"EOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"EOSPAX":{"symbol":"EOSPAX","localSymbol":"EOSPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"EOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"EOSTRY":{"symbol":"EOSTRY","localSymbol":"EOSTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"EOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"EOSTUSD":{"symbol":"EOSTUSD","localSymbol":"EOSTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"EOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"EOSUPUSDT":{"symbol":"EOSUPUSDT","localSymbol":"EOSUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"EOSUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.02120000,"maxPrice":0.40210000,"tickSize":0.00010000},"EOSUSDC":{"symbol":"EOSUSDC","localSymbol":"EOSUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"EOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"EOSUSDT":{"symbol":"EOSUSDT","localSymbol":"EOSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"EOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"EPSBTC":{"symbol":"EPSBTC","localSymbol":"EPSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"EPS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"EPSBUSD":{"symbol":"EPSBUSD","localSymbol":"EPSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"EPS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"EPSUSDT":{"symbol":"EPSUSDT","localSymbol":"EPSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"EPS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"EPXBUSD":{"symbol":"EPXBUSD","localSymbol":"EPXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"EPX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"EPXUSDT":{"symbol":"EPXUSDT","localSymbol":"EPXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"EPX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"ERDBNB":{"symbol":"ERDBNB","localSymbol":"ERDBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ERD","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ERDBTC":{"symbol":"ERDBTC","localSymbol":"ERDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ERD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ERDBUSD":{"symbol":"ERDBUSD","localSymbol":"ERDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ERD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ERDPAX":{"symbol":"ERDPAX","localSymbol":"ERDPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"ERD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ERDUSDC":{"symbol":"ERDUSDC","localSymbol":"ERDUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"ERD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ERDUSDT":{"symbol":"ERDUSDT","localSymbol":"ERDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ERD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ERNBNB":{"symbol":"ERNBNB","localSymbol":"ERNBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ERN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ERNBUSD":{"symbol":"ERNBUSD","localSymbol":"ERNBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ERN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ERNUSDT":{"symbol":"ERNUSDT","localSymbol":"ERNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ERN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ETCBNB":{"symbol":"ETCBNB","localSymbol":"ETCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ETC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ETCBRL":{"symbol":"ETCBRL","localSymbol":"ETCBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"ETC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":10000.00000000,"tickSize":0.10000000},"ETCBTC":{"symbol":"ETCBTC","localSymbol":"ETCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ETC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ETCBUSD":{"symbol":"ETCBUSD","localSymbol":"ETCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ETC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ETCETH":{"symbol":"ETCETH","localSymbol":"ETCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ETC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ETCEUR":{"symbol":"ETCEUR","localSymbol":"ETCEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"ETC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ETCGBP":{"symbol":"ETCGBP","localSymbol":"ETCGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"ETC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ETCPAX":{"symbol":"ETCPAX","localSymbol":"ETCPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"ETC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ETCTRY":{"symbol":"ETCTRY","localSymbol":"ETCTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ETC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"ETCTUSD":{"symbol":"ETCTUSD","localSymbol":"ETCTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"ETC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ETCUSDC":{"symbol":"ETCUSDC","localSymbol":"ETCUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"ETC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ETCUSDT":{"symbol":"ETCUSDT","localSymbol":"ETCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ETC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ETHAUD":{"symbol":"ETHAUD","localSymbol":"ETHAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ETHBEARBUSD":{"symbol":"ETHBEARBUSD","localSymbol":"ETHBEARBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ETHBEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ETHBEARUSDT":{"symbol":"ETHBEARUSDT","localSymbol":"ETHBEARUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ETHBEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ETHBIDR":{"symbol":"ETHBIDR","localSymbol":"ETHBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"ETH","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.00010000,"maxQuantity":100000.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":900000000.00000000,"tickSize":1.00000000},"ETHBKRW":{"symbol":"ETHBKRW","localSymbol":"ETHBKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"ETH","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.00001000,"maxQuantity":8000.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":10000000.00000000,"tickSize":1.00000000},"ETHBRL":{"symbol":"ETHBRL","localSymbol":"ETHBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":45000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":200000.00000000,"tickSize":0.01000000},"ETHBTC":{"symbol":"ETHBTC","localSymbol":"ETHBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ETH","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00010000,"maxQuantity":100000.00000000,"stepSize":0.00010000,"minPrice":0.00000100,"maxPrice":922327.00000000,"tickSize":0.00000100},"ETHBULLBUSD":{"symbol":"ETHBULLBUSD","localSymbol":"ETHBULLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ETHBULL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00000100,"maxQuantity":9000.00000000,"stepSize":0.00000100,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"ETHBULLUSDT":{"symbol":"ETHBULLUSDT","localSymbol":"ETHBULLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ETHBULL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00000100,"maxQuantity":9000.00000000,"stepSize":0.00000100,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"ETHBUSD":{"symbol":"ETHBUSD","localSymbol":"ETHBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ETHDAI":{"symbol":"ETHDAI","localSymbol":"ETHDAI","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"DAI","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ETHDOWNUSDT":{"symbol":"ETHDOWNUSDT","localSymbol":"ETHDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ETHDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":100000000.00000000,"stepSize":0.01000000,"minPrice":0.09960000,"maxPrice":1.89130000,"tickSize":0.00010000},"ETHEUR":{"symbol":"ETHEUR","localSymbol":"ETHEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ETHGBP":{"symbol":"ETHGBP","localSymbol":"ETHGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ETHNGN":{"symbol":"ETHNGN","localSymbol":"ETHNGN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"NGN","baseCurrency":"ETH","minNotional":500.00000000,"minAmount":500.00000000,"minQuantity":0.00001000,"maxQuantity":900.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":100000000.00000000,"tickSize":1.00000000},"ETHPAX":{"symbol":"ETHPAX","localSymbol":"ETHPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ETHPLN":{"symbol":"ETHPLN","localSymbol":"ETHPLN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PLN","baseCurrency":"ETH","minNotional":50.00000000,"minAmount":50.00000000,"minQuantity":0.00010000,"maxQuantity":92233.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":999996.00000000,"tickSize":1.00000000},"ETHRUB":{"symbol":"ETHRUB","localSymbol":"ETHRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"ETH","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.00010000,"maxQuantity":23055.00000000,"stepSize":0.00010000,"minPrice":0.10000000,"maxPrice":4000000.00000000,"tickSize":0.10000000},"ETHTRY":{"symbol":"ETHTRY","localSymbol":"ETHTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":153719.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":600000.00000000,"tickSize":1.00000000},"ETHTUSD":{"symbol":"ETHTUSD","localSymbol":"ETHTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ETHUAH":{"symbol":"ETHUAH","localSymbol":"ETHUAH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"UAH","baseCurrency":"ETH","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.00010000,"maxQuantity":9220.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":9999319.00000000,"tickSize":1.00000000},"ETHUPUSDT":{"symbol":"ETHUPUSDT","localSymbol":"ETHUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ETHUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":3000.00000000,"stepSize":0.01000000,"minPrice":0.29700000,"maxPrice":5.63000000,"tickSize":0.00100000},"ETHUSDC":{"symbol":"ETHUSDC","localSymbol":"ETHUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ETHUSDP":{"symbol":"ETHUSDP","localSymbol":"ETHUSDP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDP","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":92233.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":999996.00000000,"tickSize":0.01000000},"ETHUSDT":{"symbol":"ETHUSDT","localSymbol":"ETHUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":9000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"ETHUST":{"symbol":"ETHUST","localSymbol":"ETHUST","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"UST","baseCurrency":"ETH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":92233.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":999996.00000000,"tickSize":0.01000000},"ETHZAR":{"symbol":"ETHZAR","localSymbol":"ETHZAR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ZAR","baseCurrency":"ETH","minNotional":200.00000000,"minAmount":200.00000000,"minQuantity":0.00010000,"maxQuantity":9223.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":9999319.00000000,"tickSize":1.00000000},"EURBUSD":{"symbol":"EURBUSD","localSymbol":"EURBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"EUR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"EURUSDT":{"symbol":"EURUSDT","localSymbol":"EURUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"EUR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":6000000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"EVXBTC":{"symbol":"EVXBTC","localSymbol":"EVXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"EVX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"EVXETH":{"symbol":"EVXETH","localSymbol":"EVXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"EVX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"FARMBNB":{"symbol":"FARMBNB","localSymbol":"FARMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FARM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FARMBTC":{"symbol":"FARMBTC","localSymbol":"FARMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FARM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"FARMBUSD":{"symbol":"FARMBUSD","localSymbol":"FARMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FARM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"FARMETH":{"symbol":"FARMETH","localSymbol":"FARMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"FARM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00010000,"maxQuantity":92141578.00000000,"stepSize":0.00010000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"FARMUSDT":{"symbol":"FARMUSDT","localSymbol":"FARMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FARM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"FETBNB":{"symbol":"FETBNB","localSymbol":"FETBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FET","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"FETBTC":{"symbol":"FETBTC","localSymbol":"FETBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FET","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FETBUSD":{"symbol":"FETBUSD","localSymbol":"FETBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FET","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FETUSDT":{"symbol":"FETUSDT","localSymbol":"FETUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FET","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FIDABNB":{"symbol":"FIDABNB","localSymbol":"FIDABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FIDA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"FIDABTC":{"symbol":"FIDABTC","localSymbol":"FIDABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FIDA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FIDABUSD":{"symbol":"FIDABUSD","localSymbol":"FIDABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FIDA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FIDAUSDT":{"symbol":"FIDAUSDT","localSymbol":"FIDAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FIDA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FILBNB":{"symbol":"FILBNB","localSymbol":"FILBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FIL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FILBTC":{"symbol":"FILBTC","localSymbol":"FILBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FIL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"FILBUSD":{"symbol":"FILBUSD","localSymbol":"FILBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FIL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"FILDOWNUSDT":{"symbol":"FILDOWNUSDT","localSymbol":"FILDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FILDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":19998638.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":4611.00000000,"tickSize":0.00000010},"FILETH":{"symbol":"FILETH","localSymbol":"FILETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"FIL","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":913205152.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"FILTRY":{"symbol":"FILTRY","localSymbol":"FILTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"FIL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"FILUPUSDT":{"symbol":"FILUPUSDT","localSymbol":"FILUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FILUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":100000.00000000,"tickSize":0.00010000},"FILUSDT":{"symbol":"FILUSDT","localSymbol":"FILUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FIL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"FIOBNB":{"symbol":"FIOBNB","localSymbol":"FIOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FIO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"FIOBTC":{"symbol":"FIOBTC","localSymbol":"FIOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FIO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FIOBUSD":{"symbol":"FIOBUSD","localSymbol":"FIOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FIO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FIOUSDT":{"symbol":"FIOUSDT","localSymbol":"FIOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FIO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FIROBTC":{"symbol":"FIROBTC","localSymbol":"FIROBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FIRO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"FIROBUSD":{"symbol":"FIROBUSD","localSymbol":"FIROBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FIRO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"FIROETH":{"symbol":"FIROETH","localSymbol":"FIROETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"FIRO","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"FIROUSDT":{"symbol":"FIROUSDT","localSymbol":"FIROUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FIRO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"FISBIDR":{"symbol":"FISBIDR","localSymbol":"FISBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"FIS","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.10000000,"maxQuantity":9223371114.00000000,"stepSize":0.10000000,"minPrice":1.00000000,"maxPrice":10000000.00000000,"tickSize":1.00000000},"FISBRL":{"symbol":"FISBRL","localSymbol":"FISBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"FIS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"FISBTC":{"symbol":"FISBTC","localSymbol":"FISBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FIS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FISBUSD":{"symbol":"FISBUSD","localSymbol":"FISBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FIS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"FISTRY":{"symbol":"FISTRY","localSymbol":"FISTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"FIS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"FISUSDT":{"symbol":"FISUSDT","localSymbol":"FISUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FIS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"FLMBNB":{"symbol":"FLMBNB","localSymbol":"FLMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FLM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"FLMBTC":{"symbol":"FLMBTC","localSymbol":"FLMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FLM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FLMBUSD":{"symbol":"FLMBUSD","localSymbol":"FLMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FLMUSDT":{"symbol":"FLMUSDT","localSymbol":"FLMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FLOWBNB":{"symbol":"FLOWBNB","localSymbol":"FLOWBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FLOW","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"FLOWBTC":{"symbol":"FLOWBTC","localSymbol":"FLOWBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FLOW","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FLOWBUSD":{"symbol":"FLOWBUSD","localSymbol":"FLOWBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FLOW","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":100000.00000000,"tickSize":0.00100000},"FLOWUSDT":{"symbol":"FLOWUSDT","localSymbol":"FLOWUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FLOW","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":100000.00000000,"tickSize":0.00100000},"FLUXBTC":{"symbol":"FLUXBTC","localSymbol":"FLUXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FLUX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FLUXBUSD":{"symbol":"FLUXBUSD","localSymbol":"FLUXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FLUX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"FLUXUSDT":{"symbol":"FLUXUSDT","localSymbol":"FLUXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FLUX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"FORBNB":{"symbol":"FORBNB","localSymbol":"FORBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FOR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"FORBTC":{"symbol":"FORBTC","localSymbol":"FORBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FOR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FORBUSD":{"symbol":"FORBUSD","localSymbol":"FORBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FOR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"FORTHBTC":{"symbol":"FORTHBTC","localSymbol":"FORTHBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FORTH","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"FORTHBUSD":{"symbol":"FORTHBUSD","localSymbol":"FORTHBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FORTH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"FORTHUSDT":{"symbol":"FORTHUSDT","localSymbol":"FORTHUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FORTH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"FORUSDT":{"symbol":"FORUSDT","localSymbol":"FORUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FOR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"FRONTBTC":{"symbol":"FRONTBTC","localSymbol":"FRONTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FRONT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FRONTBUSD":{"symbol":"FRONTBUSD","localSymbol":"FRONTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FRONT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FRONTETH":{"symbol":"FRONTETH","localSymbol":"FRONTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"FRONT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"FRONTUSDT":{"symbol":"FRONTUSDT","localSymbol":"FRONTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FRONT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FTMAUD":{"symbol":"FTMAUD","localSymbol":"FTMAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"FTM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FTMBIDR":{"symbol":"FTMBIDR","localSymbol":"FTMBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"FTM","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.10000000,"maxQuantity":9223371114.00000000,"stepSize":0.10000000,"minPrice":1.00000000,"maxPrice":10000000.00000000,"tickSize":1.00000000},"FTMBNB":{"symbol":"FTMBNB","localSymbol":"FTMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FTM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"FTMBRL":{"symbol":"FTMBRL","localSymbol":"FTMBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"FTM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"FTMBTC":{"symbol":"FTMBTC","localSymbol":"FTMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FTM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FTMBUSD":{"symbol":"FTMBUSD","localSymbol":"FTMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FTM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"FTMETH":{"symbol":"FTMETH","localSymbol":"FTMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"FTM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"FTMEUR":{"symbol":"FTMEUR","localSymbol":"FTMEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"FTM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"FTMPAX":{"symbol":"FTMPAX","localSymbol":"FTMPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"FTM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"FTMRUB":{"symbol":"FTMRUB","localSymbol":"FTMRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"FTM","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"FTMTRY":{"symbol":"FTMTRY","localSymbol":"FTMTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"FTM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"FTMTUSD":{"symbol":"FTMTUSD","localSymbol":"FTMTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"FTM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"FTMUSDC":{"symbol":"FTMUSDC","localSymbol":"FTMUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"FTM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"FTMUSDT":{"symbol":"FTMUSDT","localSymbol":"FTMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FTM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"FTTBNB":{"symbol":"FTTBNB","localSymbol":"FTTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FTT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"FTTBTC":{"symbol":"FTTBTC","localSymbol":"FTTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FTT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"FTTBUSD":{"symbol":"FTTBUSD","localSymbol":"FTTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FTT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":100000.00000000,"tickSize":0.00010000},"FTTETH":{"symbol":"FTTETH","localSymbol":"FTTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"FTT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"FTTUSDT":{"symbol":"FTTUSDT","localSymbol":"FTTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FTT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"FUELBTC":{"symbol":"FUELBTC","localSymbol":"FUELBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FUEL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FUELETH":{"symbol":"FUELETH","localSymbol":"FUELETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"FUEL","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FUNBNB":{"symbol":"FUNBNB","localSymbol":"FUNBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"FUN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FUNBTC":{"symbol":"FUNBTC","localSymbol":"FUNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FUN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FUNETH":{"symbol":"FUNETH","localSymbol":"FUNETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"FUN","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FUNUSDT":{"symbol":"FUNUSDT","localSymbol":"FUNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FUN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"FXSBTC":{"symbol":"FXSBTC","localSymbol":"FXSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"FXS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"FXSBUSD":{"symbol":"FXSBUSD","localSymbol":"FXSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"FXS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"FXSUSDT":{"symbol":"FXSUSDT","localSymbol":"FXSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"FXS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"GALAAUD":{"symbol":"GALAAUD","localSymbol":"GALAAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"GALA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"GALABNB":{"symbol":"GALABNB","localSymbol":"GALABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"GALA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":100.00000000,"tickSize":0.00000001},"GALABRL":{"symbol":"GALABRL","localSymbol":"GALABRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"GALA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GALABTC":{"symbol":"GALABTC","localSymbol":"GALABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GALA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":100.00000000,"tickSize":0.00000001},"GALABUSD":{"symbol":"GALABUSD","localSymbol":"GALABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GALA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"GALAETH":{"symbol":"GALAETH","localSymbol":"GALAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GALA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GALAEUR":{"symbol":"GALAEUR","localSymbol":"GALAEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"GALA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"GALATRY":{"symbol":"GALATRY","localSymbol":"GALATRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"GALA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GALAUSDT":{"symbol":"GALAUSDT","localSymbol":"GALAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GALA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"GALBNB":{"symbol":"GALBNB","localSymbol":"GALBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"GAL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"GALBRL":{"symbol":"GALBRL","localSymbol":"GALBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"GAL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"GALBTC":{"symbol":"GALBTC","localSymbol":"GALBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GAL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GALBUSD":{"symbol":"GALBUSD","localSymbol":"GALBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GAL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GALETH":{"symbol":"GALETH","localSymbol":"GALETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GAL","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"GALEUR":{"symbol":"GALEUR","localSymbol":"GALEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"GAL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GALTRY":{"symbol":"GALTRY","localSymbol":"GALTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"GAL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"GALUSDT":{"symbol":"GALUSDT","localSymbol":"GALUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GAL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GASBTC":{"symbol":"GASBTC","localSymbol":"GASBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GAS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"GASBUSD":{"symbol":"GASBUSD","localSymbol":"GASBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GAS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GBPBUSD":{"symbol":"GBPBUSD","localSymbol":"GBPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GBP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GBPUSDT":{"symbol":"GBPUSDT","localSymbol":"GBPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GBP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GHSTBUSD":{"symbol":"GHSTBUSD","localSymbol":"GHSTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GHST","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GHSTETH":{"symbol":"GHSTETH","localSymbol":"GHSTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GHST","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"GHSTUSDT":{"symbol":"GHSTUSDT","localSymbol":"GHSTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GHST","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GLMBTC":{"symbol":"GLMBTC","localSymbol":"GLMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GLM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GLMBUSD":{"symbol":"GLMBUSD","localSymbol":"GLMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GLMETH":{"symbol":"GLMETH","localSymbol":"GLMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GLM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"GLMRBNB":{"symbol":"GLMRBNB","localSymbol":"GLMRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"GLMR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"GLMRBTC":{"symbol":"GLMRBTC","localSymbol":"GLMRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GLMR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GLMRBUSD":{"symbol":"GLMRBUSD","localSymbol":"GLMRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GLMR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GLMRUSDT":{"symbol":"GLMRUSDT","localSymbol":"GLMRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GLMR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GMTAUD":{"symbol":"GMTAUD","localSymbol":"GMTAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"GMT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GMTBNB":{"symbol":"GMTBNB","localSymbol":"GMTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"GMT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"GMTBRL":{"symbol":"GMTBRL","localSymbol":"GMTBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"GMT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GMTBTC":{"symbol":"GMTBTC","localSymbol":"GMTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GMT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GMTBUSD":{"symbol":"GMTBUSD","localSymbol":"GMTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GMT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GMTETH":{"symbol":"GMTETH","localSymbol":"GMTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GMT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"GMTEUR":{"symbol":"GMTEUR","localSymbol":"GMTEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"GMT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GMTGBP":{"symbol":"GMTGBP","localSymbol":"GMTGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"GMT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GMTTRY":{"symbol":"GMTTRY","localSymbol":"GMTTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"GMT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"GMTUSDT":{"symbol":"GMTUSDT","localSymbol":"GMTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GMT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GMXBTC":{"symbol":"GMXBTC","localSymbol":"GMXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GMX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":913205152.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"GMXBUSD":{"symbol":"GMXBUSD","localSymbol":"GMXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GMX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"GMXUSDT":{"symbol":"GMXUSDT","localSymbol":"GMXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GMX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"GNOBNB":{"symbol":"GNOBNB","localSymbol":"GNOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"GNO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GNOBTC":{"symbol":"GNOBTC","localSymbol":"GNOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GNO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"GNOBUSD":{"symbol":"GNOBUSD","localSymbol":"GNOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GNO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"GNOUSDT":{"symbol":"GNOUSDT","localSymbol":"GNOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GNO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"GNTBNB":{"symbol":"GNTBNB","localSymbol":"GNTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"GNT","minNotional":0.10000000,"minAmount":0.10000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"GNTBTC":{"symbol":"GNTBTC","localSymbol":"GNTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GNT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GNTETH":{"symbol":"GNTETH","localSymbol":"GNTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GNT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GOBNB":{"symbol":"GOBNB","localSymbol":"GOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"GO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"GOBTC":{"symbol":"GOBTC","localSymbol":"GOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GRSBTC":{"symbol":"GRSBTC","localSymbol":"GRSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GRS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GRSETH":{"symbol":"GRSETH","localSymbol":"GRSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GRS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GRTBTC":{"symbol":"GRTBTC","localSymbol":"GRTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GRT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GRTBUSD":{"symbol":"GRTBUSD","localSymbol":"GRTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GRT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GRTETH":{"symbol":"GRTETH","localSymbol":"GRTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GRT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"GRTEUR":{"symbol":"GRTEUR","localSymbol":"GRTEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"GRT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GRTTRY":{"symbol":"GRTTRY","localSymbol":"GRTTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"GRT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"GRTUSDT":{"symbol":"GRTUSDT","localSymbol":"GRTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GRT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"GTCBNB":{"symbol":"GTCBNB","localSymbol":"GTCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"GTC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"GTCBTC":{"symbol":"GTCBTC","localSymbol":"GTCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GTC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"GTCBUSD":{"symbol":"GTCBUSD","localSymbol":"GTCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"GTCUSDT":{"symbol":"GTCUSDT","localSymbol":"GTCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"GTOBNB":{"symbol":"GTOBNB","localSymbol":"GTOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"GTO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"GTOBTC":{"symbol":"GTOBTC","localSymbol":"GTOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GTO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GTOBUSD":{"symbol":"GTOBUSD","localSymbol":"GTOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"GTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"GTOETH":{"symbol":"GTOETH","localSymbol":"GTOETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GTO","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GTOPAX":{"symbol":"GTOPAX","localSymbol":"GTOPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"GTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"GTOTUSD":{"symbol":"GTOTUSD","localSymbol":"GTOTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"GTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"GTOUSDC":{"symbol":"GTOUSDC","localSymbol":"GTOUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"GTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"GTOUSDT":{"symbol":"GTOUSDT","localSymbol":"GTOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"GVTBTC":{"symbol":"GVTBTC","localSymbol":"GVTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GVT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GVTETH":{"symbol":"GVTETH","localSymbol":"GVTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GVT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"GXSBNB":{"symbol":"GXSBNB","localSymbol":"GXSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"GXS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"GXSBTC":{"symbol":"GXSBTC","localSymbol":"GXSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"GXS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"GXSETH":{"symbol":"GXSETH","localSymbol":"GXSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"GXS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"GXSUSDT":{"symbol":"GXSUSDT","localSymbol":"GXSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"GXS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HARDBNB":{"symbol":"HARDBNB","localSymbol":"HARDBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"HARD","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"HARDBTC":{"symbol":"HARDBTC","localSymbol":"HARDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"HARD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"HARDBUSD":{"symbol":"HARDBUSD","localSymbol":"HARDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"HARD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HARDUSDT":{"symbol":"HARDUSDT","localSymbol":"HARDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"HARD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HBARBNB":{"symbol":"HBARBNB","localSymbol":"HBARBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"HBAR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"HBARBTC":{"symbol":"HBARBTC","localSymbol":"HBARBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"HBAR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"HBARBUSD":{"symbol":"HBARBUSD","localSymbol":"HBARBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"HBAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HBARUSDT":{"symbol":"HBARUSDT","localSymbol":"HBARUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"HBAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HCBTC":{"symbol":"HCBTC","localSymbol":"HCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"HC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"HCETH":{"symbol":"HCETH","localSymbol":"HCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"HC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"HCUSDT":{"symbol":"HCUSDT","localSymbol":"HCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"HC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HEGICBUSD":{"symbol":"HEGICBUSD","localSymbol":"HEGICBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"HEGIC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HEGICETH":{"symbol":"HEGICETH","localSymbol":"HEGICETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"HEGIC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"HFTBTC":{"symbol":"HFTBTC","localSymbol":"HFTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"HFT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":46116860414.00000763,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"HFTBUSD":{"symbol":"HFTBUSD","localSymbol":"HFTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"HFT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HFTUSDT":{"symbol":"HFTUSDT","localSymbol":"HFTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"HFT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HIGHBNB":{"symbol":"HIGHBNB","localSymbol":"HIGHBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"HIGH","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"HIGHBTC":{"symbol":"HIGHBTC","localSymbol":"HIGHBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"HIGH","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"HIGHBUSD":{"symbol":"HIGHBUSD","localSymbol":"HIGHBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"HIGH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"HIGHUSDT":{"symbol":"HIGHUSDT","localSymbol":"HIGHUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"HIGH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"HIVEBNB":{"symbol":"HIVEBNB","localSymbol":"HIVEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"HIVE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"HIVEBTC":{"symbol":"HIVEBTC","localSymbol":"HIVEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"HIVE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"HIVEBUSD":{"symbol":"HIVEBUSD","localSymbol":"HIVEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"HIVE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HIVEUSDT":{"symbol":"HIVEUSDT","localSymbol":"HIVEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"HIVE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HNTBTC":{"symbol":"HNTBTC","localSymbol":"HNTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"HNT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"HNTBUSD":{"symbol":"HNTBUSD","localSymbol":"HNTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"HNT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"HNTUSDT":{"symbol":"HNTUSDT","localSymbol":"HNTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"HNT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"HOOKBNB":{"symbol":"HOOKBNB","localSymbol":"HOOKBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"HOOK","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":913205152.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":100.00000000,"tickSize":0.00000010},"HOOKBTC":{"symbol":"HOOKBTC","localSymbol":"HOOKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"HOOK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":46116860414.00000763,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"HOOKBUSD":{"symbol":"HOOKBUSD","localSymbol":"HOOKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"HOOK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HOOKUSDT":{"symbol":"HOOKUSDT","localSymbol":"HOOKUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"HOOK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HOTBNB":{"symbol":"HOTBNB","localSymbol":"HOTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"HOT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"HOTBRL":{"symbol":"HOTBRL","localSymbol":"HOTBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"HOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"HOTBTC":{"symbol":"HOTBTC","localSymbol":"HOTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"HOT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"HOTBUSD":{"symbol":"HOTBUSD","localSymbol":"HOTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"HOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"HOTETH":{"symbol":"HOTETH","localSymbol":"HOTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"HOT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"HOTEUR":{"symbol":"HOTEUR","localSymbol":"HOTEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"HOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"HOTTRY":{"symbol":"HOTTRY","localSymbol":"HOTTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"HOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"HOTUSDT":{"symbol":"HOTUSDT","localSymbol":"HOTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"HOT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"HSRBTC":{"symbol":"HSRBTC","localSymbol":"HSRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"HSR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"HSRETH":{"symbol":"HSRETH","localSymbol":"HSRETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"HSR","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ICNBTC":{"symbol":"ICNBTC","localSymbol":"ICNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ICN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ICNETH":{"symbol":"ICNETH","localSymbol":"ICNETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ICN","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ICPBNB":{"symbol":"ICPBNB","localSymbol":"ICPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ICP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ICPBTC":{"symbol":"ICPBTC","localSymbol":"ICPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ICP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ICPBUSD":{"symbol":"ICPBUSD","localSymbol":"ICPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ICP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":100000.00000000,"tickSize":0.00100000},"ICPETH":{"symbol":"ICPETH","localSymbol":"ICPETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ICP","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ICPEUR":{"symbol":"ICPEUR","localSymbol":"ICPEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"ICP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ICPRUB":{"symbol":"ICPRUB","localSymbol":"ICPRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"ICP","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.01000000,"maxQuantity":92233.00000000,"stepSize":0.01000000,"minPrice":1.00000000,"maxPrice":999996.00000000,"tickSize":1.00000000},"ICPTRY":{"symbol":"ICPTRY","localSymbol":"ICPTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ICP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"ICPUSDT":{"symbol":"ICPUSDT","localSymbol":"ICPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ICP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":100000.00000000,"tickSize":0.00100000},"ICXBNB":{"symbol":"ICXBNB","localSymbol":"ICXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ICX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ICXBTC":{"symbol":"ICXBTC","localSymbol":"ICXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ICX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ICXBUSD":{"symbol":"ICXBUSD","localSymbol":"ICXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ICX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ICXETH":{"symbol":"ICXETH","localSymbol":"ICXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ICX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ICXUSDT":{"symbol":"ICXUSDT","localSymbol":"ICXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ICX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"IDEXBNB":{"symbol":"IDEXBNB","localSymbol":"IDEXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"IDEX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"IDEXBTC":{"symbol":"IDEXBTC","localSymbol":"IDEXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"IDEX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"IDEXBUSD":{"symbol":"IDEXBUSD","localSymbol":"IDEXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"IDEX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"IDEXUSDT":{"symbol":"IDEXUSDT","localSymbol":"IDEXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"IDEX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":913205152.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":100.00000000,"tickSize":0.00001000},"ILVBNB":{"symbol":"ILVBNB","localSymbol":"ILVBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ILV","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ILVBTC":{"symbol":"ILVBTC","localSymbol":"ILVBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ILV","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ILVBUSD":{"symbol":"ILVBUSD","localSymbol":"ILVBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ILV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"ILVUSDT":{"symbol":"ILVUSDT","localSymbol":"ILVUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ILV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"IMXBNB":{"symbol":"IMXBNB","localSymbol":"IMXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"IMX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"IMXBTC":{"symbol":"IMXBTC","localSymbol":"IMXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"IMX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"IMXBUSD":{"symbol":"IMXBUSD","localSymbol":"IMXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"IMX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"IMXUSDT":{"symbol":"IMXUSDT","localSymbol":"IMXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"IMX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"INJBNB":{"symbol":"INJBNB","localSymbol":"INJBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"INJ","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"INJBTC":{"symbol":"INJBTC","localSymbol":"INJBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"INJ","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"INJBUSD":{"symbol":"INJBUSD","localSymbol":"INJBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"INJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"INJTRY":{"symbol":"INJTRY","localSymbol":"INJTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"INJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"INJUSDT":{"symbol":"INJUSDT","localSymbol":"INJUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"INJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"INSBTC":{"symbol":"INSBTC","localSymbol":"INSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"INS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"INSETH":{"symbol":"INSETH","localSymbol":"INSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"INS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"IOSTBTC":{"symbol":"IOSTBTC","localSymbol":"IOSTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"IOST","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"IOSTBUSD":{"symbol":"IOSTBUSD","localSymbol":"IOSTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"IOST","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"IOSTETH":{"symbol":"IOSTETH","localSymbol":"IOSTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"IOST","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"IOSTUSDT":{"symbol":"IOSTUSDT","localSymbol":"IOSTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"IOST","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"IOTABNB":{"symbol":"IOTABNB","localSymbol":"IOTABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"IOTA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"IOTABTC":{"symbol":"IOTABTC","localSymbol":"IOTABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"IOTA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"IOTABUSD":{"symbol":"IOTABUSD","localSymbol":"IOTABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"IOTA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"IOTAETH":{"symbol":"IOTAETH","localSymbol":"IOTAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"IOTA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"IOTAUSDT":{"symbol":"IOTAUSDT","localSymbol":"IOTAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"IOTA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"IOTXBTC":{"symbol":"IOTXBTC","localSymbol":"IOTXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"IOTX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"IOTXBUSD":{"symbol":"IOTXBUSD","localSymbol":"IOTXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"IOTX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"IOTXETH":{"symbol":"IOTXETH","localSymbol":"IOTXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"IOTX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"IOTXUSDT":{"symbol":"IOTXUSDT","localSymbol":"IOTXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"IOTX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"IQBNB":{"symbol":"IQBNB","localSymbol":"IQBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"IQ","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"IQBUSD":{"symbol":"IQBUSD","localSymbol":"IQBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"IQ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"IRISBNB":{"symbol":"IRISBNB","localSymbol":"IRISBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"IRIS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"IRISBTC":{"symbol":"IRISBTC","localSymbol":"IRISBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"IRIS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"IRISBUSD":{"symbol":"IRISBUSD","localSymbol":"IRISBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"IRIS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"IRISUSDT":{"symbol":"IRISUSDT","localSymbol":"IRISUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"IRIS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"JASMYBNB":{"symbol":"JASMYBNB","localSymbol":"JASMYBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"JASMY","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"JASMYBTC":{"symbol":"JASMYBTC","localSymbol":"JASMYBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"JASMY","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"JASMYBUSD":{"symbol":"JASMYBUSD","localSymbol":"JASMYBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"JASMY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"JASMYETH":{"symbol":"JASMYETH","localSymbol":"JASMYETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"JASMY","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"JASMYEUR":{"symbol":"JASMYEUR","localSymbol":"JASMYEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"JASMY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"JASMYTRY":{"symbol":"JASMYTRY","localSymbol":"JASMYTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"JASMY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"JASMYUSDT":{"symbol":"JASMYUSDT","localSymbol":"JASMYUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"JASMY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"JOEBTC":{"symbol":"JOEBTC","localSymbol":"JOEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"JOE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"JOEBUSD":{"symbol":"JOEBUSD","localSymbol":"JOEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"JOE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"JOEUSDT":{"symbol":"JOEUSDT","localSymbol":"JOEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"JOE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"JSTBNB":{"symbol":"JSTBNB","localSymbol":"JSTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"JST","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"JSTBTC":{"symbol":"JSTBTC","localSymbol":"JSTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"JST","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"JSTBUSD":{"symbol":"JSTBUSD","localSymbol":"JSTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"JST","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"JSTUSDT":{"symbol":"JSTUSDT","localSymbol":"JSTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"JST","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"JUVBTC":{"symbol":"JUVBTC","localSymbol":"JUVBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"JUV","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"JUVBUSD":{"symbol":"JUVBUSD","localSymbol":"JUVBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"JUV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"JUVUSDT":{"symbol":"JUVUSDT","localSymbol":"JUVUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"JUV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"KAVABNB":{"symbol":"KAVABNB","localSymbol":"KAVABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"KAVA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"KAVABTC":{"symbol":"KAVABTC","localSymbol":"KAVABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"KAVA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"KAVABUSD":{"symbol":"KAVABUSD","localSymbol":"KAVABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"KAVA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"KAVAETH":{"symbol":"KAVAETH","localSymbol":"KAVAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"KAVA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"KAVAUSDT":{"symbol":"KAVAUSDT","localSymbol":"KAVAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"KAVA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"KDABTC":{"symbol":"KDABTC","localSymbol":"KDABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"KDA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"KDABUSD":{"symbol":"KDABUSD","localSymbol":"KDABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"KDA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"KDAUSDT":{"symbol":"KDAUSDT","localSymbol":"KDAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"KDA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"KEEPBNB":{"symbol":"KEEPBNB","localSymbol":"KEEPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"KEEP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":10000.00000000,"tickSize":0.00000010},"KEEPBTC":{"symbol":"KEEPBTC","localSymbol":"KEEPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"KEEP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"KEEPBUSD":{"symbol":"KEEPBUSD","localSymbol":"KEEPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"KEEP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":100000.00000000,"tickSize":0.00010000},"KEEPUSDT":{"symbol":"KEEPUSDT","localSymbol":"KEEPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"KEEP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":100000.00000000,"tickSize":0.00010000},"KEYBTC":{"symbol":"KEYBTC","localSymbol":"KEYBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"KEY","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"KEYBUSD":{"symbol":"KEYBUSD","localSymbol":"KEYBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"KEY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"KEYETH":{"symbol":"KEYETH","localSymbol":"KEYETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"KEY","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"KEYUSDT":{"symbol":"KEYUSDT","localSymbol":"KEYUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"KEY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"KLAYBNB":{"symbol":"KLAYBNB","localSymbol":"KLAYBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"KLAY","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"KLAYBTC":{"symbol":"KLAYBTC","localSymbol":"KLAYBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"KLAY","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"KLAYBUSD":{"symbol":"KLAYBUSD","localSymbol":"KLAYBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"KLAY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"KLAYUSDT":{"symbol":"KLAYUSDT","localSymbol":"KLAYUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"KLAY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"KMDBTC":{"symbol":"KMDBTC","localSymbol":"KMDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"KMD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"KMDBUSD":{"symbol":"KMDBUSD","localSymbol":"KMDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"KMD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"KMDETH":{"symbol":"KMDETH","localSymbol":"KMDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"KMD","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"KMDUSDT":{"symbol":"KMDUSDT","localSymbol":"KMDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"KMD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"KNCBNB":{"symbol":"KNCBNB","localSymbol":"KNCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"KNC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"KNCBTC":{"symbol":"KNCBTC","localSymbol":"KNCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"KNC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"KNCBUSD":{"symbol":"KNCBUSD","localSymbol":"KNCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"KNC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"KNCETH":{"symbol":"KNCETH","localSymbol":"KNCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"KNC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"KNCUSDT":{"symbol":"KNCUSDT","localSymbol":"KNCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"KNC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"KP3RBNB":{"symbol":"KP3RBNB","localSymbol":"KP3RBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"KP3R","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"KP3RBUSD":{"symbol":"KP3RBUSD","localSymbol":"KP3RBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"KP3R","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"KP3RUSDT":{"symbol":"KP3RUSDT","localSymbol":"KP3RUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"KP3R","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92230.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":999990.00000000,"tickSize":0.01000000},"KSMAUD":{"symbol":"KSMAUD","localSymbol":"KSMAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"KSM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"KSMBNB":{"symbol":"KSMBNB","localSymbol":"KSMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"KSM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"KSMBTC":{"symbol":"KSMBTC","localSymbol":"KSMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"KSM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"KSMBUSD":{"symbol":"KSMBUSD","localSymbol":"KSMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"KSM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"KSMETH":{"symbol":"KSMETH","localSymbol":"KSMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"KSM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00010000,"maxQuantity":92141578.00000000,"stepSize":0.00010000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"KSMUSDT":{"symbol":"KSMUSDT","localSymbol":"KSMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"KSM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"LAZIOBTC":{"symbol":"LAZIOBTC","localSymbol":"LAZIOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LAZIO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LAZIOBUSD":{"symbol":"LAZIOBUSD","localSymbol":"LAZIOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LAZIO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":100000.00000000,"tickSize":0.00010000},"LAZIOEUR":{"symbol":"LAZIOEUR","localSymbol":"LAZIOEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"LAZIO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LAZIOTRY":{"symbol":"LAZIOTRY","localSymbol":"LAZIOTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"LAZIO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"LAZIOUSDT":{"symbol":"LAZIOUSDT","localSymbol":"LAZIOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LAZIO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LDOBTC":{"symbol":"LDOBTC","localSymbol":"LDOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LDO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LDOBUSD":{"symbol":"LDOBUSD","localSymbol":"LDOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LDO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"LDOUSDT":{"symbol":"LDOUSDT","localSymbol":"LDOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LDO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"LENDBKRW":{"symbol":"LENDBKRW","localSymbol":"LENDBKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"LEND","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.10000000,"maxQuantity":92232.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":1000007.00000000,"tickSize":0.01000000},"LENDBTC":{"symbol":"LENDBTC","localSymbol":"LENDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LEND","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LENDBUSD":{"symbol":"LENDBUSD","localSymbol":"LENDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LEND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LENDETH":{"symbol":"LENDETH","localSymbol":"LENDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"LEND","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LENDUSDT":{"symbol":"LENDUSDT","localSymbol":"LENDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LEND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"LEVERBUSD":{"symbol":"LEVERBUSD","localSymbol":"LEVERBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LEVER","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"LEVERUSDT":{"symbol":"LEVERUSDT","localSymbol":"LEVERUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LEVER","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"LINABNB":{"symbol":"LINABNB","localSymbol":"LINABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"LINA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LINABTC":{"symbol":"LINABTC","localSymbol":"LINABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LINA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LINABUSD":{"symbol":"LINABUSD","localSymbol":"LINABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LINA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"LINAUSDT":{"symbol":"LINAUSDT","localSymbol":"LINAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LINA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"LINKAUD":{"symbol":"LINKAUD","localSymbol":"LINKAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"LINK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"LINKBKRW":{"symbol":"LINKBKRW","localSymbol":"LINKBKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"LINK","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.10000000,"maxQuantity":9200.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"LINKBNB":{"symbol":"LINKBNB","localSymbol":"LINKBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"LINK","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"LINKBRL":{"symbol":"LINKBRL","localSymbol":"LINKBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"LINK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":45000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"LINKBTC":{"symbol":"LINKBTC","localSymbol":"LINKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LINK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"LINKBUSD":{"symbol":"LINKBUSD","localSymbol":"LINKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LINK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"LINKDOWNUSDT":{"symbol":"LINKDOWNUSDT","localSymbol":"LINKDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LINKDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":300000.00000000,"stepSize":0.01000000,"minPrice":0.00022400,"maxPrice":0.00423900,"tickSize":0.00000100},"LINKETH":{"symbol":"LINKETH","localSymbol":"LINKETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"LINK","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"LINKEUR":{"symbol":"LINKEUR","localSymbol":"LINKEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"LINK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"LINKGBP":{"symbol":"LINKGBP","localSymbol":"LINKGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"LINK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"LINKNGN":{"symbol":"LINKNGN","localSymbol":"LINKNGN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"NGN","baseCurrency":"LINK","minNotional":500.00000000,"minAmount":500.00000000,"minQuantity":0.00001000,"maxQuantity":92232.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":1000007.00000000,"tickSize":1.00000000},"LINKPAX":{"symbol":"LINKPAX","localSymbol":"LINKPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"LINK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LINKTRY":{"symbol":"LINKTRY","localSymbol":"LINKTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"LINK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"LINKTUSD":{"symbol":"LINKTUSD","localSymbol":"LINKTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"LINK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"LINKUPUSDT":{"symbol":"LINKUPUSDT","localSymbol":"LINKUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LINKUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.00093000,"maxPrice":0.01748000,"tickSize":0.00001000},"LINKUSDC":{"symbol":"LINKUSDC","localSymbol":"LINKUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"LINK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"LINKUSDT":{"symbol":"LINKUSDT","localSymbol":"LINKUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LINK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"LITBTC":{"symbol":"LITBTC","localSymbol":"LITBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LIT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LITBUSD":{"symbol":"LITBUSD","localSymbol":"LITBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LIT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"LITETH":{"symbol":"LITETH","localSymbol":"LITETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"LIT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"LITUSDT":{"symbol":"LITUSDT","localSymbol":"LITUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LIT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"LOKABNB":{"symbol":"LOKABNB","localSymbol":"LOKABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"LOKA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"LOKABTC":{"symbol":"LOKABTC","localSymbol":"LOKABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LOKA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LOKABUSD":{"symbol":"LOKABUSD","localSymbol":"LOKABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LOKA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LOKAUSDT":{"symbol":"LOKAUSDT","localSymbol":"LOKAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LOKA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LOOMBNB":{"symbol":"LOOMBNB","localSymbol":"LOOMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"LOOM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"LOOMBTC":{"symbol":"LOOMBTC","localSymbol":"LOOMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LOOM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LOOMBUSD":{"symbol":"LOOMBUSD","localSymbol":"LOOMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LOOM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"LOOMETH":{"symbol":"LOOMETH","localSymbol":"LOOMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"LOOM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LPTBNB":{"symbol":"LPTBNB","localSymbol":"LPTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"LPT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141570.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"LPTBTC":{"symbol":"LPTBTC","localSymbol":"LPTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LPT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141570.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"LPTBUSD":{"symbol":"LPTBUSD","localSymbol":"LPTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LPT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222440.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"LPTUSDT":{"symbol":"LPTUSDT","localSymbol":"LPTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LPT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222440.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"LRCBNB":{"symbol":"LRCBNB","localSymbol":"LRCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"LRC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"LRCBTC":{"symbol":"LRCBTC","localSymbol":"LRCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LRC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LRCBUSD":{"symbol":"LRCBUSD","localSymbol":"LRCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LRC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LRCETH":{"symbol":"LRCETH","localSymbol":"LRCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"LRC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LRCTRY":{"symbol":"LRCTRY","localSymbol":"LRCTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"LRC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"LRCUSDT":{"symbol":"LRCUSDT","localSymbol":"LRCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LRC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LSKBNB":{"symbol":"LSKBNB","localSymbol":"LSKBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"LSK","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"LSKBTC":{"symbol":"LSKBTC","localSymbol":"LSKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LSK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LSKBUSD":{"symbol":"LSKBUSD","localSymbol":"LSKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LSK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"LSKETH":{"symbol":"LSKETH","localSymbol":"LSKETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"LSK","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"LSKUSDT":{"symbol":"LSKUSDT","localSymbol":"LSKUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LSK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"LTCBNB":{"symbol":"LTCBNB","localSymbol":"LTCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"LTC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LTCBRL":{"symbol":"LTCBRL","localSymbol":"LTCBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"LTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":45000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"LTCBTC":{"symbol":"LTCBTC","localSymbol":"LTCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LTC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":100000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":100000.00000000,"tickSize":0.00000100},"LTCBUSD":{"symbol":"LTCBUSD","localSymbol":"LTCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"LTCDOWNUSDT":{"symbol":"LTCDOWNUSDT","localSymbol":"LTCDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LTCDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":610819340.00000000,"stepSize":0.01000000,"minPrice":0.22280000,"maxPrice":4.23210000,"tickSize":0.00010000},"LTCETH":{"symbol":"LTCETH","localSymbol":"LTCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"LTC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":9000000.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"LTCEUR":{"symbol":"LTCEUR","localSymbol":"LTCEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"LTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"LTCGBP":{"symbol":"LTCGBP","localSymbol":"LTCGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"LTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"LTCNGN":{"symbol":"LTCNGN","localSymbol":"LTCNGN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"NGN","baseCurrency":"LTC","minNotional":500.00000000,"minAmount":500.00000000,"minQuantity":0.00001000,"maxQuantity":9221.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":10001487.00000000,"tickSize":1.00000000},"LTCPAX":{"symbol":"LTCPAX","localSymbol":"LTCPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"LTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"LTCRUB":{"symbol":"LTCRUB","localSymbol":"LTCRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"LTC","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":1000000.00000000,"tickSize":0.10000000},"LTCTUSD":{"symbol":"LTCTUSD","localSymbol":"LTCTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"LTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"LTCUAH":{"symbol":"LTCUAH","localSymbol":"LTCUAH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"UAH","baseCurrency":"LTC","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.00100000,"maxQuantity":92233.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":999996.00000000,"tickSize":1.00000000},"LTCUPUSDT":{"symbol":"LTCUPUSDT","localSymbol":"LTCUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LTCUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.04860000,"maxPrice":0.92300000,"tickSize":0.00010000},"LTCUSDC":{"symbol":"LTCUSDC","localSymbol":"LTCUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"LTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"LTCUSDT":{"symbol":"LTCUSDT","localSymbol":"LTCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"LTOBNB":{"symbol":"LTOBNB","localSymbol":"LTOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"LTO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"LTOBTC":{"symbol":"LTOBTC","localSymbol":"LTOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LTO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LTOBUSD":{"symbol":"LTOBUSD","localSymbol":"LTOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LTOUSDT":{"symbol":"LTOUSDT","localSymbol":"LTOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LUNAAUD":{"symbol":"LUNAAUD","localSymbol":"LUNAAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"LUNA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"LUNABIDR":{"symbol":"LUNABIDR","localSymbol":"LUNABIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"LUNA","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.01000000,"maxQuantity":922337194.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000000.00000000,"tickSize":0.01000000},"LUNABNB":{"symbol":"LUNABNB","localSymbol":"LUNABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"LUNA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LUNABRL":{"symbol":"LUNABRL","localSymbol":"LUNABRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"LUNA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9221525.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"LUNABTC":{"symbol":"LUNABTC","localSymbol":"LUNABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LUNA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LUNABUSD":{"symbol":"LUNABUSD","localSymbol":"LUNABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LUNA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LUNAETH":{"symbol":"LUNAETH","localSymbol":"LUNAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"LUNA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"LUNAEUR":{"symbol":"LUNAEUR","localSymbol":"LUNAEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"LUNA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"LUNAGBP":{"symbol":"LUNAGBP","localSymbol":"LUNAGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"LUNA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"LUNATRY":{"symbol":"LUNATRY","localSymbol":"LUNATRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"LUNA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"LUNAUSDT":{"symbol":"LUNAUSDT","localSymbol":"LUNAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LUNA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"LUNAUST":{"symbol":"LUNAUST","localSymbol":"LUNAUST","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"UST","baseCurrency":"LUNA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":10000.00000000,"tickSize":0.00000010},"LUNBTC":{"symbol":"LUNBTC","localSymbol":"LUNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"LUN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"LUNCBUSD":{"symbol":"LUNCBUSD","localSymbol":"LUNCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"LUNC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":8384883677.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":10.00000000,"tickSize":0.00000001},"LUNCUSDT":{"symbol":"LUNCUSDT","localSymbol":"LUNCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"LUNC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":8384883677.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":10.00000000,"tickSize":0.00000001},"LUNETH":{"symbol":"LUNETH","localSymbol":"LUNETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"LUN","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MANABIDR":{"symbol":"MANABIDR","localSymbol":"MANABIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"MANA","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.01000000,"maxQuantity":9223371110.00000000,"stepSize":0.01000000,"minPrice":1.00000000,"maxPrice":10000000.00000000,"tickSize":1.00000000},"MANABNB":{"symbol":"MANABNB","localSymbol":"MANABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MANA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MANABRL":{"symbol":"MANABRL","localSymbol":"MANABRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"MANA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"MANABTC":{"symbol":"MANABTC","localSymbol":"MANABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MANA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MANABUSD":{"symbol":"MANABUSD","localSymbol":"MANABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MANA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"MANAETH":{"symbol":"MANAETH","localSymbol":"MANAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"MANA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"MANATRY":{"symbol":"MANATRY","localSymbol":"MANATRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"MANA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"MANAUSDT":{"symbol":"MANAUSDT","localSymbol":"MANAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MANA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"MASKBNB":{"symbol":"MASKBNB","localSymbol":"MASKBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MASK","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"MASKBUSD":{"symbol":"MASKBUSD","localSymbol":"MASKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MASK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"MASKUSDT":{"symbol":"MASKUSDT","localSymbol":"MASKUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MASK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"MATICAUD":{"symbol":"MATICAUD","localSymbol":"MATICAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"MATIC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"MATICBIDR":{"symbol":"MATICBIDR","localSymbol":"MATICBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"MATIC","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.10000000,"maxQuantity":184467.00000000,"stepSize":0.10000000,"minPrice":1.00000000,"maxPrice":500000.00000000,"tickSize":1.00000000},"MATICBNB":{"symbol":"MATICBNB","localSymbol":"MATICBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MATIC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MATICBRL":{"symbol":"MATICBRL","localSymbol":"MATICBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"MATIC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222440.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"MATICBTC":{"symbol":"MATICBTC","localSymbol":"MATICBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MATIC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MATICBUSD":{"symbol":"MATICBUSD","localSymbol":"MATICBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MATIC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"MATICETH":{"symbol":"MATICETH","localSymbol":"MATICETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"MATIC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"MATICEUR":{"symbol":"MATICEUR","localSymbol":"MATICEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"MATIC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"MATICGBP":{"symbol":"MATICGBP","localSymbol":"MATICGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"MATIC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"MATICRUB":{"symbol":"MATICRUB","localSymbol":"MATICRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"MATIC","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"MATICTRY":{"symbol":"MATICTRY","localSymbol":"MATICTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"MATIC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"MATICUSDT":{"symbol":"MATICUSDT","localSymbol":"MATICUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MATIC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"MBLBNB":{"symbol":"MBLBNB","localSymbol":"MBLBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MBL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MBLBTC":{"symbol":"MBLBTC","localSymbol":"MBLBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MBL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MBLBUSD":{"symbol":"MBLBUSD","localSymbol":"MBLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MBL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MBLUSDT":{"symbol":"MBLUSDT","localSymbol":"MBLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MBL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MBOXBNB":{"symbol":"MBOXBNB","localSymbol":"MBOXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MBOX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MBOXBTC":{"symbol":"MBOXBTC","localSymbol":"MBOXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MBOX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MBOXBUSD":{"symbol":"MBOXBUSD","localSymbol":"MBOXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MBOX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"MBOXTRY":{"symbol":"MBOXTRY","localSymbol":"MBOXTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"MBOX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"MBOXUSDT":{"symbol":"MBOXUSDT","localSymbol":"MBOXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MBOX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"MCBNB":{"symbol":"MCBNB","localSymbol":"MCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MCBTC":{"symbol":"MCBTC","localSymbol":"MCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MCBUSD":{"symbol":"MCBUSD","localSymbol":"MCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"MCOBNB":{"symbol":"MCOBNB","localSymbol":"MCOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MCO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"MCOBTC":{"symbol":"MCOBTC","localSymbol":"MCOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MCO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"MCOETH":{"symbol":"MCOETH","localSymbol":"MCOETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"MCO","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MCOUSDT":{"symbol":"MCOUSDT","localSymbol":"MCOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MCO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"MCUSDT":{"symbol":"MCUSDT","localSymbol":"MCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"MDABTC":{"symbol":"MDABTC","localSymbol":"MDABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MDA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MDAETH":{"symbol":"MDAETH","localSymbol":"MDAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"MDA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"MDTBNB":{"symbol":"MDTBNB","localSymbol":"MDTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MDT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"MDTBTC":{"symbol":"MDTBTC","localSymbol":"MDTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MDT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MDTBUSD":{"symbol":"MDTBUSD","localSymbol":"MDTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MDT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"MDTUSDT":{"symbol":"MDTUSDT","localSymbol":"MDTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MDT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"MDXBNB":{"symbol":"MDXBNB","localSymbol":"MDXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MDX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MDXBTC":{"symbol":"MDXBTC","localSymbol":"MDXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MDX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MDXBUSD":{"symbol":"MDXBUSD","localSymbol":"MDXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MDX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"MDXUSDT":{"symbol":"MDXUSDT","localSymbol":"MDXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MDX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"MFTBNB":{"symbol":"MFTBNB","localSymbol":"MFTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MFT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MFTBTC":{"symbol":"MFTBTC","localSymbol":"MFTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MFT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MFTETH":{"symbol":"MFTETH","localSymbol":"MFTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"MFT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MFTUSDT":{"symbol":"MFTUSDT","localSymbol":"MFTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MFT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MINABNB":{"symbol":"MINABNB","localSymbol":"MINABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MINA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MINABTC":{"symbol":"MINABTC","localSymbol":"MINABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MINA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MINABUSD":{"symbol":"MINABUSD","localSymbol":"MINABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MINA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"MINATRY":{"symbol":"MINATRY","localSymbol":"MINATRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"MINA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"MINAUSDT":{"symbol":"MINAUSDT","localSymbol":"MINAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MINA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"MIRBTC":{"symbol":"MIRBTC","localSymbol":"MIRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MIR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MIRBUSD":{"symbol":"MIRBUSD","localSymbol":"MIRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MIR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"MIRUSDT":{"symbol":"MIRUSDT","localSymbol":"MIRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MIR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"MITHBNB":{"symbol":"MITHBNB","localSymbol":"MITHBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MITH","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"MITHBTC":{"symbol":"MITHBTC","localSymbol":"MITHBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MITH","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MITHUSDT":{"symbol":"MITHUSDT","localSymbol":"MITHUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MITH","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"MKRBNB":{"symbol":"MKRBNB","localSymbol":"MKRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MKR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00010000,"maxQuantity":900000.00000000,"stepSize":0.00010000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"MKRBTC":{"symbol":"MKRBTC","localSymbol":"MKRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MKR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00010000,"maxQuantity":10000000.00000000,"stepSize":0.00010000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"MKRBUSD":{"symbol":"MKRBUSD","localSymbol":"MKRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MKR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":100000.00000000,"tickSize":1.00000000},"MKRUSDT":{"symbol":"MKRUSDT","localSymbol":"MKRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MKR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":100000.00000000,"tickSize":1.00000000},"MLNBNB":{"symbol":"MLNBNB","localSymbol":"MLNBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MLN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"MLNBTC":{"symbol":"MLNBTC","localSymbol":"MLNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MLN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MLNBUSD":{"symbol":"MLNBUSD","localSymbol":"MLNBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MLN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"MLNUSDT":{"symbol":"MLNUSDT","localSymbol":"MLNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MLN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"MOBBTC":{"symbol":"MOBBTC","localSymbol":"MOBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MOB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MOBBUSD":{"symbol":"MOBBUSD","localSymbol":"MOBBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MOB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"MOBUSDT":{"symbol":"MOBUSDT","localSymbol":"MOBUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MOB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"MODBTC":{"symbol":"MODBTC","localSymbol":"MODBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MOD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"MODETH":{"symbol":"MODETH","localSymbol":"MODETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"MOD","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"MOVRBNB":{"symbol":"MOVRBNB","localSymbol":"MOVRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"MOVR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"MOVRBTC":{"symbol":"MOVRBTC","localSymbol":"MOVRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MOVR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"MOVRBUSD":{"symbol":"MOVRBUSD","localSymbol":"MOVRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MOVR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"MOVRUSDT":{"symbol":"MOVRUSDT","localSymbol":"MOVRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MOVR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"MTHBTC":{"symbol":"MTHBTC","localSymbol":"MTHBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MTH","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MTHETH":{"symbol":"MTHETH","localSymbol":"MTHETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"MTH","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MTLBTC":{"symbol":"MTLBTC","localSymbol":"MTLBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MTL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"MTLBUSD":{"symbol":"MTLBUSD","localSymbol":"MTLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MTL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"MTLETH":{"symbol":"MTLETH","localSymbol":"MTLETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"MTL","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"MTLUSDT":{"symbol":"MTLUSDT","localSymbol":"MTLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MTL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"MULTIBTC":{"symbol":"MULTIBTC","localSymbol":"MULTIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"MULTI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"MULTIBUSD":{"symbol":"MULTIBUSD","localSymbol":"MULTIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"MULTI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"MULTIUSDT":{"symbol":"MULTIUSDT","localSymbol":"MULTIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"MULTI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"NANOBNB":{"symbol":"NANOBNB","localSymbol":"NANOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NANO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"NANOBTC":{"symbol":"NANOBTC","localSymbol":"NANOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NANO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"NANOBUSD":{"symbol":"NANOBUSD","localSymbol":"NANOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"NANO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"NANOETH":{"symbol":"NANOETH","localSymbol":"NANOETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"NANO","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"NANOUSDT":{"symbol":"NANOUSDT","localSymbol":"NANOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NANO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"NASBNB":{"symbol":"NASBNB","localSymbol":"NASBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NAS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"NASBTC":{"symbol":"NASBTC","localSymbol":"NASBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NAS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NASETH":{"symbol":"NASETH","localSymbol":"NASETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"NAS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NAVBNB":{"symbol":"NAVBNB","localSymbol":"NAVBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NAV","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"NAVBTC":{"symbol":"NAVBTC","localSymbol":"NAVBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NAV","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NAVETH":{"symbol":"NAVETH","localSymbol":"NAVETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"NAV","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"NBSBTC":{"symbol":"NBSBTC","localSymbol":"NBSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NBS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NBSUSDT":{"symbol":"NBSUSDT","localSymbol":"NBSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NBS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"NCASHBNB":{"symbol":"NCASHBNB","localSymbol":"NCASHBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NCASH","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":10000.00000000,"tickSize":0.00000010},"NCASHBTC":{"symbol":"NCASHBTC","localSymbol":"NCASHBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NCASH","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NCASHETH":{"symbol":"NCASHETH","localSymbol":"NCASHETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"NCASH","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NEARBNB":{"symbol":"NEARBNB","localSymbol":"NEARBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NEAR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"NEARBTC":{"symbol":"NEARBTC","localSymbol":"NEARBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NEAR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NEARBUSD":{"symbol":"NEARBUSD","localSymbol":"NEARBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"NEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"NEARETH":{"symbol":"NEARETH","localSymbol":"NEARETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"NEAR","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"NEAREUR":{"symbol":"NEAREUR","localSymbol":"NEAREUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"NEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"NEARRUB":{"symbol":"NEARRUB","localSymbol":"NEARRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"NEAR","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.00100000,"maxQuantity":92233.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":92233.00000000,"tickSize":0.10000000},"NEARTRY":{"symbol":"NEARTRY","localSymbol":"NEARTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"NEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"NEARUSDT":{"symbol":"NEARUSDT","localSymbol":"NEARUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"NEBLBNB":{"symbol":"NEBLBNB","localSymbol":"NEBLBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NEBL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"NEBLBTC":{"symbol":"NEBLBTC","localSymbol":"NEBLBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NEBL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NEBLBUSD":{"symbol":"NEBLBUSD","localSymbol":"NEBLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"NEBL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"NEBLUSDT":{"symbol":"NEBLUSDT","localSymbol":"NEBLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NEBL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"NEOBNB":{"symbol":"NEOBNB","localSymbol":"NEOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NEO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"NEOBTC":{"symbol":"NEOBTC","localSymbol":"NEOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NEO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":100000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":100000.00000000,"tickSize":0.00000100},"NEOBUSD":{"symbol":"NEOBUSD","localSymbol":"NEOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"NEO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"NEOETH":{"symbol":"NEOETH","localSymbol":"NEOETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"NEO","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"NEOPAX":{"symbol":"NEOPAX","localSymbol":"NEOPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"NEO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"NEORUB":{"symbol":"NEORUB","localSymbol":"NEORUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"NEO","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.00100000,"maxQuantity":92233.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":999996.00000000,"tickSize":1.00000000},"NEOTRY":{"symbol":"NEOTRY","localSymbol":"NEOTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"NEO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"NEOTUSD":{"symbol":"NEOTUSD","localSymbol":"NEOTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"NEO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"NEOUSDC":{"symbol":"NEOUSDC","localSymbol":"NEOUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"NEO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"NEOUSDT":{"symbol":"NEOUSDT","localSymbol":"NEOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NEO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"NEXOBTC":{"symbol":"NEXOBTC","localSymbol":"NEXOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NEXO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NEXOBUSD":{"symbol":"NEXOBUSD","localSymbol":"NEXOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"NEXO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"NEXOUSDT":{"symbol":"NEXOUSDT","localSymbol":"NEXOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NEXO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"NKNBNB":{"symbol":"NKNBNB","localSymbol":"NKNBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NKN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"NKNBTC":{"symbol":"NKNBTC","localSymbol":"NKNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NKN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NKNBUSD":{"symbol":"NKNBUSD","localSymbol":"NKNBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"NKN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"NKNUSDT":{"symbol":"NKNUSDT","localSymbol":"NKNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NKN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"NMRBTC":{"symbol":"NMRBTC","localSymbol":"NMRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NMR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":10000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"NMRBUSD":{"symbol":"NMRBUSD","localSymbol":"NMRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"NMR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"NMRUSDT":{"symbol":"NMRUSDT","localSymbol":"NMRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NMR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"NPXSBTC":{"symbol":"NPXSBTC","localSymbol":"NPXSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NPXS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NPXSETH":{"symbol":"NPXSETH","localSymbol":"NPXSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"NPXS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NPXSUSDC":{"symbol":"NPXSUSDC","localSymbol":"NPXSUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"NPXS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"NPXSUSDT":{"symbol":"NPXSUSDT","localSymbol":"NPXSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NPXS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"NUAUD":{"symbol":"NUAUD","localSymbol":"NUAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"NU","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"NUBNB":{"symbol":"NUBNB","localSymbol":"NUBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NU","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"NUBTC":{"symbol":"NUBTC","localSymbol":"NUBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NU","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NUBUSD":{"symbol":"NUBUSD","localSymbol":"NUBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"NU","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"NULSBNB":{"symbol":"NULSBNB","localSymbol":"NULSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NULS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"NULSBTC":{"symbol":"NULSBTC","localSymbol":"NULSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NULS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NULSBUSD":{"symbol":"NULSBUSD","localSymbol":"NULSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"NULS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"NULSETH":{"symbol":"NULSETH","localSymbol":"NULSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"NULS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NULSUSDT":{"symbol":"NULSUSDT","localSymbol":"NULSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NULS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"NURUB":{"symbol":"NURUB","localSymbol":"NURUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"NU","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"NUUSDT":{"symbol":"NUUSDT","localSymbol":"NUUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"NU","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"NXSBNB":{"symbol":"NXSBNB","localSymbol":"NXSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"NXS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"NXSBTC":{"symbol":"NXSBTC","localSymbol":"NXSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"NXS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"NXSETH":{"symbol":"NXSETH","localSymbol":"NXSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"NXS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"OAXBTC":{"symbol":"OAXBTC","localSymbol":"OAXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"OAX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"OAXETH":{"symbol":"OAXETH","localSymbol":"OAXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"OAX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"OCEANBNB":{"symbol":"OCEANBNB","localSymbol":"OCEANBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"OCEAN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"OCEANBTC":{"symbol":"OCEANBTC","localSymbol":"OCEANBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"OCEAN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"OCEANBUSD":{"symbol":"OCEANBUSD","localSymbol":"OCEANBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"OCEAN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"OCEANUSDT":{"symbol":"OCEANUSDT","localSymbol":"OCEANUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"OCEAN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"OGBTC":{"symbol":"OGBTC","localSymbol":"OGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"OG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"OGBUSD":{"symbol":"OGBUSD","localSymbol":"OGBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"OG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"OGNBNB":{"symbol":"OGNBNB","localSymbol":"OGNBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"OGN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"OGNBTC":{"symbol":"OGNBTC","localSymbol":"OGNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"OGN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"OGNBUSD":{"symbol":"OGNBUSD","localSymbol":"OGNBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"OGN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"OGNUSDT":{"symbol":"OGNUSDT","localSymbol":"OGNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"OGN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"OGUSDT":{"symbol":"OGUSDT","localSymbol":"OGUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"OG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"OMBTC":{"symbol":"OMBTC","localSymbol":"OMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"OM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"OMBUSD":{"symbol":"OMBUSD","localSymbol":"OMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"OM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"OMGBNB":{"symbol":"OMGBNB","localSymbol":"OMGBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"OMG","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"OMGBTC":{"symbol":"OMGBTC","localSymbol":"OMGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"OMG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"OMGBUSD":{"symbol":"OMGBUSD","localSymbol":"OMGBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"OMG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"OMGETH":{"symbol":"OMGETH","localSymbol":"OMGETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"OMG","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"OMGUSDT":{"symbol":"OMGUSDT","localSymbol":"OMGUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"OMG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"OMUSDT":{"symbol":"OMUSDT","localSymbol":"OMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"OM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ONEBIDR":{"symbol":"ONEBIDR","localSymbol":"ONEBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"ONE","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"ONEBNB":{"symbol":"ONEBNB","localSymbol":"ONEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ONE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ONEBTC":{"symbol":"ONEBTC","localSymbol":"ONEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ONE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ONEBUSD":{"symbol":"ONEBUSD","localSymbol":"ONEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ONE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ONEETH":{"symbol":"ONEETH","localSymbol":"ONEETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ONE","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ONEPAX":{"symbol":"ONEPAX","localSymbol":"ONEPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"ONE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ONETRY":{"symbol":"ONETRY","localSymbol":"ONETRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ONE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ONETUSD":{"symbol":"ONETUSD","localSymbol":"ONETUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"ONE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ONEUSDC":{"symbol":"ONEUSDC","localSymbol":"ONEUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"ONE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ONEUSDT":{"symbol":"ONEUSDT","localSymbol":"ONEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ONE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ONGBNB":{"symbol":"ONGBNB","localSymbol":"ONGBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ONG","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ONGBTC":{"symbol":"ONGBTC","localSymbol":"ONGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ONG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ONGUSDT":{"symbol":"ONGUSDT","localSymbol":"ONGUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ONG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ONTBNB":{"symbol":"ONTBNB","localSymbol":"ONTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ONT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ONTBTC":{"symbol":"ONTBTC","localSymbol":"ONTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ONT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ONTBUSD":{"symbol":"ONTBUSD","localSymbol":"ONTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ONT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ONTETH":{"symbol":"ONTETH","localSymbol":"ONTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ONT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ONTPAX":{"symbol":"ONTPAX","localSymbol":"ONTPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"ONT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ONTTRY":{"symbol":"ONTTRY","localSymbol":"ONTTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ONT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ONTUSDC":{"symbol":"ONTUSDC","localSymbol":"ONTUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"ONT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ONTUSDT":{"symbol":"ONTUSDT","localSymbol":"ONTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ONT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"OOKIBNB":{"symbol":"OOKIBNB","localSymbol":"OOKIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"OOKI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"OOKIBUSD":{"symbol":"OOKIBUSD","localSymbol":"OOKIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"OOKI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"OOKIETH":{"symbol":"OOKIETH","localSymbol":"OOKIETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"OOKI","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"OOKIUSDT":{"symbol":"OOKIUSDT","localSymbol":"OOKIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"OOKI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"OPBNB":{"symbol":"OPBNB","localSymbol":"OPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"OP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"OPBTC":{"symbol":"OPBTC","localSymbol":"OPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"OP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"OPBUSD":{"symbol":"OPBUSD","localSymbol":"OPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"OP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"OPETH":{"symbol":"OPETH","localSymbol":"OPETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"OP","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":913205152.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"OPEUR":{"symbol":"OPEUR","localSymbol":"OPEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"OP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"OPUSDT":{"symbol":"OPUSDT","localSymbol":"OPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"OP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ORNBTC":{"symbol":"ORNBTC","localSymbol":"ORNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ORN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ORNBUSD":{"symbol":"ORNBUSD","localSymbol":"ORNBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ORN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ORNUSDT":{"symbol":"ORNUSDT","localSymbol":"ORNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ORN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"OSMOBTC":{"symbol":"OSMOBTC","localSymbol":"OSMOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"OSMO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":8384883677.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":10.00000000,"tickSize":0.00000001},"OSMOBUSD":{"symbol":"OSMOBUSD","localSymbol":"OSMOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"OSMO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"OSMOUSDT":{"symbol":"OSMOUSDT","localSymbol":"OSMOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"OSMO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"OSTBNB":{"symbol":"OSTBNB","localSymbol":"OSTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"OST","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"OSTBTC":{"symbol":"OSTBTC","localSymbol":"OSTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"OST","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"OSTETH":{"symbol":"OSTETH","localSymbol":"OSTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"OST","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"OXTBTC":{"symbol":"OXTBTC","localSymbol":"OXTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"OXT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"OXTBUSD":{"symbol":"OXTBUSD","localSymbol":"OXTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"OXT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":100000.00000000,"tickSize":0.00010000},"OXTUSDT":{"symbol":"OXTUSDT","localSymbol":"OXTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"OXT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PAXBNB":{"symbol":"PAXBNB","localSymbol":"PAXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"PAX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"PAXBTC":{"symbol":"PAXBTC","localSymbol":"PAXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PAX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PAXBUSD":{"symbol":"PAXBUSD","localSymbol":"PAXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.69990000,"maxPrice":1.29980000,"tickSize":0.00010000},"PAXETH":{"symbol":"PAXETH","localSymbol":"PAXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"PAX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PAXGBNB":{"symbol":"PAXGBNB","localSymbol":"PAXGBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"PAXG","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00010000,"maxQuantity":900000.00000000,"stepSize":0.00010000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"PAXGBTC":{"symbol":"PAXGBTC","localSymbol":"PAXGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PAXG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00010000,"maxQuantity":9222449.00000000,"stepSize":0.00010000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"PAXGBUSD":{"symbol":"PAXGBUSD","localSymbol":"PAXGBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PAXG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":9000.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"PAXGUSDT":{"symbol":"PAXGUSDT","localSymbol":"PAXGUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PAXG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":9000.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"PAXTUSD":{"symbol":"PAXTUSD","localSymbol":"PAXTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"PAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PAXUSDT":{"symbol":"PAXUSDT","localSymbol":"PAXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.70000000,"maxPrice":1.30000000,"tickSize":0.00010000},"PEOPLEBNB":{"symbol":"PEOPLEBNB","localSymbol":"PEOPLEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"PEOPLE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PEOPLEBTC":{"symbol":"PEOPLEBTC","localSymbol":"PEOPLEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PEOPLE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PEOPLEBUSD":{"symbol":"PEOPLEBUSD","localSymbol":"PEOPLEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PEOPLE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"PEOPLEETH":{"symbol":"PEOPLEETH","localSymbol":"PEOPLEETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"PEOPLE","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PEOPLEUSDT":{"symbol":"PEOPLEUSDT","localSymbol":"PEOPLEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PEOPLE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"PERLBNB":{"symbol":"PERLBNB","localSymbol":"PERLBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"PERL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"PERLBTC":{"symbol":"PERLBTC","localSymbol":"PERLBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PERL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PERLUSDC":{"symbol":"PERLUSDC","localSymbol":"PERLUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"PERL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"PERLUSDT":{"symbol":"PERLUSDT","localSymbol":"PERLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PERL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"PERPBTC":{"symbol":"PERPBTC","localSymbol":"PERPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PERP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PERPBUSD":{"symbol":"PERPBUSD","localSymbol":"PERPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PERP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"PERPUSDT":{"symbol":"PERPUSDT","localSymbol":"PERPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PERP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"PHABTC":{"symbol":"PHABTC","localSymbol":"PHABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PHA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PHABUSD":{"symbol":"PHABUSD","localSymbol":"PHABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PHA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PHAUSDT":{"symbol":"PHAUSDT","localSymbol":"PHAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PHA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PHBBNB":{"symbol":"PHBBNB","localSymbol":"PHBBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"PHB","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"PHBBTC":{"symbol":"PHBBTC","localSymbol":"PHBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PHB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PHBBUSD":{"symbol":"PHBBUSD","localSymbol":"PHBBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PHB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PHBPAX":{"symbol":"PHBPAX","localSymbol":"PHBPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"PHB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"PHBTUSD":{"symbol":"PHBTUSD","localSymbol":"PHBTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"PHB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"PHBUSDC":{"symbol":"PHBUSDC","localSymbol":"PHBUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"PHB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"PHBUSDT":{"symbol":"PHBUSDT","localSymbol":"PHBUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PHB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PHXBNB":{"symbol":"PHXBNB","localSymbol":"PHXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"PHX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"PHXBTC":{"symbol":"PHXBTC","localSymbol":"PHXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PHX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PHXETH":{"symbol":"PHXETH","localSymbol":"PHXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"PHX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PIVXBNB":{"symbol":"PIVXBNB","localSymbol":"PIVXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"PIVX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"PIVXBTC":{"symbol":"PIVXBTC","localSymbol":"PIVXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PIVX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PLABNB":{"symbol":"PLABNB","localSymbol":"PLABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"PLA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"PLABTC":{"symbol":"PLABTC","localSymbol":"PLABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PLA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PLABUSD":{"symbol":"PLABUSD","localSymbol":"PLABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PLA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"PLAUSDT":{"symbol":"PLAUSDT","localSymbol":"PLAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PLA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"PNTBTC":{"symbol":"PNTBTC","localSymbol":"PNTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PNT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PNTUSDT":{"symbol":"PNTUSDT","localSymbol":"PNTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PNT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"POABNB":{"symbol":"POABNB","localSymbol":"POABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"POA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"POABTC":{"symbol":"POABTC","localSymbol":"POABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"POA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"POAETH":{"symbol":"POAETH","localSymbol":"POAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"POA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"POEBTC":{"symbol":"POEBTC","localSymbol":"POEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"POE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"POEETH":{"symbol":"POEETH","localSymbol":"POEETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"POE","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"POLSBNB":{"symbol":"POLSBNB","localSymbol":"POLSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"POLS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"POLSBTC":{"symbol":"POLSBTC","localSymbol":"POLSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"POLS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"POLSBUSD":{"symbol":"POLSBUSD","localSymbol":"POLSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"POLS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"POLSUSDT":{"symbol":"POLSUSDT","localSymbol":"POLSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"POLS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"POLYBNB":{"symbol":"POLYBNB","localSymbol":"POLYBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"POLY","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"POLYBTC":{"symbol":"POLYBTC","localSymbol":"POLYBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"POLY","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"POLYBUSD":{"symbol":"POLYBUSD","localSymbol":"POLYBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"POLY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"POLYUSDT":{"symbol":"POLYUSDT","localSymbol":"POLYUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"POLY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":913205152.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":100.00000000,"tickSize":0.00010000},"POLYXBTC":{"symbol":"POLYXBTC","localSymbol":"POLYXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"POLYX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"POLYXBUSD":{"symbol":"POLYXBUSD","localSymbol":"POLYXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"POLYX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"POLYXUSDT":{"symbol":"POLYXUSDT","localSymbol":"POLYXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"POLYX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PONDBTC":{"symbol":"PONDBTC","localSymbol":"PONDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"POND","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PONDBUSD":{"symbol":"PONDBUSD","localSymbol":"PONDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"POND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"PONDUSDT":{"symbol":"PONDUSDT","localSymbol":"PONDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"POND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"PORTOBTC":{"symbol":"PORTOBTC","localSymbol":"PORTOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PORTO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PORTOBUSD":{"symbol":"PORTOBUSD","localSymbol":"PORTOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PORTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PORTOEUR":{"symbol":"PORTOEUR","localSymbol":"PORTOEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"PORTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PORTOTRY":{"symbol":"PORTOTRY","localSymbol":"PORTOTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"PORTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"PORTOUSDT":{"symbol":"PORTOUSDT","localSymbol":"PORTOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PORTO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"POWRBNB":{"symbol":"POWRBNB","localSymbol":"POWRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"POWR","minNotional":0.10000000,"minAmount":0.10000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"POWRBTC":{"symbol":"POWRBTC","localSymbol":"POWRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"POWR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"POWRBUSD":{"symbol":"POWRBUSD","localSymbol":"POWRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"POWR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"POWRETH":{"symbol":"POWRETH","localSymbol":"POWRETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"POWR","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"POWRUSDT":{"symbol":"POWRUSDT","localSymbol":"POWRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"POWR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PPTBTC":{"symbol":"PPTBTC","localSymbol":"PPTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PPT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"PPTETH":{"symbol":"PPTETH","localSymbol":"PPTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"PPT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"PROMBNB":{"symbol":"PROMBNB","localSymbol":"PROMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"PROM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"PROMBTC":{"symbol":"PROMBTC","localSymbol":"PROMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PROM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"PROMBUSD":{"symbol":"PROMBUSD","localSymbol":"PROMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PROM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"PROSBUSD":{"symbol":"PROSBUSD","localSymbol":"PROSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PROS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"PROSETH":{"symbol":"PROSETH","localSymbol":"PROSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"PROS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"PSGBTC":{"symbol":"PSGBTC","localSymbol":"PSGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PSG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"PSGBUSD":{"symbol":"PSGBUSD","localSymbol":"PSGBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PSG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"PSGUSDT":{"symbol":"PSGUSDT","localSymbol":"PSGUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PSG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"PUNDIXBUSD":{"symbol":"PUNDIXBUSD","localSymbol":"PUNDIXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PUNDIX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"PUNDIXETH":{"symbol":"PUNDIXETH","localSymbol":"PUNDIXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"PUNDIX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"PUNDIXUSDT":{"symbol":"PUNDIXUSDT","localSymbol":"PUNDIXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PUNDIX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"PYRBTC":{"symbol":"PYRBTC","localSymbol":"PYRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"PYR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"PYRBUSD":{"symbol":"PYRBUSD","localSymbol":"PYRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"PYR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"PYRUSDT":{"symbol":"PYRUSDT","localSymbol":"PYRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"PYR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"QIBNB":{"symbol":"QIBNB","localSymbol":"QIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"QI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"QIBTC":{"symbol":"QIBTC","localSymbol":"QIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"QI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"QIBUSD":{"symbol":"QIBUSD","localSymbol":"QIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"QI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"QIUSDT":{"symbol":"QIUSDT","localSymbol":"QIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"QI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"QKCBTC":{"symbol":"QKCBTC","localSymbol":"QKCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"QKC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"QKCBUSD":{"symbol":"QKCBUSD","localSymbol":"QKCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"QKC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"QKCETH":{"symbol":"QKCETH","localSymbol":"QKCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"QKC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"QLCBNB":{"symbol":"QLCBNB","localSymbol":"QLCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"QLC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"QLCBTC":{"symbol":"QLCBTC","localSymbol":"QLCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"QLC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"QLCETH":{"symbol":"QLCETH","localSymbol":"QLCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"QLC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"QNTBNB":{"symbol":"QNTBNB","localSymbol":"QNTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"QNT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"QNTBTC":{"symbol":"QNTBTC","localSymbol":"QNTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"QNT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"QNTBUSD":{"symbol":"QNTBUSD","localSymbol":"QNTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"QNT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"QNTUSDT":{"symbol":"QNTUSDT","localSymbol":"QNTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"QNT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"QSPBNB":{"symbol":"QSPBNB","localSymbol":"QSPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"QSP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"QSPBTC":{"symbol":"QSPBTC","localSymbol":"QSPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"QSP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"QSPETH":{"symbol":"QSPETH","localSymbol":"QSPETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"QSP","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"QTUMBNB":{"symbol":"QTUMBNB","localSymbol":"QTUMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"QTUM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"QTUMBTC":{"symbol":"QTUMBTC","localSymbol":"QTUMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"QTUM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"QTUMBUSD":{"symbol":"QTUMBUSD","localSymbol":"QTUMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"QTUM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"QTUMETH":{"symbol":"QTUMETH","localSymbol":"QTUMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"QTUM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"QTUMUSDT":{"symbol":"QTUMUSDT","localSymbol":"QTUMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"QTUM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"QUICKBNB":{"symbol":"QUICKBNB","localSymbol":"QUICKBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"QUICK","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"QUICKBTC":{"symbol":"QUICKBTC","localSymbol":"QUICKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"QUICK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"QUICKBUSD":{"symbol":"QUICKBUSD","localSymbol":"QUICKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"QUICK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"QUICKUSDT":{"symbol":"QUICKUSDT","localSymbol":"QUICKUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"QUICK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"RADBNB":{"symbol":"RADBNB","localSymbol":"RADBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"RAD","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"RADBTC":{"symbol":"RADBTC","localSymbol":"RADBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RAD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"RADBUSD":{"symbol":"RADBUSD","localSymbol":"RADBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"RAD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"RADUSDT":{"symbol":"RADUSDT","localSymbol":"RADUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RAD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"RAMPBTC":{"symbol":"RAMPBTC","localSymbol":"RAMPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RAMP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RAMPBUSD":{"symbol":"RAMPBUSD","localSymbol":"RAMPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"RAMP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"RAMPUSDT":{"symbol":"RAMPUSDT","localSymbol":"RAMPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RAMP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"RAREBNB":{"symbol":"RAREBNB","localSymbol":"RAREBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"RARE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"RAREBTC":{"symbol":"RAREBTC","localSymbol":"RAREBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RARE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RAREBUSD":{"symbol":"RAREBUSD","localSymbol":"RAREBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"RARE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"RAREUSDT":{"symbol":"RAREUSDT","localSymbol":"RAREUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RARE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"RAYBNB":{"symbol":"RAYBNB","localSymbol":"RAYBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"RAY","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"RAYBUSD":{"symbol":"RAYBUSD","localSymbol":"RAYBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"RAY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"RAYUSDT":{"symbol":"RAYUSDT","localSymbol":"RAYUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RAY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"RCNBNB":{"symbol":"RCNBNB","localSymbol":"RCNBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"RCN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"RCNBTC":{"symbol":"RCNBTC","localSymbol":"RCNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RCN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RCNETH":{"symbol":"RCNETH","localSymbol":"RCNETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"RCN","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RDNBNB":{"symbol":"RDNBNB","localSymbol":"RDNBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"RDN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"RDNBTC":{"symbol":"RDNBTC","localSymbol":"RDNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RDN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RDNETH":{"symbol":"RDNETH","localSymbol":"RDNETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"RDN","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"REEFBIDR":{"symbol":"REEFBIDR","localSymbol":"REEFBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"REEF","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"REEFBTC":{"symbol":"REEFBTC","localSymbol":"REEFBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"REEF","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"REEFBUSD":{"symbol":"REEFBUSD","localSymbol":"REEFBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"REEF","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"REEFTRY":{"symbol":"REEFTRY","localSymbol":"REEFTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"REEF","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"REEFUSDT":{"symbol":"REEFUSDT","localSymbol":"REEFUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"REEF","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"REIBNB":{"symbol":"REIBNB","localSymbol":"REIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"REI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"REIBUSD":{"symbol":"REIBUSD","localSymbol":"REIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"REI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"REIETH":{"symbol":"REIETH","localSymbol":"REIETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"REI","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"REIUSDT":{"symbol":"REIUSDT","localSymbol":"REIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"REI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"RENBNB":{"symbol":"RENBNB","localSymbol":"RENBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"REN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"RENBTC":{"symbol":"RENBTC","localSymbol":"RENBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"REN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RENBTCBTC":{"symbol":"RENBTCBTC","localSymbol":"RENBTCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RENBTC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00001000,"maxQuantity":92141578.00000000,"stepSize":0.00001000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"RENBTCETH":{"symbol":"RENBTCETH","localSymbol":"RENBTCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"RENBTC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00001000,"maxQuantity":100000.00000000,"stepSize":0.00001000,"minPrice":0.00100000,"maxPrice":100000.00000000,"tickSize":0.00100000},"RENBUSD":{"symbol":"RENBUSD","localSymbol":"RENBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"REN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"RENUSDT":{"symbol":"RENUSDT","localSymbol":"RENUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"REN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"REPBNB":{"symbol":"REPBNB","localSymbol":"REPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"REP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"REPBTC":{"symbol":"REPBTC","localSymbol":"REPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"REP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"REPBUSD":{"symbol":"REPBUSD","localSymbol":"REPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"REP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"REPUSDT":{"symbol":"REPUSDT","localSymbol":"REPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"REP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"REQBTC":{"symbol":"REQBTC","localSymbol":"REQBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"REQ","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"REQBUSD":{"symbol":"REQBUSD","localSymbol":"REQBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"REQ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"REQETH":{"symbol":"REQETH","localSymbol":"REQETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"REQ","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"REQUSDT":{"symbol":"REQUSDT","localSymbol":"REQUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"REQ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"RGTBNB":{"symbol":"RGTBNB","localSymbol":"RGTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"RGT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"RGTBTC":{"symbol":"RGTBTC","localSymbol":"RGTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RGT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"RGTBUSD":{"symbol":"RGTBUSD","localSymbol":"RGTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"RGT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"RGTUSDT":{"symbol":"RGTUSDT","localSymbol":"RGTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RGT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"RIFBTC":{"symbol":"RIFBTC","localSymbol":"RIFBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RIF","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RIFUSDT":{"symbol":"RIFUSDT","localSymbol":"RIFUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RIF","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"RLCBNB":{"symbol":"RLCBNB","localSymbol":"RLCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"RLC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"RLCBTC":{"symbol":"RLCBTC","localSymbol":"RLCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RLC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RLCBUSD":{"symbol":"RLCBUSD","localSymbol":"RLCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"RLC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"RLCETH":{"symbol":"RLCETH","localSymbol":"RLCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"RLC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"RLCUSDT":{"symbol":"RLCUSDT","localSymbol":"RLCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RLC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"RNDRBTC":{"symbol":"RNDRBTC","localSymbol":"RNDRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RNDR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"RNDRBUSD":{"symbol":"RNDRBUSD","localSymbol":"RNDRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"RNDR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"RNDRUSDT":{"symbol":"RNDRUSDT","localSymbol":"RNDRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RNDR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ROSEBNB":{"symbol":"ROSEBNB","localSymbol":"ROSEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ROSE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ROSEBTC":{"symbol":"ROSEBTC","localSymbol":"ROSEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ROSE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ROSEBUSD":{"symbol":"ROSEBUSD","localSymbol":"ROSEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ROSE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ROSEETH":{"symbol":"ROSEETH","localSymbol":"ROSEETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ROSE","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ROSETRY":{"symbol":"ROSETRY","localSymbol":"ROSETRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ROSE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"ROSEUSDT":{"symbol":"ROSEUSDT","localSymbol":"ROSEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ROSE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"RPXBNB":{"symbol":"RPXBNB","localSymbol":"RPXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"RPX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"RPXBTC":{"symbol":"RPXBTC","localSymbol":"RPXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RPX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RPXETH":{"symbol":"RPXETH","localSymbol":"RPXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"RPX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RSRBNB":{"symbol":"RSRBNB","localSymbol":"RSRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"RSR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"RSRBTC":{"symbol":"RSRBTC","localSymbol":"RSRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RSR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RSRBUSD":{"symbol":"RSRBUSD","localSymbol":"RSRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"RSR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"RSRUSDT":{"symbol":"RSRUSDT","localSymbol":"RSRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RSR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"RUNEAUD":{"symbol":"RUNEAUD","localSymbol":"RUNEAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"RUNE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00000100,"maxQuantity":9222449.00000000,"stepSize":0.00000100,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"RUNEBNB":{"symbol":"RUNEBNB","localSymbol":"RUNEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"RUNE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"RUNEBTC":{"symbol":"RUNEBTC","localSymbol":"RUNEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RUNE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"RUNEBUSD":{"symbol":"RUNEBUSD","localSymbol":"RUNEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"RUNE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"RUNEETH":{"symbol":"RUNEETH","localSymbol":"RUNEETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"RUNE","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"RUNEEUR":{"symbol":"RUNEEUR","localSymbol":"RUNEEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"RUNE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"RUNEGBP":{"symbol":"RUNEGBP","localSymbol":"RUNEGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"RUNE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"RUNETRY":{"symbol":"RUNETRY","localSymbol":"RUNETRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"RUNE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"RUNEUSDT":{"symbol":"RUNEUSDT","localSymbol":"RUNEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RUNE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"RVNBTC":{"symbol":"RVNBTC","localSymbol":"RVNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"RVN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"RVNBUSD":{"symbol":"RVNBUSD","localSymbol":"RVNBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"RVN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"RVNTRY":{"symbol":"RVNTRY","localSymbol":"RVNTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"RVN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"RVNUSDT":{"symbol":"RVNUSDT","localSymbol":"RVNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"RVN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SALTBTC":{"symbol":"SALTBTC","localSymbol":"SALTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SALT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SALTETH":{"symbol":"SALTETH","localSymbol":"SALTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SALT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"SANDAUD":{"symbol":"SANDAUD","localSymbol":"SANDAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"SAND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"SANDBIDR":{"symbol":"SANDBIDR","localSymbol":"SANDBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"SAND","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.01000000,"maxQuantity":9223371110.00000000,"stepSize":0.01000000,"minPrice":1.00000000,"maxPrice":10000000.00000000,"tickSize":1.00000000},"SANDBNB":{"symbol":"SANDBNB","localSymbol":"SANDBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SAND","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"SANDBRL":{"symbol":"SANDBRL","localSymbol":"SANDBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"SAND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"SANDBTC":{"symbol":"SANDBTC","localSymbol":"SANDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SAND","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SANDBUSD":{"symbol":"SANDBUSD","localSymbol":"SANDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SAND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"SANDETH":{"symbol":"SANDETH","localSymbol":"SANDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SAND","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SANDTRY":{"symbol":"SANDTRY","localSymbol":"SANDTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"SAND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"SANDUSDT":{"symbol":"SANDUSDT","localSymbol":"SANDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SAND","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"SANTOSBRL":{"symbol":"SANTOSBRL","localSymbol":"SANTOSBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"SANTOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"SANTOSBTC":{"symbol":"SANTOSBTC","localSymbol":"SANTOSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SANTOS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SANTOSBUSD":{"symbol":"SANTOSBUSD","localSymbol":"SANTOSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SANTOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"SANTOSTRY":{"symbol":"SANTOSTRY","localSymbol":"SANTOSTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"SANTOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"SANTOSUSDT":{"symbol":"SANTOSUSDT","localSymbol":"SANTOSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SANTOS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"SCBTC":{"symbol":"SCBTC","localSymbol":"SCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SCBUSD":{"symbol":"SCBUSD","localSymbol":"SCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"SCETH":{"symbol":"SCETH","localSymbol":"SCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SCRTBTC":{"symbol":"SCRTBTC","localSymbol":"SCRTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SCRT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SCRTBUSD":{"symbol":"SCRTBUSD","localSymbol":"SCRTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SCRT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"SCRTETH":{"symbol":"SCRTETH","localSymbol":"SCRTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SCRT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SCRTUSDT":{"symbol":"SCRTUSDT","localSymbol":"SCRTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SCRT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"SCUSDT":{"symbol":"SCUSDT","localSymbol":"SCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SFPBTC":{"symbol":"SFPBTC","localSymbol":"SFPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SFP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SFPBUSD":{"symbol":"SFPBUSD","localSymbol":"SFPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SFP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"SFPUSDT":{"symbol":"SFPUSDT","localSymbol":"SFPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SFP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"SHIBAUD":{"symbol":"SHIBAUD","localSymbol":"SHIBAUD","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"AUD","baseCurrency":"SHIB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"SHIBBRL":{"symbol":"SHIBBRL","localSymbol":"SHIBBRL","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"BRL","baseCurrency":"SHIB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"SHIBBUSD":{"symbol":"SHIBBUSD","localSymbol":"SHIBBUSD","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"BUSD","baseCurrency":"SHIB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"SHIBDOGE":{"symbol":"SHIBDOGE","localSymbol":"SHIBDOGE","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"DOGE","baseCurrency":"SHIB","minNotional":30.00000000,"minAmount":30.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1.00000000,"tickSize":0.00000010},"SHIBEUR":{"symbol":"SHIBEUR","localSymbol":"SHIBEUR","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"EUR","baseCurrency":"SHIB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"SHIBGBP":{"symbol":"SHIBGBP","localSymbol":"SHIBGBP","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"GBP","baseCurrency":"SHIB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"SHIBRUB":{"symbol":"SHIBRUB","localSymbol":"SHIBRUB","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"RUB","baseCurrency":"SHIB","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1.00000000,"tickSize":0.00000010},"SHIBTRY":{"symbol":"SHIBTRY","localSymbol":"SHIBTRY","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"TRY","baseCurrency":"SHIB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"SHIBUAH":{"symbol":"SHIBUAH","localSymbol":"SHIBUAH","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"UAH","baseCurrency":"SHIB","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"SHIBUSDT":{"symbol":"SHIBUSDT","localSymbol":"SHIBUSDT","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"USDT","baseCurrency":"SHIB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"SKLBTC":{"symbol":"SKLBTC","localSymbol":"SKLBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SKL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SKLBUSD":{"symbol":"SKLBUSD","localSymbol":"SKLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SKL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SKLUSDT":{"symbol":"SKLUSDT","localSymbol":"SKLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SKL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SKYBNB":{"symbol":"SKYBNB","localSymbol":"SKYBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SKY","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SKYBTC":{"symbol":"SKYBTC","localSymbol":"SKYBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SKY","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SKYETH":{"symbol":"SKYETH","localSymbol":"SKYETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SKY","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":9000000.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SLPBIDR":{"symbol":"SLPBIDR","localSymbol":"SLPBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"SLP","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":1.00000000,"maxQuantity":9223362229.00000000,"stepSize":1.00000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"SLPBNB":{"symbol":"SLPBNB","localSymbol":"SLPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SLP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SLPBUSD":{"symbol":"SLPBUSD","localSymbol":"SLPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SLP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SLPETH":{"symbol":"SLPETH","localSymbol":"SLPETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SLP","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SLPTRY":{"symbol":"SLPTRY","localSymbol":"SLPTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"SLP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"SLPUSDT":{"symbol":"SLPUSDT","localSymbol":"SLPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SLP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SNGLSBTC":{"symbol":"SNGLSBTC","localSymbol":"SNGLSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SNGLS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SNGLSETH":{"symbol":"SNGLSETH","localSymbol":"SNGLSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SNGLS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SNMBTC":{"symbol":"SNMBTC","localSymbol":"SNMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SNM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SNMBUSD":{"symbol":"SNMBUSD","localSymbol":"SNMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SNM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"SNMETH":{"symbol":"SNMETH","localSymbol":"SNMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SNM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SNTBTC":{"symbol":"SNTBTC","localSymbol":"SNTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SNT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SNTBUSD":{"symbol":"SNTBUSD","localSymbol":"SNTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SNT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SNTETH":{"symbol":"SNTETH","localSymbol":"SNTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SNT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SNXBNB":{"symbol":"SNXBNB","localSymbol":"SNXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SNX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SNXBTC":{"symbol":"SNXBTC","localSymbol":"SNXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SNX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SNXBUSD":{"symbol":"SNXBUSD","localSymbol":"SNXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SNX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"SNXETH":{"symbol":"SNXETH","localSymbol":"SNXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SNX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"SNXUSDT":{"symbol":"SNXUSDT","localSymbol":"SNXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SNX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"SOLAUD":{"symbol":"SOLAUD","localSymbol":"SOLAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"SOL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":922327.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"SOLBIDR":{"symbol":"SOLBIDR","localSymbol":"SOLBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"SOL","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.00010000,"maxQuantity":9223372.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":9999998955.00000000,"tickSize":1.00000000},"SOLBNB":{"symbol":"SOLBNB","localSymbol":"SOLBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SOL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"SOLBRL":{"symbol":"SOLBRL","localSymbol":"SOLBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"SOL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":10000.00000000,"tickSize":0.10000000},"SOLBTC":{"symbol":"SOLBTC","localSymbol":"SOLBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SOL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SOLBUSD":{"symbol":"SOLBUSD","localSymbol":"SOLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SOL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"SOLETH":{"symbol":"SOLETH","localSymbol":"SOLETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SOL","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SOLEUR":{"symbol":"SOLEUR","localSymbol":"SOLEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"SOL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222440.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"SOLGBP":{"symbol":"SOLGBP","localSymbol":"SOLGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"SOL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"SOLRUB":{"symbol":"SOLRUB","localSymbol":"SOLRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"SOL","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.01000000,"maxQuantity":92233.00000000,"stepSize":0.01000000,"minPrice":1.00000000,"maxPrice":999996.00000000,"tickSize":1.00000000},"SOLTRY":{"symbol":"SOLTRY","localSymbol":"SOLTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"SOL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":3074353.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":30000.00000000,"tickSize":0.10000000},"SOLUSDC":{"symbol":"SOLUSDC","localSymbol":"SOLUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"SOL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"SOLUSDT":{"symbol":"SOLUSDT","localSymbol":"SOLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SOL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"SPARTABNB":{"symbol":"SPARTABNB","localSymbol":"SPARTABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SPARTA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SPELLBNB":{"symbol":"SPELLBNB","localSymbol":"SPELLBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SPELL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SPELLBTC":{"symbol":"SPELLBTC","localSymbol":"SPELLBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SPELL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SPELLBUSD":{"symbol":"SPELLBUSD","localSymbol":"SPELLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SPELL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SPELLTRY":{"symbol":"SPELLTRY","localSymbol":"SPELLTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"SPELL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SPELLUSDT":{"symbol":"SPELLUSDT","localSymbol":"SPELLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SPELL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SRMBIDR":{"symbol":"SRMBIDR","localSymbol":"SRMBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"SRM","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.00100000,"maxQuantity":100000.00000000,"stepSize":0.00100000,"minPrice":1.00000000,"maxPrice":50000000.00000000,"tickSize":1.00000000},"SRMBNB":{"symbol":"SRMBNB","localSymbol":"SRMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SRM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SRMBTC":{"symbol":"SRMBTC","localSymbol":"SRMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SRM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SRMBUSD":{"symbol":"SRMBUSD","localSymbol":"SRMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SRM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SRMUSDT":{"symbol":"SRMUSDT","localSymbol":"SRMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SRM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SSVBTC":{"symbol":"SSVBTC","localSymbol":"SSVBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SSV","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SSVBUSD":{"symbol":"SSVBUSD","localSymbol":"SSVBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SSV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":9222449.00000000,"stepSize":0.00100000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"SSVETH":{"symbol":"SSVETH","localSymbol":"SSVETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SSV","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"STEEMBNB":{"symbol":"STEEMBNB","localSymbol":"STEEMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"STEEM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"STEEMBTC":{"symbol":"STEEMBTC","localSymbol":"STEEMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"STEEM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"STEEMBUSD":{"symbol":"STEEMBUSD","localSymbol":"STEEMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"STEEM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"STEEMETH":{"symbol":"STEEMETH","localSymbol":"STEEMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"STEEM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"STEEMUSDT":{"symbol":"STEEMUSDT","localSymbol":"STEEMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"STEEM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"STGBTC":{"symbol":"STGBTC","localSymbol":"STGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"STG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":913205152.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":100.00000000,"tickSize":0.00000001},"STGBUSD":{"symbol":"STGBUSD","localSymbol":"STGBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"STG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":913205152.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":100.00000000,"tickSize":0.00010000},"STGUSDT":{"symbol":"STGUSDT","localSymbol":"STGUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"STG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":913205152.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":100.00000000,"tickSize":0.00010000},"STMXBTC":{"symbol":"STMXBTC","localSymbol":"STMXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"STMX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"STMXBUSD":{"symbol":"STMXBUSD","localSymbol":"STMXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"STMX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":10000.00000000,"tickSize":0.00000100},"STMXETH":{"symbol":"STMXETH","localSymbol":"STMXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"STMX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"STMXUSDT":{"symbol":"STMXUSDT","localSymbol":"STMXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"STMX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"STORJBTC":{"symbol":"STORJBTC","localSymbol":"STORJBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"STORJ","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"STORJBUSD":{"symbol":"STORJBUSD","localSymbol":"STORJBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"STORJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222448.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"STORJETH":{"symbol":"STORJETH","localSymbol":"STORJETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"STORJ","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"STORJTRY":{"symbol":"STORJTRY","localSymbol":"STORJTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"STORJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"STORJUSDT":{"symbol":"STORJUSDT","localSymbol":"STORJUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"STORJ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"STORMBNB":{"symbol":"STORMBNB","localSymbol":"STORMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"STORM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":10000.00000000,"tickSize":0.00000010},"STORMBTC":{"symbol":"STORMBTC","localSymbol":"STORMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"STORM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"STORMETH":{"symbol":"STORMETH","localSymbol":"STORMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"STORM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"STORMUSDT":{"symbol":"STORMUSDT","localSymbol":"STORMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"STORM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"STPTBNB":{"symbol":"STPTBNB","localSymbol":"STPTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"STPT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"STPTBTC":{"symbol":"STPTBTC","localSymbol":"STPTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"STPT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"STPTBUSD":{"symbol":"STPTBUSD","localSymbol":"STPTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"STPT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"STPTUSDT":{"symbol":"STPTUSDT","localSymbol":"STPTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"STPT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"STRATBNB":{"symbol":"STRATBNB","localSymbol":"STRATBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"STRAT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"STRATBTC":{"symbol":"STRATBTC","localSymbol":"STRATBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"STRAT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"STRATBUSD":{"symbol":"STRATBUSD","localSymbol":"STRATBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"STRAT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"STRATETH":{"symbol":"STRATETH","localSymbol":"STRATETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"STRAT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"STRATUSDT":{"symbol":"STRATUSDT","localSymbol":"STRATUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"STRAT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"STRAXBTC":{"symbol":"STRAXBTC","localSymbol":"STRAXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"STRAX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"STRAXBUSD":{"symbol":"STRAXBUSD","localSymbol":"STRAXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"STRAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"STRAXETH":{"symbol":"STRAXETH","localSymbol":"STRAXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"STRAX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"STRAXUSDT":{"symbol":"STRAXUSDT","localSymbol":"STRAXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"STRAX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"STXBNB":{"symbol":"STXBNB","localSymbol":"STXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"STX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"STXBTC":{"symbol":"STXBTC","localSymbol":"STXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"STX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"STXBUSD":{"symbol":"STXBUSD","localSymbol":"STXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"STX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"STXUSDT":{"symbol":"STXUSDT","localSymbol":"STXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"STX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"SUBBTC":{"symbol":"SUBBTC","localSymbol":"SUBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SUB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SUBETH":{"symbol":"SUBETH","localSymbol":"SUBETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SUB","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SUNBTC":{"symbol":"SUNBTC","localSymbol":"SUNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SUN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"SUNBUSD":{"symbol":"SUNBUSD","localSymbol":"SUNBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SUN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SUNUSDT":{"symbol":"SUNUSDT","localSymbol":"SUNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SUN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SUPERBTC":{"symbol":"SUPERBTC","localSymbol":"SUPERBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SUPER","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SUPERBUSD":{"symbol":"SUPERBUSD","localSymbol":"SUPERBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SUPER","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"SUPERUSDT":{"symbol":"SUPERUSDT","localSymbol":"SUPERUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SUPER","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"SUSDBTC":{"symbol":"SUSDBTC","localSymbol":"SUSDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SUSD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SUSDETH":{"symbol":"SUSDETH","localSymbol":"SUSDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SUSD","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SUSDUSDT":{"symbol":"SUSDUSDT","localSymbol":"SUSDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SUSD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"SUSHIBNB":{"symbol":"SUSHIBNB","localSymbol":"SUSHIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SUSHI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"SUSHIBTC":{"symbol":"SUSHIBTC","localSymbol":"SUSHIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SUSHI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"SUSHIBUSD":{"symbol":"SUSHIBUSD","localSymbol":"SUSHIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SUSHI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"SUSHIDOWNUSDT":{"symbol":"SUSHIDOWNUSDT","localSymbol":"SUSHIDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SUSHIDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":19998638.00000000,"stepSize":0.01000000,"minPrice":3.78000000,"maxPrice":71.80200000,"tickSize":0.00100000},"SUSHIUPUSDT":{"symbol":"SUSHIUPUSDT","localSymbol":"SUSHIUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SUSHIUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":150000.00000000,"stepSize":0.01000000,"minPrice":0.00047000,"maxPrice":0.00892199,"tickSize":0.00000100},"SUSHIUSDT":{"symbol":"SUSHIUSDT","localSymbol":"SUSHIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SUSHI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"SWRVBNB":{"symbol":"SWRVBNB","localSymbol":"SWRVBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SWRV","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"SWRVBUSD":{"symbol":"SWRVBUSD","localSymbol":"SWRVBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SWRV","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"SXPAUD":{"symbol":"SXPAUD","localSymbol":"SXPAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"SXP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"SXPBIDR":{"symbol":"SXPBIDR","localSymbol":"SXPBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"SXP","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.10000000,"maxQuantity":1000000.00000000,"stepSize":0.10000000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"SXPBNB":{"symbol":"SXPBNB","localSymbol":"SXPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SXP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"SXPBTC":{"symbol":"SXPBTC","localSymbol":"SXPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SXP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SXPBUSD":{"symbol":"SXPBUSD","localSymbol":"SXPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SXP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"SXPDOWNUSDT":{"symbol":"SXPDOWNUSDT","localSymbol":"SXPDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SXPDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":100000000.00000000,"stepSize":0.01000000,"minPrice":0.00897000,"maxPrice":0.17035000,"tickSize":0.00001000},"SXPEUR":{"symbol":"SXPEUR","localSymbol":"SXPEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"SXP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"SXPGBP":{"symbol":"SXPGBP","localSymbol":"SXPGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"SXP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"SXPTRY":{"symbol":"SXPTRY","localSymbol":"SXPTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"SXP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":100000.00000000,"tickSize":0.00100000},"SXPUPUSDT":{"symbol":"SXPUPUSDT","localSymbol":"SXPUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SXPUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.01745000,"maxPrice":0.33141000,"tickSize":0.00001000},"SXPUSDT":{"symbol":"SXPUSDT","localSymbol":"SXPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SXP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"SYSBNB":{"symbol":"SYSBNB","localSymbol":"SYSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"SYS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"SYSBTC":{"symbol":"SYSBTC","localSymbol":"SYSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"SYS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SYSBUSD":{"symbol":"SYSBUSD","localSymbol":"SYSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"SYS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"SYSETH":{"symbol":"SYSETH","localSymbol":"SYSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"SYS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"SYSUSDT":{"symbol":"SYSUSDT","localSymbol":"SYSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"SYS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TBUSD":{"symbol":"TBUSD","localSymbol":"TBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"T","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TCTBNB":{"symbol":"TCTBNB","localSymbol":"TCTBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TCT","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"TCTBTC":{"symbol":"TCTBTC","localSymbol":"TCTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TCT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TCTUSDT":{"symbol":"TCTUSDT","localSymbol":"TCTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TCT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TFUELBNB":{"symbol":"TFUELBNB","localSymbol":"TFUELBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TFUEL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"TFUELBTC":{"symbol":"TFUELBTC","localSymbol":"TFUELBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TFUEL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TFUELBUSD":{"symbol":"TFUELBUSD","localSymbol":"TFUELBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TFUEL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TFUELPAX":{"symbol":"TFUELPAX","localSymbol":"TFUELPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"TFUEL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"TFUELTUSD":{"symbol":"TFUELTUSD","localSymbol":"TFUELTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"TFUEL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"TFUELUSDC":{"symbol":"TFUELUSDC","localSymbol":"TFUELUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"TFUEL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"TFUELUSDT":{"symbol":"TFUELUSDT","localSymbol":"TFUELUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TFUEL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"THETABNB":{"symbol":"THETABNB","localSymbol":"THETABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"THETA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"THETABTC":{"symbol":"THETABTC","localSymbol":"THETABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"THETA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"THETABUSD":{"symbol":"THETABUSD","localSymbol":"THETABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"THETA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"THETAETH":{"symbol":"THETAETH","localSymbol":"THETAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"THETA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"THETAEUR":{"symbol":"THETAEUR","localSymbol":"THETAEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"THETA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"THETAUSDT":{"symbol":"THETAUSDT","localSymbol":"THETAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"THETA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"TKOBIDR":{"symbol":"TKOBIDR","localSymbol":"TKOBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"TKO","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"TKOBTC":{"symbol":"TKOBTC","localSymbol":"TKOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TKO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TKOBUSD":{"symbol":"TKOBUSD","localSymbol":"TKOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TKO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"TKOUSDT":{"symbol":"TKOUSDT","localSymbol":"TKOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TKO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"TLMBNB":{"symbol":"TLMBNB","localSymbol":"TLMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TLM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"TLMBTC":{"symbol":"TLMBTC","localSymbol":"TLMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TLM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TLMBUSD":{"symbol":"TLMBUSD","localSymbol":"TLMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TLMTRY":{"symbol":"TLMTRY","localSymbol":"TLMTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"TLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TLMUSDT":{"symbol":"TLMUSDT","localSymbol":"TLMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TNBBTC":{"symbol":"TNBBTC","localSymbol":"TNBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TNB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TNBETH":{"symbol":"TNBETH","localSymbol":"TNBETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"TNB","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TNTBTC":{"symbol":"TNTBTC","localSymbol":"TNTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TNT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TNTETH":{"symbol":"TNTETH","localSymbol":"TNTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"TNT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TOMOBNB":{"symbol":"TOMOBNB","localSymbol":"TOMOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TOMO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TOMOBTC":{"symbol":"TOMOBTC","localSymbol":"TOMOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TOMO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TOMOBUSD":{"symbol":"TOMOBUSD","localSymbol":"TOMOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TOMO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TOMOUSDC":{"symbol":"TOMOUSDC","localSymbol":"TOMOUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"TOMO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TOMOUSDT":{"symbol":"TOMOUSDT","localSymbol":"TOMOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TOMO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TORNBNB":{"symbol":"TORNBNB","localSymbol":"TORNBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TORN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"TORNBTC":{"symbol":"TORNBTC","localSymbol":"TORNBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TORN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"TORNBUSD":{"symbol":"TORNBUSD","localSymbol":"TORNBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TORN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"TORNUSDT":{"symbol":"TORNUSDT","localSymbol":"TORNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TORN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"TRBBNB":{"symbol":"TRBBNB","localSymbol":"TRBBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TRB","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"TRBBTC":{"symbol":"TRBBTC","localSymbol":"TRBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TRB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":10000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"TRBBUSD":{"symbol":"TRBBUSD","localSymbol":"TRBBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TRB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"TRBUSDT":{"symbol":"TRBUSDT","localSymbol":"TRBUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TRB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"TRIBEBNB":{"symbol":"TRIBEBNB","localSymbol":"TRIBEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TRIBE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"TRIBEBTC":{"symbol":"TRIBEBTC","localSymbol":"TRIBEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TRIBE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TRIBEBUSD":{"symbol":"TRIBEBUSD","localSymbol":"TRIBEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TRIBE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TRIBEUSDT":{"symbol":"TRIBEUSDT","localSymbol":"TRIBEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TRIBE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TRIGBNB":{"symbol":"TRIGBNB","localSymbol":"TRIGBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TRIG","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TRIGBTC":{"symbol":"TRIGBTC","localSymbol":"TRIGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TRIG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"TRIGETH":{"symbol":"TRIGETH","localSymbol":"TRIGETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"TRIG","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"TROYBNB":{"symbol":"TROYBNB","localSymbol":"TROYBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TROY","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TROYBTC":{"symbol":"TROYBTC","localSymbol":"TROYBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TROY","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TROYBUSD":{"symbol":"TROYBUSD","localSymbol":"TROYBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TROY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"TROYUSDT":{"symbol":"TROYUSDT","localSymbol":"TROYUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TROY","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"TRUBTC":{"symbol":"TRUBTC","localSymbol":"TRUBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TRU","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TRUBUSD":{"symbol":"TRUBUSD","localSymbol":"TRUBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TRU","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TRURUB":{"symbol":"TRURUB","localSymbol":"TRURUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"TRU","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"TRUUSDT":{"symbol":"TRUUSDT","localSymbol":"TRUUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TRU","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TRXAUD":{"symbol":"TRXAUD","localSymbol":"TRXAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"TRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TRXBNB":{"symbol":"TRXBNB","localSymbol":"TRXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TRX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"TRXBTC":{"symbol":"TRXBTC","localSymbol":"TRXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TRX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TRXBUSD":{"symbol":"TRXBUSD","localSymbol":"TRXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TRXDOWNUSDT":{"symbol":"TRXDOWNUSDT","localSymbol":"TRXDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TRXDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":89984117.00000000,"stepSize":0.01000000,"minPrice":0.11400000,"maxPrice":2.16200000,"tickSize":0.00100000},"TRXETH":{"symbol":"TRXETH","localSymbol":"TRXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"TRX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TRXEUR":{"symbol":"TRXEUR","localSymbol":"TRXEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"TRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TRXNGN":{"symbol":"TRXNGN","localSymbol":"TRXNGN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"NGN","baseCurrency":"TRX","minNotional":500.00000000,"minAmount":500.00000000,"minQuantity":0.01000000,"maxQuantity":922320.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"TRXPAX":{"symbol":"TRXPAX","localSymbol":"TRXPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"TRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TRXTRY":{"symbol":"TRXTRY","localSymbol":"TRXTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"TRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TRXTUSD":{"symbol":"TRXTUSD","localSymbol":"TRXTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"TRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TRXUPUSDT":{"symbol":"TRXUPUSDT","localSymbol":"TRXUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TRXUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.00389000,"maxPrice":0.07383000,"tickSize":0.00001000},"TRXUSDC":{"symbol":"TRXUSDC","localSymbol":"TRXUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"TRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TRXUSDT":{"symbol":"TRXUSDT","localSymbol":"TRXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TRXXRP":{"symbol":"TRXXRP","localSymbol":"TRXXRP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"XRP","baseCurrency":"TRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TUSDBNB":{"symbol":"TUSDBNB","localSymbol":"TUSDBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"TUSD","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"TUSDBTC":{"symbol":"TUSDBTC","localSymbol":"TUSDBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TUSD","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TUSDBTUSD":{"symbol":"TUSDBTUSD","localSymbol":"TUSDBTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"TUSDB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TUSDBUSD":{"symbol":"TUSDBUSD","localSymbol":"TUSDBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TUSD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TUSDETH":{"symbol":"TUSDETH","localSymbol":"TUSDETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"TUSD","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TUSDT":{"symbol":"TUSDT","localSymbol":"TUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"T","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TUSDUSDT":{"symbol":"TUSDUSDT","localSymbol":"TUSDUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TUSD","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TVKBTC":{"symbol":"TVKBTC","localSymbol":"TVKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TVK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TVKBUSD":{"symbol":"TVKBUSD","localSymbol":"TVKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TVK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TVKUSDT":{"symbol":"TVKUSDT","localSymbol":"TVKUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TVK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TWTBTC":{"symbol":"TWTBTC","localSymbol":"TWTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"TWT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"TWTBUSD":{"symbol":"TWTBUSD","localSymbol":"TWTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"TWT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"TWTTRY":{"symbol":"TWTTRY","localSymbol":"TWTTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"TWT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"TWTUSDT":{"symbol":"TWTUSDT","localSymbol":"TWTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"TWT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"UFTBUSD":{"symbol":"UFTBUSD","localSymbol":"UFTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"UFT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"UFTETH":{"symbol":"UFTETH","localSymbol":"UFTETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"UFT","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"UMABTC":{"symbol":"UMABTC","localSymbol":"UMABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"UMA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"UMABUSD":{"symbol":"UMABUSD","localSymbol":"UMABUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"UMA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"UMATRY":{"symbol":"UMATRY","localSymbol":"UMATRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"UMA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":922327.00000000,"stepSize":0.01000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"UMAUSDT":{"symbol":"UMAUSDT","localSymbol":"UMAUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"UMA","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"UNFIBNB":{"symbol":"UNFIBNB","localSymbol":"UNFIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"UNFI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"UNFIBTC":{"symbol":"UNFIBTC","localSymbol":"UNFIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"UNFI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"UNFIBUSD":{"symbol":"UNFIBUSD","localSymbol":"UNFIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"UNFI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"UNFIETH":{"symbol":"UNFIETH","localSymbol":"UNFIETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"UNFI","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"UNFIUSDT":{"symbol":"UNFIUSDT","localSymbol":"UNFIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"UNFI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":90000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"UNIAUD":{"symbol":"UNIAUD","localSymbol":"UNIAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"UNI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"UNIBNB":{"symbol":"UNIBNB","localSymbol":"UNIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"UNI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"UNIBTC":{"symbol":"UNIBTC","localSymbol":"UNIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"UNI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"UNIBUSD":{"symbol":"UNIBUSD","localSymbol":"UNIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"UNI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"UNIDOWNUSDT":{"symbol":"UNIDOWNUSDT","localSymbol":"UNIDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"UNIDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":99999999.00000000,"stepSize":0.01000000,"minPrice":0.21900000,"maxPrice":4.15000000,"tickSize":0.00100000},"UNIETH":{"symbol":"UNIETH","localSymbol":"UNIETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"UNI","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":92141578.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"UNIEUR":{"symbol":"UNIEUR","localSymbol":"UNIEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"UNI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"UNIUPUSDT":{"symbol":"UNIUPUSDT","localSymbol":"UNIUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"UNIUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.10700000,"maxPrice":2.01799999,"tickSize":0.00100000},"UNIUSDT":{"symbol":"UNIUSDT","localSymbol":"UNIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"UNI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"USDCBNB":{"symbol":"USDCBNB","localSymbol":"USDCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"USDC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"USDCBUSD":{"symbol":"USDCBUSD","localSymbol":"USDCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"USDC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDCPAX":{"symbol":"USDCPAX","localSymbol":"USDCPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"USDC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDCTUSD":{"symbol":"USDCTUSD","localSymbol":"USDCTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"USDC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDCUSDT":{"symbol":"USDCUSDT","localSymbol":"USDCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"USDC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDPBUSD":{"symbol":"USDPBUSD","localSymbol":"USDPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"USDP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDPUSDT":{"symbol":"USDPUSDT","localSymbol":"USDPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"USDP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDSBUSDS":{"symbol":"USDSBUSDS","localSymbol":"USDSBUSDS","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDS","baseCurrency":"USDSB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDSBUSDT":{"symbol":"USDSBUSDT","localSymbol":"USDSBUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"USDSB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDSPAX":{"symbol":"USDSPAX","localSymbol":"USDSPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"USDS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDSTUSD":{"symbol":"USDSTUSD","localSymbol":"USDSTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"USDS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDSUSDC":{"symbol":"USDSUSDC","localSymbol":"USDSUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"USDS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDSUSDT":{"symbol":"USDSUSDT","localSymbol":"USDSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"USDS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDTBIDR":{"symbol":"USDTBIDR","localSymbol":"USDTBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"USDT","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.10000000,"maxQuantity":1000000.00000000,"stepSize":0.10000000,"minPrice":1.00000000,"maxPrice":100099999.00000000,"tickSize":1.00000000},"USDTBKRW":{"symbol":"USDTBKRW","localSymbol":"USDTBKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"USDT","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"USDTBRL":{"symbol":"USDTBRL","localSymbol":"USDTBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"USDT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"USDTBVND":{"symbol":"USDTBVND","localSymbol":"USDTBVND","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BVND","baseCurrency":"USDT","minNotional":30000.00000000,"minAmount":30000.00000000,"minQuantity":1.00000000,"maxQuantity":1000000.00000000,"stepSize":1.00000000,"minPrice":12003.00000000,"maxPrice":36010.00000000,"tickSize":1.00000000},"USDTDAI":{"symbol":"USDTDAI","localSymbol":"USDTDAI","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"DAI","baseCurrency":"USDT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"USDTIDRT":{"symbol":"USDTIDRT","localSymbol":"USDTIDRT","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"IDRT","baseCurrency":"USDT","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":1.00000000,"maxQuantity":1000000.00000000,"stepSize":1.00000000,"minPrice":1.00000000,"maxPrice":100099999.00000000,"tickSize":1.00000000},"USDTNGN":{"symbol":"USDTNGN","localSymbol":"USDTNGN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"NGN","baseCurrency":"USDT","minNotional":500.00000000,"minAmount":500.00000000,"minQuantity":0.10000000,"maxQuantity":922320.00000000,"stepSize":0.10000000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"USDTRUB":{"symbol":"USDTRUB","localSymbol":"USDTRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"USDT","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"USDTTRY":{"symbol":"USDTTRY","localSymbol":"USDTTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"USDT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"USDTUAH":{"symbol":"USDTUAH","localSymbol":"USDTUAH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"UAH","baseCurrency":"USDT","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"USDTZAR":{"symbol":"USDTZAR","localSymbol":"USDTZAR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ZAR","baseCurrency":"USDT","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.10000000,"maxQuantity":100000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"USTBTC":{"symbol":"USTBTC","localSymbol":"USTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"UST","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"USTBUSD":{"symbol":"USTBUSD","localSymbol":"USTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"UST","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.30000000,"tickSize":0.00000001},"USTCBUSD":{"symbol":"USTCBUSD","localSymbol":"USTCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"USTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"USTUSDT":{"symbol":"USTUSDT","localSymbol":"USTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"UST","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1.30000000,"tickSize":0.00000100},"UTKBTC":{"symbol":"UTKBTC","localSymbol":"UTKBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"UTK","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"UTKBUSD":{"symbol":"UTKBUSD","localSymbol":"UTKBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"UTK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"UTKUSDT":{"symbol":"UTKUSDT","localSymbol":"UTKUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"UTK","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"VENBNB":{"symbol":"VENBNB","localSymbol":"VENBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"VEN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"VENBTC":{"symbol":"VENBTC","localSymbol":"VENBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"VEN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VENETH":{"symbol":"VENETH","localSymbol":"VENETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"VEN","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VENUSDT":{"symbol":"VENUSDT","localSymbol":"VENUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"VEN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"VETBNB":{"symbol":"VETBNB","localSymbol":"VETBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"VET","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"VETBTC":{"symbol":"VETBTC","localSymbol":"VETBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"VET","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VETBUSD":{"symbol":"VETBUSD","localSymbol":"VETBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"VET","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"VETETH":{"symbol":"VETETH","localSymbol":"VETETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"VET","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VETEUR":{"symbol":"VETEUR","localSymbol":"VETEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"VET","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"VETGBP":{"symbol":"VETGBP","localSymbol":"VETGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"VET","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"VETTRY":{"symbol":"VETTRY","localSymbol":"VETTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"VET","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"VETUSDT":{"symbol":"VETUSDT","localSymbol":"VETUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"VET","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"VGXBTC":{"symbol":"VGXBTC","localSymbol":"VGXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"VGX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VGXETH":{"symbol":"VGXETH","localSymbol":"VGXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"VGX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"VGXUSDT":{"symbol":"VGXUSDT","localSymbol":"VGXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"VGX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"VIABNB":{"symbol":"VIABNB","localSymbol":"VIABNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"VIA","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"VIABTC":{"symbol":"VIABTC","localSymbol":"VIABTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"VIA","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VIAETH":{"symbol":"VIAETH","localSymbol":"VIAETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"VIA","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"VIBBTC":{"symbol":"VIBBTC","localSymbol":"VIBBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"VIB","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VIBBUSD":{"symbol":"VIBBUSD","localSymbol":"VIBBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"VIB","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"VIBEBTC":{"symbol":"VIBEBTC","localSymbol":"VIBEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"VIBE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VIBEETH":{"symbol":"VIBEETH","localSymbol":"VIBEETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"VIBE","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"VIBETH":{"symbol":"VIBETH","localSymbol":"VIBETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"VIB","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VIDTBTC":{"symbol":"VIDTBTC","localSymbol":"VIDTBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"VIDT","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"VIDTBUSD":{"symbol":"VIDTBUSD","localSymbol":"VIDTBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"VIDT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":100.00000000,"tickSize":0.00001000},"VIDTUSDT":{"symbol":"VIDTUSDT","localSymbol":"VIDTUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"VIDT","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":100.00000000,"tickSize":0.00001000},"VITEBNB":{"symbol":"VITEBNB","localSymbol":"VITEBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"VITE","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"VITEBTC":{"symbol":"VITEBTC","localSymbol":"VITEBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"VITE","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VITEBUSD":{"symbol":"VITEBUSD","localSymbol":"VITEBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"VITE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"VITEUSDT":{"symbol":"VITEUSDT","localSymbol":"VITEUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"VITE","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"VOXELBNB":{"symbol":"VOXELBNB","localSymbol":"VOXELBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"VOXEL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"VOXELBTC":{"symbol":"VOXELBTC","localSymbol":"VOXELBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"VOXEL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VOXELBUSD":{"symbol":"VOXELBUSD","localSymbol":"VOXELBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"VOXEL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"VOXELETH":{"symbol":"VOXELETH","localSymbol":"VOXELETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"VOXEL","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"VOXELUSDT":{"symbol":"VOXELUSDT","localSymbol":"VOXELUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"VOXEL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"VTHOBNB":{"symbol":"VTHOBNB","localSymbol":"VTHOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"VTHO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"VTHOBUSD":{"symbol":"VTHOBUSD","localSymbol":"VTHOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"VTHO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"VTHOUSDT":{"symbol":"VTHOUSDT","localSymbol":"VTHOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"VTHO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"WABIBNB":{"symbol":"WABIBNB","localSymbol":"WABIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"WABI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"WABIBTC":{"symbol":"WABIBTC","localSymbol":"WABIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WABI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WABIETH":{"symbol":"WABIETH","localSymbol":"WABIETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"WABI","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WANBNB":{"symbol":"WANBNB","localSymbol":"WANBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"WAN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"WANBTC":{"symbol":"WANBTC","localSymbol":"WANBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WAN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WANETH":{"symbol":"WANETH","localSymbol":"WANETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"WAN","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"WANUSDT":{"symbol":"WANUSDT","localSymbol":"WANUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"WAN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WAVESBNB":{"symbol":"WAVESBNB","localSymbol":"WAVESBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"WAVES","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"WAVESBTC":{"symbol":"WAVESBTC","localSymbol":"WAVESBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WAVES","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"WAVESBUSD":{"symbol":"WAVESBUSD","localSymbol":"WAVESBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"WAVES","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"WAVESETH":{"symbol":"WAVESETH","localSymbol":"WAVESETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"WAVES","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"WAVESEUR":{"symbol":"WAVESEUR","localSymbol":"WAVESEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"WAVES","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"WAVESPAX":{"symbol":"WAVESPAX","localSymbol":"WAVESPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"WAVES","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WAVESRUB":{"symbol":"WAVESRUB","localSymbol":"WAVESRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"WAVES","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.01000000,"maxQuantity":92233.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":999996.00000000,"tickSize":0.01000000},"WAVESTRY":{"symbol":"WAVESTRY","localSymbol":"WAVESTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"WAVES","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":92233.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":999996.00000000,"tickSize":0.10000000},"WAVESTUSD":{"symbol":"WAVESTUSD","localSymbol":"WAVESTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"WAVES","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WAVESUSDC":{"symbol":"WAVESUSDC","localSymbol":"WAVESUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"WAVES","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WAVESUSDT":{"symbol":"WAVESUSDT","localSymbol":"WAVESUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"WAVES","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"WAXPBNB":{"symbol":"WAXPBNB","localSymbol":"WAXPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"WAXP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"WAXPBTC":{"symbol":"WAXPBTC","localSymbol":"WAXPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WAXP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WAXPBUSD":{"symbol":"WAXPBUSD","localSymbol":"WAXPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"WAXP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WAXPUSDT":{"symbol":"WAXPUSDT","localSymbol":"WAXPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"WAXP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WBTCBTC":{"symbol":"WBTCBTC","localSymbol":"WBTCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WBTC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00001000,"maxQuantity":92141578.00000000,"stepSize":0.00001000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WBTCBUSD":{"symbol":"WBTCBUSD","localSymbol":"WBTCBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"WBTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"WBTCETH":{"symbol":"WBTCETH","localSymbol":"WBTCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"WBTC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00001000,"maxQuantity":100000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"WINBNB":{"symbol":"WINBNB","localSymbol":"WINBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"WIN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":100.00000000,"tickSize":0.00000001},"WINBRL":{"symbol":"WINBRL","localSymbol":"WINBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"WIN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"WINBTC":{"symbol":"WINBTC","localSymbol":"WINBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WIN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WINBUSD":{"symbol":"WINBUSD","localSymbol":"WINBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"WIN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":100.00000000,"tickSize":0.00000010},"WINEUR":{"symbol":"WINEUR","localSymbol":"WINEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"WIN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":100.00000000,"tickSize":0.00000010},"WINGBNB":{"symbol":"WINGBNB","localSymbol":"WINGBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"WING","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"WINGBTC":{"symbol":"WINGBTC","localSymbol":"WINGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WING","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":10000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"WINGBUSD":{"symbol":"WINGBUSD","localSymbol":"WINGBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"WING","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"WINGETH":{"symbol":"WINGETH","localSymbol":"WINGETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"WING","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":913205152.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"WINGSBTC":{"symbol":"WINGSBTC","localSymbol":"WINGSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WINGS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WINGSETH":{"symbol":"WINGSETH","localSymbol":"WINGSETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"WINGS","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"WINGUSDT":{"symbol":"WINGUSDT","localSymbol":"WINGUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"WING","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"WINTRX":{"symbol":"WINTRX","localSymbol":"WINTRX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRX","baseCurrency":"WIN","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":0.10000000,"maxQuantity":913205152.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":100.00000000,"tickSize":0.00000100},"WINUSDC":{"symbol":"WINUSDC","localSymbol":"WINUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"WIN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":100.00000000,"tickSize":0.00000010},"WINUSDT":{"symbol":"WINUSDT","localSymbol":"WINUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"WIN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":913205152.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":100.00000000,"tickSize":0.00000010},"WNXMBNB":{"symbol":"WNXMBNB","localSymbol":"WNXMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"WNXM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WNXMBTC":{"symbol":"WNXMBTC","localSymbol":"WNXMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WNXM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":10000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"WNXMBUSD":{"symbol":"WNXMBUSD","localSymbol":"WNXMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"WNXM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"WNXMUSDT":{"symbol":"WNXMUSDT","localSymbol":"WNXMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"WNXM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":90000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"WOOBNB":{"symbol":"WOOBNB","localSymbol":"WOOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"WOO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"WOOBTC":{"symbol":"WOOBTC","localSymbol":"WOOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WOO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WOOBUSD":{"symbol":"WOOBUSD","localSymbol":"WOOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"WOO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WOOUSDT":{"symbol":"WOOUSDT","localSymbol":"WOOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"WOO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WPRBTC":{"symbol":"WPRBTC","localSymbol":"WPRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WPR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WPRETH":{"symbol":"WPRETH","localSymbol":"WPRETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"WPR","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WRXBNB":{"symbol":"WRXBNB","localSymbol":"WRXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"WRX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"WRXBTC":{"symbol":"WRXBTC","localSymbol":"WRXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WRX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WRXBUSD":{"symbol":"WRXBUSD","localSymbol":"WRXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"WRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WRXEUR":{"symbol":"WRXEUR","localSymbol":"WRXEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"WRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"WRXUSDT":{"symbol":"WRXUSDT","localSymbol":"WRXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"WRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"WTCBNB":{"symbol":"WTCBNB","localSymbol":"WTCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"WTC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"WTCBTC":{"symbol":"WTCBTC","localSymbol":"WTCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"WTC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"WTCETH":{"symbol":"WTCETH","localSymbol":"WTCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"WTC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"WTCUSDT":{"symbol":"WTCUSDT","localSymbol":"WTCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"WTC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XECBUSD":{"symbol":"XECBUSD","localSymbol":"XECBUSD","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"BUSD","baseCurrency":"XEC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"XECUSDT":{"symbol":"XECUSDT","localSymbol":"XECUSDT","pricePrecision":8,"volumePrecision":2,"quoteCurrency":"USDT","baseCurrency":"XEC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":46116860414.00000763,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1.00000000,"tickSize":0.00000001},"XEMBNB":{"symbol":"XEMBNB","localSymbol":"XEMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"XEM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"XEMBTC":{"symbol":"XEMBTC","localSymbol":"XEMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"XEM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"XEMBUSD":{"symbol":"XEMBUSD","localSymbol":"XEMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"XEM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"XEMETH":{"symbol":"XEMETH","localSymbol":"XEMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"XEM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"XEMUSDT":{"symbol":"XEMUSDT","localSymbol":"XEMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XEM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XLMBNB":{"symbol":"XLMBNB","localSymbol":"XLMBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"XLM","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"XLMBTC":{"symbol":"XLMBTC","localSymbol":"XLMBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"XLM","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"XLMBUSD":{"symbol":"XLMBUSD","localSymbol":"XLMBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"XLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XLMDOWNUSDT":{"symbol":"XLMDOWNUSDT","localSymbol":"XLMDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XLMDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":399280174.00000000,"stepSize":0.01000000,"minPrice":0.43100000,"maxPrice":8.18200000,"tickSize":0.00100000},"XLMETH":{"symbol":"XLMETH","localSymbol":"XLMETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"XLM","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"XLMEUR":{"symbol":"XLMEUR","localSymbol":"XLMEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"XLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XLMPAX":{"symbol":"XLMPAX","localSymbol":"XLMPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"XLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"XLMTRY":{"symbol":"XLMTRY","localSymbol":"XLMTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"XLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"XLMTUSD":{"symbol":"XLMTUSD","localSymbol":"XLMTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"XLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"XLMUPUSDT":{"symbol":"XLMUPUSDT","localSymbol":"XLMUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XLMUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.00244000,"maxPrice":0.04624000,"tickSize":0.00001000},"XLMUSDC":{"symbol":"XLMUSDC","localSymbol":"XLMUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"XLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"XLMUSDT":{"symbol":"XLMUSDT","localSymbol":"XLMUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XLM","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XMRBNB":{"symbol":"XMRBNB","localSymbol":"XMRBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"XMR","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XMRBTC":{"symbol":"XMRBTC","localSymbol":"XMRBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"XMR","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"XMRBUSD":{"symbol":"XMRBUSD","localSymbol":"XMRBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"XMR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"XMRETH":{"symbol":"XMRETH","localSymbol":"XMRETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"XMR","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":9000000.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"XMRUSDT":{"symbol":"XMRUSDT","localSymbol":"XMRUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XMR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"XNOBTC":{"symbol":"XNOBTC","localSymbol":"XNOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"XNO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"XNOBUSD":{"symbol":"XNOBUSD","localSymbol":"XNOBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"XNO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"XNOETH":{"symbol":"XNOETH","localSymbol":"XNOETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"XNO","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"XNOUSDT":{"symbol":"XNOUSDT","localSymbol":"XNOUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XNO","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"XRPAUD":{"symbol":"XRPAUD","localSymbol":"XRPAUD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"AUD","baseCurrency":"XRP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XRPBEARBUSD":{"symbol":"XRPBEARBUSD","localSymbol":"XRPBEARBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"XRPBEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"XRPBEARUSDT":{"symbol":"XRPBEARUSDT","localSymbol":"XRPBEARUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XRPBEAR","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"XRPBIDR":{"symbol":"XRPBIDR","localSymbol":"XRPBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"XRP","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":0.10000000,"maxQuantity":92233.00000000,"stepSize":0.10000000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"XRPBKRW":{"symbol":"XRPBKRW","localSymbol":"XRPBKRW","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BKRW","baseCurrency":"XRP","minNotional":1000.00000000,"minAmount":1000.00000000,"minQuantity":0.10000000,"maxQuantity":922327.00000000,"stepSize":0.10000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"XRPBNB":{"symbol":"XRPBNB","localSymbol":"XRPBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"XRP","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"XRPBRL":{"symbol":"XRPBRL","localSymbol":"XRPBRL","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BRL","baseCurrency":"XRP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":922327.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":100000.00000000,"tickSize":0.00100000},"XRPBTC":{"symbol":"XRPBTC","localSymbol":"XRPBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"XRP","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"XRPBULLBUSD":{"symbol":"XRPBULLBUSD","localSymbol":"XRPBULLBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"XRPBULL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"XRPBULLUSDT":{"symbol":"XRPBULLUSDT","localSymbol":"XRPBULLUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XRPBULL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"XRPBUSD":{"symbol":"XRPBUSD","localSymbol":"XRPBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"XRP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XRPDOWNUSDT":{"symbol":"XRPDOWNUSDT","localSymbol":"XRPDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XRPDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":99999999.00000000,"stepSize":0.01000000,"minPrice":0.00002530,"maxPrice":0.00047950,"tickSize":0.00000010},"XRPETH":{"symbol":"XRPETH","localSymbol":"XRPETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"XRP","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"XRPEUR":{"symbol":"XRPEUR","localSymbol":"XRPEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"XRP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XRPGBP":{"symbol":"XRPGBP","localSymbol":"XRPGBP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"GBP","baseCurrency":"XRP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XRPNGN":{"symbol":"XRPNGN","localSymbol":"XRPNGN","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"NGN","baseCurrency":"XRP","minNotional":500.00000000,"minAmount":500.00000000,"minQuantity":0.01000000,"maxQuantity":922320.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"XRPPAX":{"symbol":"XRPPAX","localSymbol":"XRPPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"XRP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XRPRUB":{"symbol":"XRPRUB","localSymbol":"XRPRUB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"RUB","baseCurrency":"XRP","minNotional":100.00000000,"minAmount":100.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"XRPTRY":{"symbol":"XRPTRY","localSymbol":"XRPTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"XRP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":92141578.00000000,"stepSize":1.00000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"XRPTUSD":{"symbol":"XRPTUSD","localSymbol":"XRPTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"XRP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XRPUPUSDT":{"symbol":"XRPUPUSDT","localSymbol":"XRPUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XRPUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.00980000,"maxPrice":0.18590000,"tickSize":0.00010000},"XRPUSDC":{"symbol":"XRPUSDC","localSymbol":"XRPUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"XRP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"XRPUSDT":{"symbol":"XRPUSDT","localSymbol":"XRPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XRP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"XTZBNB":{"symbol":"XTZBNB","localSymbol":"XTZBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"XTZ","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"XTZBTC":{"symbol":"XTZBTC","localSymbol":"XTZBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"XTZ","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":90000000.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"XTZBUSD":{"symbol":"XTZBUSD","localSymbol":"XTZBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"XTZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"XTZDOWNUSDT":{"symbol":"XTZDOWNUSDT","localSymbol":"XTZDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XTZDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":49991176.00000000,"stepSize":0.01000000,"minPrice":0.25879999,"maxPrice":4.91690000,"tickSize":0.00010000},"XTZETH":{"symbol":"XTZETH","localSymbol":"XTZETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"XTZ","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":92141578.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"XTZTRY":{"symbol":"XTZTRY","localSymbol":"XTZTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"XTZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"XTZUPUSDT":{"symbol":"XTZUPUSDT","localSymbol":"XTZUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XTZUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.00025999,"maxPrice":0.00492800,"tickSize":0.00000100},"XTZUSDT":{"symbol":"XTZUSDT","localSymbol":"XTZUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XTZ","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":900000.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":1000.00000000,"tickSize":0.00100000},"XVGBTC":{"symbol":"XVGBTC","localSymbol":"XVGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"XVG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"XVGBUSD":{"symbol":"XVGBUSD","localSymbol":"XVGBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"XVG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"XVGETH":{"symbol":"XVGETH","localSymbol":"XVGETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"XVG","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"XVGUSDT":{"symbol":"XVGUSDT","localSymbol":"XVGUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XVG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":9222449.00000000,"stepSize":1.00000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"XVSBNB":{"symbol":"XVSBNB","localSymbol":"XVSBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"XVS","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":10000.00000000,"tickSize":0.00001000},"XVSBTC":{"symbol":"XVSBTC","localSymbol":"XVSBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"XVS","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"XVSBUSD":{"symbol":"XVSBUSD","localSymbol":"XVSBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"XVS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"XVSUSDT":{"symbol":"XVSUSDT","localSymbol":"XVSUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XVS","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"XZCBNB":{"symbol":"XZCBNB","localSymbol":"XZCBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"XZC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"XZCBTC":{"symbol":"XZCBTC","localSymbol":"XZCBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"XZC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"XZCETH":{"symbol":"XZCETH","localSymbol":"XZCETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"XZC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"XZCUSDT":{"symbol":"XZCUSDT","localSymbol":"XZCUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"XZC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"XZCXRP":{"symbol":"XZCXRP","localSymbol":"XZCXRP","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"XRP","baseCurrency":"XZC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"YFIBNB":{"symbol":"YFIBNB","localSymbol":"YFIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"YFI","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00001000,"maxQuantity":10000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":1000000.00000000,"tickSize":0.01000000},"YFIBTC":{"symbol":"YFIBTC","localSymbol":"YFIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"YFI","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":0.00010000,"maxPrice":100000.00000000,"tickSize":0.00010000},"YFIBUSD":{"symbol":"YFIBUSD","localSymbol":"YFIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"YFI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"YFIDOWNUSDT":{"symbol":"YFIDOWNUSDT","localSymbol":"YFIDOWNUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"YFIDOWN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":30000.00000000,"stepSize":0.01000000,"minPrice":0.09200000,"maxPrice":1.74790000,"tickSize":0.00010000},"YFIEUR":{"symbol":"YFIEUR","localSymbol":"YFIEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"YFI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"YFIIBNB":{"symbol":"YFIIBNB","localSymbol":"YFIIBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"YFII","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00010000,"maxQuantity":90000.00000000,"stepSize":0.00010000,"minPrice":0.01000000,"maxPrice":1000.00000000,"tickSize":0.01000000},"YFIIBTC":{"symbol":"YFIIBTC","localSymbol":"YFIIBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"YFII","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00010000,"maxQuantity":92141578.00000000,"stepSize":0.00010000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"YFIIBUSD":{"symbol":"YFIIBUSD","localSymbol":"YFIIBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"YFII","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":9000.00000000,"stepSize":0.00010000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"YFIIUSDT":{"symbol":"YFIIUSDT","localSymbol":"YFIIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"YFII","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00010000,"maxQuantity":9000.00000000,"stepSize":0.00010000,"minPrice":0.10000000,"maxPrice":1000000.00000000,"tickSize":0.10000000},"YFIUPUSDT":{"symbol":"YFIUPUSDT","localSymbol":"YFIUPUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"YFIUP","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":920000.00000000,"stepSize":0.01000000,"minPrice":0.01170000,"maxPrice":0.22040000,"tickSize":0.00010000},"YFIUSDT":{"symbol":"YFIUSDT","localSymbol":"YFIUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"YFI","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":9000.00000000,"stepSize":0.00001000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"YGGBNB":{"symbol":"YGGBNB","localSymbol":"YGGBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"YGG","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"YGGBTC":{"symbol":"YGGBTC","localSymbol":"YGGBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"YGG","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"YGGBUSD":{"symbol":"YGGBUSD","localSymbol":"YGGBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"YGG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"YGGUSDT":{"symbol":"YGGUSDT","localSymbol":"YGGUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"YGG","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":92141578.00000000,"stepSize":0.10000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"YOYOBNB":{"symbol":"YOYOBNB","localSymbol":"YOYOBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"YOYO","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"YOYOBTC":{"symbol":"YOYOBTC","localSymbol":"YOYOBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"YOYO","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"YOYOETH":{"symbol":"YOYOETH","localSymbol":"YOYOETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"YOYO","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ZECBNB":{"symbol":"ZECBNB","localSymbol":"ZECBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ZEC","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.00100000,"maxQuantity":900000.00000000,"stepSize":0.00100000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ZECBTC":{"symbol":"ZECBTC","localSymbol":"ZECBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ZEC","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.00100000,"maxQuantity":10000000.00000000,"stepSize":0.00100000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ZECBUSD":{"symbol":"ZECBUSD","localSymbol":"ZECBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ZEC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"ZECETH":{"symbol":"ZECETH","localSymbol":"ZECETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ZEC","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.00100000,"maxQuantity":9000000.00000000,"stepSize":0.00100000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ZECPAX":{"symbol":"ZECPAX","localSymbol":"ZECPAX","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"PAX","baseCurrency":"ZEC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ZECTUSD":{"symbol":"ZECTUSD","localSymbol":"ZECTUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TUSD","baseCurrency":"ZEC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00001000,"maxQuantity":90000.00000000,"stepSize":0.00001000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ZECUSDC":{"symbol":"ZECUSDC","localSymbol":"ZECUSDC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDC","baseCurrency":"ZEC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"ZECUSDT":{"symbol":"ZECUSDT","localSymbol":"ZECUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ZEC","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.00100000,"maxQuantity":90000.00000000,"stepSize":0.00100000,"minPrice":0.10000000,"maxPrice":100000.00000000,"tickSize":0.10000000},"ZENBNB":{"symbol":"ZENBNB","localSymbol":"ZENBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ZEN","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.00010000,"maxPrice":10000.00000000,"tickSize":0.00010000},"ZENBTC":{"symbol":"ZENBTC","localSymbol":"ZENBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ZEN","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":0.01000000,"maxQuantity":90000000.00000000,"stepSize":0.01000000,"minPrice":0.00000100,"maxPrice":1000.00000000,"tickSize":0.00000100},"ZENBUSD":{"symbol":"ZENBUSD","localSymbol":"ZENBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ZEN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":9222449.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":10000.00000000,"tickSize":0.01000000},"ZENETH":{"symbol":"ZENETH","localSymbol":"ZENETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ZEN","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":0.01000000,"maxQuantity":9000000.00000000,"stepSize":0.01000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ZENUSDT":{"symbol":"ZENUSDT","localSymbol":"ZENUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ZEN","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.01000000,"maxQuantity":900000.00000000,"stepSize":0.01000000,"minPrice":0.01000000,"maxPrice":100000.00000000,"tickSize":0.01000000},"ZILBIDR":{"symbol":"ZILBIDR","localSymbol":"ZILBIDR","pricePrecision":2,"volumePrecision":8,"quoteCurrency":"BIDR","baseCurrency":"ZIL","minNotional":20000.00000000,"minAmount":20000.00000000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":1.00000000,"maxPrice":1000000.00000000,"tickSize":1.00000000},"ZILBNB":{"symbol":"ZILBNB","localSymbol":"ZILBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ZIL","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":1.00000000,"maxQuantity":9000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ZILBTC":{"symbol":"ZILBTC","localSymbol":"ZILBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ZIL","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ZILBUSD":{"symbol":"ZILBUSD","localSymbol":"ZILBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ZIL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ZILETH":{"symbol":"ZILETH","localSymbol":"ZILETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ZIL","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ZILEUR":{"symbol":"ZILEUR","localSymbol":"ZILEUR","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"EUR","baseCurrency":"ZIL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ZILTRY":{"symbol":"ZILTRY","localSymbol":"ZILTRY","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"TRY","baseCurrency":"ZIL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9222449.00000000,"stepSize":0.10000000,"minPrice":0.00100000,"maxPrice":10000.00000000,"tickSize":0.00100000},"ZILUSDT":{"symbol":"ZILUSDT","localSymbol":"ZILUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ZIL","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ZRXBNB":{"symbol":"ZRXBNB","localSymbol":"ZRXBNB","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BNB","baseCurrency":"ZRX","minNotional":0.05000000,"minAmount":0.05000000,"minQuantity":0.10000000,"maxQuantity":9000000.00000000,"stepSize":0.10000000,"minPrice":0.00001000,"maxPrice":1000.00000000,"tickSize":0.00001000},"ZRXBTC":{"symbol":"ZRXBTC","localSymbol":"ZRXBTC","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BTC","baseCurrency":"ZRX","minNotional":0.00010000,"minAmount":0.00010000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000001,"maxPrice":1000.00000000,"tickSize":0.00000001},"ZRXBUSD":{"symbol":"ZRXBUSD","localSymbol":"ZRXBUSD","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"BUSD","baseCurrency":"ZRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000},"ZRXETH":{"symbol":"ZRXETH","localSymbol":"ZRXETH","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"ETH","baseCurrency":"ZRX","minNotional":0.00500000,"minAmount":0.00500000,"minQuantity":1.00000000,"maxQuantity":90000000.00000000,"stepSize":1.00000000,"minPrice":0.00000010,"maxPrice":1000.00000000,"tickSize":0.00000010},"ZRXUSDT":{"symbol":"ZRXUSDT","localSymbol":"ZRXUSDT","pricePrecision":8,"volumePrecision":8,"quoteCurrency":"USDT","baseCurrency":"ZRX","minNotional":10.00000000,"minAmount":10.00000000,"minQuantity":1.00000000,"maxQuantity":900000.00000000,"stepSize":1.00000000,"minPrice":0.00010000,"maxPrice":1000.00000000,"tickSize":0.00010000}} \ No newline at end of file From 7a8ae7cd0161c36512f83140b50798cb686408e3 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 02:49:48 +0800 Subject: [PATCH 0218/1392] github: link market data --- .github/workflows/go.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1c89f9d3f4..c845bb3de1 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -69,6 +69,12 @@ jobs: - name: Build run: go build -v ./cmd/bbgo + - name: Link Market Data + run: | + mkdir -p ~/.bbgo/cache + ln -s $PWD/data/binance-markets.json ~/.bbgo/cache/binance-markets.json + touch data/binance-markets.json + - name: Test run: | go test -race -coverprofile coverage.txt -covermode atomic ./pkg/... From e29f3c50e89936419f534824dc5dbd03c9f44304 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 10:05:43 +0800 Subject: [PATCH 0219/1392] grid2: calculate TotalFee --- config/grid2.yaml | 3 +- pkg/strategy/grid2/profit.go | 39 ------------------- pkg/strategy/grid2/profit_stats.go | 60 ++++++++++++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 8 +++- 4 files changed, 69 insertions(+), 41 deletions(-) create mode 100644 pkg/strategy/grid2/profit_stats.go diff --git a/config/grid2.yaml b/config/grid2.yaml index b6f22190fb..2e464e318a 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -21,6 +21,7 @@ backtest: symbols: - BTCUSDT sessions: [binance] + feeMode: token accounts: binance: balances: @@ -32,7 +33,7 @@ exchangeStrategies: - on: binance grid2: symbol: BTCUSDT - upperPrice: 60_000.0 + upperPrice: 50_000.0 lowerPrice: 28_000.0 gridNumber: 1000 diff --git a/pkg/strategy/grid2/profit.go b/pkg/strategy/grid2/profit.go index 1d7ab1f793..dd1e622fdb 100644 --- a/pkg/strategy/grid2/profit.go +++ b/pkg/strategy/grid2/profit.go @@ -13,42 +13,3 @@ type GridProfit struct { Time time.Time `json:"time"` Order types.Order `json:"order"` } - -type GridProfitStats struct { - Symbol string `json:"symbol"` - TotalBaseProfit fixedpoint.Value `json:"totalBaseProfit,omitempty"` - TotalQuoteProfit fixedpoint.Value `json:"totalQuoteProfit,omitempty"` - FloatProfit fixedpoint.Value `json:"floatProfit,omitempty"` - GridProfit fixedpoint.Value `json:"gridProfit,omitempty"` - ArbitrageCount int `json:"arbitrageCount,omitempty"` - TotalFee fixedpoint.Value `json:"totalFee,omitempty"` - Volume fixedpoint.Value `json:"volume,omitempty"` - Market types.Market `json:"market,omitempty"` - ProfitEntries []*GridProfit `json:"profitEntries,omitempty"` -} - -func newGridProfitStats(market types.Market) *GridProfitStats { - return &GridProfitStats{ - Symbol: market.Symbol, - TotalBaseProfit: fixedpoint.Zero, - TotalQuoteProfit: fixedpoint.Zero, - FloatProfit: fixedpoint.Zero, - GridProfit: fixedpoint.Zero, - ArbitrageCount: 0, - TotalFee: fixedpoint.Zero, - Volume: fixedpoint.Zero, - Market: market, - ProfitEntries: nil, - } -} - -func (s *GridProfitStats) AddProfit(profit *GridProfit) { - switch profit.Currency { - case s.Market.QuoteCurrency: - s.TotalQuoteProfit = s.TotalQuoteProfit.Add(profit.Profit) - case s.Market.BaseCurrency: - s.TotalBaseProfit = s.TotalBaseProfit.Add(profit.Profit) - } - - s.ProfitEntries = append(s.ProfitEntries, profit) -} diff --git a/pkg/strategy/grid2/profit_stats.go b/pkg/strategy/grid2/profit_stats.go new file mode 100644 index 0000000000..e1eed06ad8 --- /dev/null +++ b/pkg/strategy/grid2/profit_stats.go @@ -0,0 +1,60 @@ +package grid2 + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type GridProfitStats struct { + Symbol string `json:"symbol"` + TotalBaseProfit fixedpoint.Value `json:"totalBaseProfit,omitempty"` + TotalQuoteProfit fixedpoint.Value `json:"totalQuoteProfit,omitempty"` + FloatProfit fixedpoint.Value `json:"floatProfit,omitempty"` + GridProfit fixedpoint.Value `json:"gridProfit,omitempty"` + ArbitrageCount int `json:"arbitrageCount,omitempty"` + TotalFee map[string]fixedpoint.Value `json:"totalFee,omitempty"` + Volume fixedpoint.Value `json:"volume,omitempty"` + Market types.Market `json:"market,omitempty"` + ProfitEntries []*GridProfit `json:"profitEntries,omitempty"` +} + +func newGridProfitStats(market types.Market) *GridProfitStats { + return &GridProfitStats{ + Symbol: market.Symbol, + TotalBaseProfit: fixedpoint.Zero, + TotalQuoteProfit: fixedpoint.Zero, + FloatProfit: fixedpoint.Zero, + GridProfit: fixedpoint.Zero, + ArbitrageCount: 0, + TotalFee: make(map[string]fixedpoint.Value), + Volume: fixedpoint.Zero, + Market: market, + ProfitEntries: nil, + } +} + +func (s *GridProfitStats) AddTrade(trade types.Trade) { + if s.TotalFee == nil { + s.TotalFee = make(map[string]fixedpoint.Value) + } + + if fee, ok := s.TotalFee[trade.FeeCurrency]; ok { + s.TotalFee[trade.FeeCurrency] = fee.Add(trade.Fee) + } else { + s.TotalFee[trade.FeeCurrency] = trade.Fee + } +} + +func (s *GridProfitStats) AddProfit(profit *GridProfit) { + // increase arbitrage count per profit round + s.ArbitrageCount++ + + switch profit.Currency { + case s.Market.QuoteCurrency: + s.TotalQuoteProfit = s.TotalQuoteProfit.Add(profit.Profit) + case s.Market.BaseCurrency: + s.TotalBaseProfit = s.TotalBaseProfit.Add(profit.Profit) + } + + s.ProfitEntries = append(s.ProfitEntries, profit) +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6ead2e953d..db0725e1fd 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -261,7 +261,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { if o.Side == types.SideTypeBuy { orderTrades := s.historicalTrades.GetOrderTrades(o) if len(orderTrades) > 0 { - s.logger.Infof("FOUND FILLED ORDER TRADES: %+v", orderTrades) + s.logger.Infof("found filled order trades: %+v", orderTrades) } if !s.verifyOrderTrades(o, orderTrades) { @@ -276,6 +276,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { if err != nil { s.logger.WithError(err).Errorf("query order trades error") } else { + s.logger.Infof("fetch api trades: %+v", apiOrderTrades) orderTrades = apiOrderTrades } } @@ -959,9 +960,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindProfitStats(s.ProfitStats) s.orderExecutor.Bind() + + s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { + s.GridProfitStats.AddTrade(trade) + }) s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) + s.orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) // TODO: detect if there are previous grid orders on the order book From 402b625126f6cff684c03d628e6764e149d75843 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 10:06:58 +0800 Subject: [PATCH 0220/1392] grid2: add stringer method on gridProfit --- pkg/strategy/grid2/profit.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/strategy/grid2/profit.go b/pkg/strategy/grid2/profit.go index dd1e622fdb..245a7a586a 100644 --- a/pkg/strategy/grid2/profit.go +++ b/pkg/strategy/grid2/profit.go @@ -1,6 +1,7 @@ package grid2 import ( + "fmt" "time" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -13,3 +14,7 @@ type GridProfit struct { Time time.Time `json:"time"` Order types.Order `json:"order"` } + +func (p *GridProfit) String() string { + return fmt.Sprintf("GRID PROFIT: %f %s @ %s orderID %d", p.Profit.Float64(), p.Currency, p.Time.String(), p.Order.OrderID) +} From 82a1009f35441f022cf44754ab4ec406298eedd5 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 10:08:34 +0800 Subject: [PATCH 0221/1392] config: add baseInvestment --- config/grid2.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/grid2.yaml b/config/grid2.yaml index 2e464e318a..2a4360b976 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -25,7 +25,7 @@ backtest: accounts: binance: balances: - BTC: 0.0 + BTC: 1.0 USDT: 21_000.0 exchangeStrategies: @@ -77,7 +77,7 @@ exchangeStrategies: quoteInvestment: 20_000 ## baseInvestment (optional) can be useful when you have existing inventory, maybe bought at much lower price - baseInvestment: 0.0 + baseInvestment: 1.0 ## closeWhenCancelOrder (optional) ## default to false From c2e219e18002a4f60aebfb087f63ed6493a63e53 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 10:35:16 +0800 Subject: [PATCH 0222/1392] config: add fee rate to backtest settings --- config/grid2.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/grid2.yaml b/config/grid2.yaml index 2a4360b976..6fcb16a692 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -24,6 +24,8 @@ backtest: feeMode: token accounts: binance: + makerFeeRate: 0.075% + takerFeeRate: 0.075% balances: BTC: 1.0 USDT: 21_000.0 From 68e7d0ec241279ab27103eb5df9524361a73267a Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 10:47:19 +0800 Subject: [PATCH 0223/1392] grid2: add doc comment for gridNumber --- config/grid2.yaml | 8 ++++++-- pkg/strategy/grid2/strategy.go | 13 ++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/config/grid2.yaml b/config/grid2.yaml index 6fcb16a692..6d240ae42e 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -35,9 +35,13 @@ exchangeStrategies: - on: binance grid2: symbol: BTCUSDT - upperPrice: 50_000.0 lowerPrice: 28_000.0 - gridNumber: 1000 + upperPrice: 50_000.0 + + ## gridNumber is the total orders between the upper price and the lower price + ## gridSpread = (upperPrice - lowerPrice) / gridNumber + ## Make sure your gridNumber satisfy this: MIN(gridSpread/lowerPrice, gridSpread/upperPrice) > (makerFeeRate * 2) + gridNumber: 150 ## compound is used for buying more inventory when the profit is made by the filled SELL order. ## when compound is disabled, fixed quantity is used for each grid order. diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index db0725e1fd..bbf0224339 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -131,12 +131,15 @@ func (s *Strategy) Validate() error { } if !s.ProfitSpread.IsZero() { - percent := s.ProfitSpread.Div(s.LowerPrice) + // the min fee rate from 2 maker/taker orders (with 0.1 rate for profit) + gridFeeRate := s.FeeRate.Mul(fixedpoint.NewFromFloat(2.01)) - // the min fee rate from 2 maker/taker orders - minProfitSpread := s.FeeRate.Mul(fixedpoint.NewFromInt(2)) - if percent.Compare(minProfitSpread) < 0 { - return fmt.Errorf("profitSpread %f %s is too small, less than the fee rate: %s", s.ProfitSpread.Float64(), percent.Percentage(), s.FeeRate.Percentage()) + if s.ProfitSpread.Div(s.LowerPrice).Compare(gridFeeRate) < 0 { + return fmt.Errorf("profitSpread %f %s is too small for lower price, less than the fee rate: %s", s.ProfitSpread.Float64(), s.ProfitSpread.Div(s.LowerPrice).Percentage(), s.FeeRate.Percentage()) + } + + if s.ProfitSpread.Div(s.UpperPrice).Compare(gridFeeRate) < 0 { + return fmt.Errorf("profitSpread %f %s is too small for upper price, less than the fee rate: %s", s.ProfitSpread.Float64(), s.ProfitSpread.Div(s.UpperPrice).Percentage(), s.FeeRate.Percentage()) } } From 75521352a963faa743c34b8187b0265abb5519f6 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 11:48:32 +0800 Subject: [PATCH 0224/1392] grid2: pull out aggregateOrderBaseFee --- pkg/strategy/grid2/strategy.go | 80 +++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index bbf0224339..a8d691dc04 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -241,7 +241,50 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { return true } -// handleOrderFilled is called when a order status is FILLED +// aggregateOrderBaseFee collects the base fee quantity from the given order +// it falls back to query the trades via the RESTful API when the websocket trades are not all received. +func (s *Strategy) aggregateOrderBaseFee(o types.Order) fixedpoint.Value { + baseSellQuantityReduction := fixedpoint.Zero + + orderTrades := s.historicalTrades.GetOrderTrades(o) + if len(orderTrades) > 0 { + s.logger.Infof("found filled order trades: %+v", orderTrades) + } + + if !s.verifyOrderTrades(o, orderTrades) { + if s.orderQueryService == nil { + return baseSellQuantityReduction + } + + s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") + + // if orderQueryService is supported, use it to query the trades of the filled order + apiOrderTrades, err := s.orderQueryService.QueryOrderTrades(context.Background(), types.OrderQuery{ + Symbol: o.Symbol, + OrderID: strconv.FormatUint(o.OrderID, 10), + }) + if err != nil { + s.logger.WithError(err).Errorf("query order trades error") + } else { + s.logger.Infof("fetch api trades: %+v", apiOrderTrades) + orderTrades = apiOrderTrades + } + } + + if s.verifyOrderTrades(o, orderTrades) { + // check if there is a BaseCurrency fee collected + fees := collectTradeFee(orderTrades) + if fee, ok := fees[s.Market.BaseCurrency]; ok { + baseSellQuantityReduction = fee + s.logger.Infof("baseSellQuantityReduction: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) + return baseSellQuantityReduction + } + } + + return baseSellQuantityReduction +} + +// handleOrderFilled is called when an order status is FILLED func (s *Strategy) handleOrderFilled(o types.Order) { if s.grid == nil { return @@ -262,39 +305,8 @@ func (s *Strategy) handleOrderFilled(o types.Order) { // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC // if we don't reduce the sell quantity, than we might fail to place the sell order if o.Side == types.SideTypeBuy { - orderTrades := s.historicalTrades.GetOrderTrades(o) - if len(orderTrades) > 0 { - s.logger.Infof("found filled order trades: %+v", orderTrades) - } - - if !s.verifyOrderTrades(o, orderTrades) { - s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") - - // if orderQueryService is supported, use it to query the trades of the filled order - if s.orderQueryService != nil { - apiOrderTrades, err := s.orderQueryService.QueryOrderTrades(context.Background(), types.OrderQuery{ - Symbol: o.Symbol, - OrderID: strconv.FormatUint(o.OrderID, 10), - }) - if err != nil { - s.logger.WithError(err).Errorf("query order trades error") - } else { - s.logger.Infof("fetch api trades: %+v", apiOrderTrades) - orderTrades = apiOrderTrades - } - } - } - - if s.verifyOrderTrades(o, orderTrades) { - // check if there is a BaseCurrency fee collected - fees := collectTradeFee(orderTrades) - if fee, ok := fees[s.Market.BaseCurrency]; ok { - baseSellQuantityReduction = fee - s.logger.Infof("baseSellQuantityReduction: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) - - newQuantity = newQuantity.Sub(baseSellQuantityReduction) - } - } + baseSellQuantityReduction = s.aggregateOrderBaseFee(o) + newQuantity = newQuantity.Sub(baseSellQuantityReduction) } switch o.Side { From 846695e6329d2782dde3484a13e0b50a08ab0e8b Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 11:56:30 +0800 Subject: [PATCH 0225/1392] grid2: add retry to orderQuery --- pkg/strategy/grid2/strategy.go | 37 +++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index a8d691dc04..659529f22a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -19,6 +19,8 @@ const ID = "grid2" var log = logrus.WithField("strategy", ID) +var maxNumberOfOrderTradesQueryTries = 10 + func init() { // Register the pointer of the strategy struct, // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) @@ -244,16 +246,26 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { // aggregateOrderBaseFee collects the base fee quantity from the given order // it falls back to query the trades via the RESTful API when the websocket trades are not all received. func (s *Strategy) aggregateOrderBaseFee(o types.Order) fixedpoint.Value { - baseSellQuantityReduction := fixedpoint.Zero - + // try to get the received trades (websocket trades) orderTrades := s.historicalTrades.GetOrderTrades(o) if len(orderTrades) > 0 { s.logger.Infof("found filled order trades: %+v", orderTrades) } - if !s.verifyOrderTrades(o, orderTrades) { + for maxTries := maxNumberOfOrderTradesQueryTries; maxTries > 0; maxTries-- { + // if one of the trades is missing, we need to query the trades from the RESTful API + if s.verifyOrderTrades(o, orderTrades) { + // if trades are verified + fees := collectTradeFee(orderTrades) + if fee, ok := fees[s.Market.BaseCurrency]; ok { + return fee + } + return fixedpoint.Zero + } + + // if we don't support orderQueryService, then we should just skip if s.orderQueryService == nil { - return baseSellQuantityReduction + return fixedpoint.Zero } s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") @@ -266,22 +278,12 @@ func (s *Strategy) aggregateOrderBaseFee(o types.Order) fixedpoint.Value { if err != nil { s.logger.WithError(err).Errorf("query order trades error") } else { - s.logger.Infof("fetch api trades: %+v", apiOrderTrades) + s.logger.Infof("fetched api trades: %+v", apiOrderTrades) orderTrades = apiOrderTrades } } - if s.verifyOrderTrades(o, orderTrades) { - // check if there is a BaseCurrency fee collected - fees := collectTradeFee(orderTrades) - if fee, ok := fees[s.Market.BaseCurrency]; ok { - baseSellQuantityReduction = fee - s.logger.Infof("baseSellQuantityReduction: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) - return baseSellQuantityReduction - } - } - - return baseSellQuantityReduction + return fixedpoint.Zero } // handleOrderFilled is called when an order status is FILLED @@ -306,6 +308,9 @@ func (s *Strategy) handleOrderFilled(o types.Order) { // if we don't reduce the sell quantity, than we might fail to place the sell order if o.Side == types.SideTypeBuy { baseSellQuantityReduction = s.aggregateOrderBaseFee(o) + + s.logger.Infof("base fee: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) + newQuantity = newQuantity.Sub(baseSellQuantityReduction) } From c6ce223a13bd3c968306a83bd21682d9aebcc97c Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 13:16:12 +0800 Subject: [PATCH 0226/1392] all: refactor backtest functions so that we can run backtest in test --- pkg/backtest/exchange.go | 3 +- pkg/backtest/utils.go | 57 +++++++++++++++++++++++++ pkg/cmd/backtest.go | 56 +++---------------------- pkg/strategy/grid2/strategy_test.go | 64 ++++++++++++++++++++++++----- 4 files changed, 116 insertions(+), 64 deletions(-) create mode 100644 pkg/backtest/utils.go diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index 0f4d1d44a9..5faa412897 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -358,8 +358,7 @@ func (e *Exchange) SubscribeMarketData(startTime, endTime time.Time, requiredInt intervals = append(intervals, interval) } - log.Infof("using symbols: %v and intervals: %v for back-testing", symbols, intervals) - log.Infof("querying klines from database...") + log.Infof("querying klines from database with exchange: %v symbols: %v and intervals: %v for back-testing", e.Name(), symbols, intervals) klineC, errC := e.srv.QueryKLinesCh(startTime, endTime, e, symbols, intervals) go func() { if err := <-errC; err != nil { diff --git a/pkg/backtest/utils.go b/pkg/backtest/utils.go new file mode 100644 index 0000000000..870f36556f --- /dev/null +++ b/pkg/backtest/utils.go @@ -0,0 +1,57 @@ +package backtest + +import ( + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +func CollectSubscriptionIntervals(environ *bbgo.Environment) (allKLineIntervals map[types.Interval]struct{}, requiredInterval types.Interval, backTestIntervals []types.Interval) { + // default extra back-test intervals + backTestIntervals = []types.Interval{types.Interval1h, types.Interval1d} + // all subscribed intervals + allKLineIntervals = make(map[types.Interval]struct{}) + + for _, interval := range backTestIntervals { + allKLineIntervals[interval] = struct{}{} + } + // default interval is 1m for all exchanges + requiredInterval = types.Interval1m + for _, session := range environ.Sessions() { + for _, sub := range session.Subscriptions { + if sub.Channel == types.KLineChannel { + if sub.Options.Interval.Seconds()%60 > 0 { + // if any subscription interval is less than 60s, then we will use 1s for back-testing + requiredInterval = types.Interval1s + logrus.Warnf("found kline subscription interval less than 60s, modify default backtest interval to 1s") + } + allKLineIntervals[sub.Options.Interval] = struct{}{} + } + } + } + return allKLineIntervals, requiredInterval, backTestIntervals +} + +func InitializeExchangeSources(sessions map[string]*bbgo.ExchangeSession, startTime, endTime time.Time, requiredInterval types.Interval, extraIntervals ...types.Interval) (exchangeSources []*ExchangeDataSource, err error) { + for _, session := range sessions { + backtestEx := session.Exchange.(*Exchange) + + c, err := backtestEx.SubscribeMarketData(startTime, endTime, requiredInterval, extraIntervals...) + if err != nil { + return exchangeSources, err + } + + sessionCopy := session + src := &ExchangeDataSource{ + C: c, + Exchange: backtestEx, + Session: sessionCopy, + } + backtestEx.Src = src + exchangeSources = append(exchangeSources, src) + } + return exchangeSources, nil +} diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 3e2c24a428..b8d6886039 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -11,11 +11,12 @@ import ( "syscall" "time" + "github.com/fatih/color" + "github.com/google/uuid" + "github.com/c9s/bbgo/pkg/cmd/cmdutil" "github.com/c9s/bbgo/pkg/data/tsv" "github.com/c9s/bbgo/pkg/util" - "github.com/fatih/color" - "github.com/google/uuid" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -295,8 +296,8 @@ var BacktestCmd = &cobra.Command{ return err } - allKLineIntervals, requiredInterval, backTestIntervals := collectSubscriptionIntervals(environ) - exchangeSources, err := toExchangeSources(environ.Sessions(), startTime, endTime, requiredInterval, backTestIntervals...) + allKLineIntervals, requiredInterval, backTestIntervals := backtest.CollectSubscriptionIntervals(environ) + exchangeSources, err := backtest.InitializeExchangeSources(environ.Sessions(), startTime, endTime, requiredInterval, backTestIntervals...) if err != nil { return err } @@ -593,32 +594,6 @@ var BacktestCmd = &cobra.Command{ }, } -func collectSubscriptionIntervals(environ *bbgo.Environment) (allKLineIntervals map[types.Interval]struct{}, requiredInterval types.Interval, backTestIntervals []types.Interval) { - // default extra back-test intervals - backTestIntervals = []types.Interval{types.Interval1h, types.Interval1d} - // all subscribed intervals - allKLineIntervals = make(map[types.Interval]struct{}) - - for _, interval := range backTestIntervals { - allKLineIntervals[interval] = struct{}{} - } - // default interval is 1m for all exchanges - requiredInterval = types.Interval1m - for _, session := range environ.Sessions() { - for _, sub := range session.Subscriptions { - if sub.Channel == types.KLineChannel { - if sub.Options.Interval.Seconds()%60 > 0 { - // if any subscription interval is less than 60s, then we will use 1s for back-testing - requiredInterval = types.Interval1s - log.Warnf("found kline subscription interval less than 60s, modify default backtest interval to 1s") - } - allKLineIntervals[sub.Options.Interval] = struct{}{} - } - } - } - return allKLineIntervals, requiredInterval, backTestIntervals -} - func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, intervalProfit *types.IntervalProfitCollector, profitFactor, winningRatio fixedpoint.Value) ( *backtest.SessionSymbolReport, @@ -722,27 +697,6 @@ func confirmation(s string) bool { } } -func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, startTime, endTime time.Time, requiredInterval types.Interval, extraIntervals ...types.Interval) (exchangeSources []*backtest.ExchangeDataSource, err error) { - for _, session := range sessions { - backtestEx := session.Exchange.(*backtest.Exchange) - - c, err := backtestEx.SubscribeMarketData(startTime, endTime, requiredInterval, extraIntervals...) - if err != nil { - return exchangeSources, err - } - - sessionCopy := session - src := &backtest.ExchangeDataSource{ - C: c, - Exchange: backtestEx, - Session: sessionCopy, - } - backtestEx.Src = src - exchangeSources = append(exchangeSources, src) - } - return exchangeSources, nil -} - func sync(ctx context.Context, userConfig *bbgo.Config, backtestService *service.BacktestService, sourceExchanges map[types.ExchangeName]types.Exchange, syncFrom, syncTo time.Time) error { for _, symbol := range userConfig.Backtest.Symbols { for _, sourceExchange := range sourceExchanges { diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index f14e867ceb..3912e98d50 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -4,6 +4,7 @@ package grid2 import ( "context" + "os" "testing" "github.com/sirupsen/logrus" @@ -350,12 +351,20 @@ func TestBacktestStrategy(t *testing.T) { GridNum: 100, QuoteInvestment: number(9000.0), } + RunBacktest(t, strategy) +} +func RunBacktest(t *testing.T, strategy bbgo.SingleExchangeStrategy) { // TEMPLATE {{{ start backtest - startTime, err := types.ParseLooseFormatTime("2021-06-01") + const sqliteDbFile = "../../../data/bbgo_test.sqlite3" + const backtestExchangeName = "binance" + const backtestStartTime = "2022-06-01" + const backtestEndTime = "2022-06-30" + + startTime, err := types.ParseLooseFormatTime(backtestStartTime) assert.NoError(t, err) - endTime, err := types.ParseLooseFormatTime("2021-06-30") + endTime, err := types.ParseLooseFormatTime(backtestEndTime) assert.NoError(t, err) backtestConfig := &bbgo.Backtest{ @@ -364,7 +373,7 @@ func TestBacktestStrategy(t *testing.T) { RecordTrades: false, FeeMode: bbgo.BacktestFeeModeToken, Accounts: map[string]bbgo.BacktestAccount{ - "binance": { + backtestExchangeName: { MakerFeeRate: number(0.075 * 0.01), TakerFeeRate: number(0.075 * 0.01), Balances: bbgo.BacktestAccountBalanceMap{ @@ -374,7 +383,7 @@ func TestBacktestStrategy(t *testing.T) { }, }, Symbols: []string{"BTCUSDT"}, - Sessions: []string{"binance"}, + Sessions: []string{backtestExchangeName}, SyncSecKLines: false, } @@ -384,8 +393,14 @@ func TestBacktestStrategy(t *testing.T) { environ := bbgo.NewEnvironment() environ.SetStartTime(startTime.Time()) - err = environ.ConfigureDatabaseDriver(ctx, "sqlite3", "../../../data/bbgo_test.sqlite3") + info, err := os.Stat(sqliteDbFile) assert.NoError(t, err) + t.Logf("sqlite: %+v", info) + + err = environ.ConfigureDatabaseDriver(ctx, "sqlite3", sqliteDbFile) + if !assert.NoError(t, err) { + return + } backtestService := &service.BacktestService{DB: environ.DatabaseService.DB} defer func() { @@ -397,22 +412,24 @@ func TestBacktestStrategy(t *testing.T) { bbgo.SetBackTesting(backtestService) defer bbgo.SetBackTesting(nil) - exName, err := types.ValidExchangeName("binance") + exName, err := types.ValidExchangeName(backtestExchangeName) if !assert.NoError(t, err) { return } + t.Logf("using exchange source: %s", exName) + publicExchange, err := exchange.NewPublic(exName) if !assert.NoError(t, err) { return } - backtestExchange, err := backtest.NewExchange(publicExchange.Name(), publicExchange, backtestService, backtestConfig) + backtestExchange, err := backtest.NewExchange(exName, publicExchange, backtestService, backtestConfig) if !assert.NoError(t, err) { return } - session := environ.AddExchange(exName.String(), backtestExchange) + session := environ.AddExchange(backtestExchangeName, backtestExchange) assert.NotNil(t, session) err = environ.Init(ctx) @@ -430,11 +447,11 @@ func TestBacktestStrategy(t *testing.T) { trader.DisableLogging() } - // TODO: add grid2 to the user config and run backtest userConfig := &bbgo.Config{ + Backtest: backtestConfig, ExchangeStrategies: []bbgo.ExchangeStrategyMount{ { - Mounts: []string{"binance"}, + Mounts: []string{backtestExchangeName}, Strategy: strategy, }, }, @@ -446,7 +463,32 @@ func TestBacktestStrategy(t *testing.T) { err = trader.Run(ctx) assert.NoError(t, err) - // TODO: feed data + allKLineIntervals, requiredInterval, backTestIntervals := backtest.CollectSubscriptionIntervals(environ) + t.Logf("requiredInterval: %s backTestIntervals: %v", requiredInterval, backTestIntervals) + + _ = allKLineIntervals + exchangeSources, err := backtest.InitializeExchangeSources(environ.Sessions(), startTime.Time(), endTime.Time(), requiredInterval, backTestIntervals...) + if !assert.NoError(t, err) { + return + } + + doneC := make(chan struct{}) + go func() { + count := 0 + exSource := exchangeSources[0] + for k := range exSource.C { + exSource.Exchange.ConsumeKLine(k, requiredInterval) + count++ + } + + err = exSource.Exchange.CloseMarketData() + assert.NoError(t, err) + + assert.Greater(t, count, 0, "kLines count must be greater than 0, please check your backtest date range and symbol settings") + + close(doneC) + }() + <-doneC // }}} } From 555d2c50460cbe252f5a2840ed445cfa2825f084 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 15:46:04 +0800 Subject: [PATCH 0227/1392] mocks: add mocks --- pkg/types/mocks/mock_exchange_order_query.go | 66 ++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 pkg/types/mocks/mock_exchange_order_query.go diff --git a/pkg/types/mocks/mock_exchange_order_query.go b/pkg/types/mocks/mock_exchange_order_query.go new file mode 100644 index 0000000000..2e9a9fb177 --- /dev/null +++ b/pkg/types/mocks/mock_exchange_order_query.go @@ -0,0 +1,66 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/c9s/bbgo/pkg/types (interfaces: ExchangeOrderQueryService) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + types "github.com/c9s/bbgo/pkg/types" + gomock "github.com/golang/mock/gomock" +) + +// MockExchangeOrderQueryService is a mock of ExchangeOrderQueryService interface. +type MockExchangeOrderQueryService struct { + ctrl *gomock.Controller + recorder *MockExchangeOrderQueryServiceMockRecorder +} + +// MockExchangeOrderQueryServiceMockRecorder is the mock recorder for MockExchangeOrderQueryService. +type MockExchangeOrderQueryServiceMockRecorder struct { + mock *MockExchangeOrderQueryService +} + +// NewMockExchangeOrderQueryService creates a new mock instance. +func NewMockExchangeOrderQueryService(ctrl *gomock.Controller) *MockExchangeOrderQueryService { + mock := &MockExchangeOrderQueryService{ctrl: ctrl} + mock.recorder = &MockExchangeOrderQueryServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchangeOrderQueryService) EXPECT() *MockExchangeOrderQueryServiceMockRecorder { + return m.recorder +} + +// QueryOrder mocks base method. +func (m *MockExchangeOrderQueryService) QueryOrder(arg0 context.Context, arg1 types.OrderQuery) (*types.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryOrder", arg0, arg1) + ret0, _ := ret[0].(*types.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryOrder indicates an expected call of QueryOrder. +func (mr *MockExchangeOrderQueryServiceMockRecorder) QueryOrder(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOrder", reflect.TypeOf((*MockExchangeOrderQueryService)(nil).QueryOrder), arg0, arg1) +} + +// QueryOrderTrades mocks base method. +func (m *MockExchangeOrderQueryService) QueryOrderTrades(arg0 context.Context, arg1 types.OrderQuery) ([]types.Trade, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryOrderTrades", arg0, arg1) + ret0, _ := ret[0].([]types.Trade) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryOrderTrades indicates an expected call of QueryOrderTrades. +func (mr *MockExchangeOrderQueryServiceMockRecorder) QueryOrderTrades(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOrderTrades", reflect.TypeOf((*MockExchangeOrderQueryService)(nil).QueryOrderTrades), arg0, arg1) +} From 3d0cfd16b536cebd21ac460761bae4ee3e0399b5 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 15:21:22 +0800 Subject: [PATCH 0228/1392] grid2: add test case for aggregateOrderBaseFee --- pkg/strategy/grid2/backtest_test.go | 154 +++++++++++++++++++ pkg/strategy/grid2/strategy_test.go | 220 +++++++++------------------- pkg/types/exchange.go | 1 + 3 files changed, 226 insertions(+), 149 deletions(-) create mode 100644 pkg/strategy/grid2/backtest_test.go diff --git a/pkg/strategy/grid2/backtest_test.go b/pkg/strategy/grid2/backtest_test.go new file mode 100644 index 0000000000..1d6aa23071 --- /dev/null +++ b/pkg/strategy/grid2/backtest_test.go @@ -0,0 +1,154 @@ +package grid2 + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/backtest" + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/exchange" + "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/types" +) + +func RunBacktest(t *testing.T, strategy bbgo.SingleExchangeStrategy) { + // TEMPLATE {{{ start backtest + const sqliteDbFile = "../../../data/bbgo_test.sqlite3" + const backtestExchangeName = "binance" + const backtestStartTime = "2022-06-01" + const backtestEndTime = "2022-06-30" + + startTime, err := types.ParseLooseFormatTime(backtestStartTime) + assert.NoError(t, err) + + endTime, err := types.ParseLooseFormatTime(backtestEndTime) + assert.NoError(t, err) + + backtestConfig := &bbgo.Backtest{ + StartTime: startTime, + EndTime: &endTime, + RecordTrades: false, + FeeMode: bbgo.BacktestFeeModeToken, + Accounts: map[string]bbgo.BacktestAccount{ + backtestExchangeName: { + MakerFeeRate: number(0.075 * 0.01), + TakerFeeRate: number(0.075 * 0.01), + Balances: bbgo.BacktestAccountBalanceMap{ + "USDT": number(10_000.0), + "BTC": number(1.0), + }, + }, + }, + Symbols: []string{"BTCUSDT"}, + Sessions: []string{backtestExchangeName}, + SyncSecKLines: false, + } + + t.Logf("backtestConfig: %+v", backtestConfig) + + ctx := context.Background() + environ := bbgo.NewEnvironment() + environ.SetStartTime(startTime.Time()) + + info, err := os.Stat(sqliteDbFile) + assert.NoError(t, err) + t.Logf("sqlite: %+v", info) + + err = environ.ConfigureDatabaseDriver(ctx, "sqlite3", sqliteDbFile) + if !assert.NoError(t, err) { + return + } + + backtestService := &service.BacktestService{DB: environ.DatabaseService.DB} + defer func() { + err := environ.DatabaseService.DB.Close() + assert.NoError(t, err) + }() + + environ.BacktestService = backtestService + bbgo.SetBackTesting(backtestService) + defer bbgo.SetBackTesting(nil) + + exName, err := types.ValidExchangeName(backtestExchangeName) + if !assert.NoError(t, err) { + return + } + + t.Logf("using exchange source: %s", exName) + + publicExchange, err := exchange.NewPublic(exName) + if !assert.NoError(t, err) { + return + } + + backtestExchange, err := backtest.NewExchange(exName, publicExchange, backtestService, backtestConfig) + if !assert.NoError(t, err) { + return + } + + session := environ.AddExchange(backtestExchangeName, backtestExchange) + assert.NotNil(t, session) + + err = environ.Init(ctx) + assert.NoError(t, err) + + for _, ses := range environ.Sessions() { + userDataStream := ses.UserDataStream.(types.StandardStreamEmitter) + backtestEx := ses.Exchange.(*backtest.Exchange) + backtestEx.MarketDataStream = ses.MarketDataStream.(types.StandardStreamEmitter) + backtestEx.BindUserData(userDataStream) + } + + trader := bbgo.NewTrader(environ) + if assert.NotNil(t, trader) { + trader.DisableLogging() + } + + userConfig := &bbgo.Config{ + Backtest: backtestConfig, + ExchangeStrategies: []bbgo.ExchangeStrategyMount{ + { + Mounts: []string{backtestExchangeName}, + Strategy: strategy, + }, + }, + } + + err = trader.Configure(userConfig) + assert.NoError(t, err) + + err = trader.Run(ctx) + assert.NoError(t, err) + + allKLineIntervals, requiredInterval, backTestIntervals := backtest.CollectSubscriptionIntervals(environ) + t.Logf("requiredInterval: %s backTestIntervals: %v", requiredInterval, backTestIntervals) + + _ = allKLineIntervals + exchangeSources, err := backtest.InitializeExchangeSources(environ.Sessions(), startTime.Time(), endTime.Time(), requiredInterval, backTestIntervals...) + if !assert.NoError(t, err) { + return + } + + doneC := make(chan struct{}) + go func() { + count := 0 + exSource := exchangeSources[0] + for k := range exSource.C { + exSource.Exchange.ConsumeKLine(k, requiredInterval) + count++ + } + + err = exSource.Exchange.CloseMarketData() + assert.NoError(t, err) + + assert.Greater(t, count, 0, "kLines count must be greater than 0, please check your backtest date range and symbol settings") + + close(doneC) + }() + + <-doneC + // }}} +} diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 3912e98d50..4ea85258df 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -4,18 +4,16 @@ package grid2 import ( "context" - "os" "testing" + "github.com/golang/mock/gomock" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" - "github.com/c9s/bbgo/pkg/backtest" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/mocks" ) func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { @@ -267,12 +265,13 @@ func newTestStrategy() *Strategy { } s := &Strategy{ - logger: logrus.NewEntry(logrus.New()), - Market: market, - GridProfitStats: newGridProfitStats(market), - UpperPrice: number(20_000), - LowerPrice: number(10_000), - GridNum: 10, + logger: logrus.NewEntry(logrus.New()), + Market: market, + GridProfitStats: newGridProfitStats(market), + UpperPrice: number(20_000), + LowerPrice: number(10_000), + GridNum: 10, + historicalTrades: bbgo.NewTradeStore(), // QuoteInvestment: number(9000.0), } @@ -333,6 +332,68 @@ func TestStrategy_calculateProfit(t *testing.T) { }) } +func TestStrategy_aggregateOrderBaseFee(t *testing.T) { + s := newTestStrategy() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl) + s.orderQueryService = mockService + + ctx := context.Background() + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "3", + }).Return([]types.Trade{ + { + ID: 1, + OrderID: 3, + Exchange: "binance", + Price: number(20000.0), + Quantity: number(0.2), + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + FeeCurrency: "BTC", + Fee: number(0.2 * 0.01), + }, + { + ID: 1, + OrderID: 3, + Exchange: "binance", + Price: number(20000.0), + Quantity: number(0.8), + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + FeeCurrency: "BTC", + Fee: number(0.8 * 0.01), + }, + }, nil) + + baseFee := s.aggregateOrderBaseFee(types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(1.0), + Price: number(20000.0), + AveragePrice: number(0), + StopPrice: number(0), + Market: types.Market{}, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "binance", + GID: 1, + OrderID: 3, + Status: types.OrderStatusFilled, + ExecutedQuantity: number(1.0), + IsWorking: false, + }) + assert.Equal(t, "0.01", baseFee.String()) +} + func TestBacktestStrategy(t *testing.T) { market := types.Market{ BaseCurrency: "BTC", @@ -353,142 +414,3 @@ func TestBacktestStrategy(t *testing.T) { } RunBacktest(t, strategy) } - -func RunBacktest(t *testing.T, strategy bbgo.SingleExchangeStrategy) { - // TEMPLATE {{{ start backtest - const sqliteDbFile = "../../../data/bbgo_test.sqlite3" - const backtestExchangeName = "binance" - const backtestStartTime = "2022-06-01" - const backtestEndTime = "2022-06-30" - - startTime, err := types.ParseLooseFormatTime(backtestStartTime) - assert.NoError(t, err) - - endTime, err := types.ParseLooseFormatTime(backtestEndTime) - assert.NoError(t, err) - - backtestConfig := &bbgo.Backtest{ - StartTime: startTime, - EndTime: &endTime, - RecordTrades: false, - FeeMode: bbgo.BacktestFeeModeToken, - Accounts: map[string]bbgo.BacktestAccount{ - backtestExchangeName: { - MakerFeeRate: number(0.075 * 0.01), - TakerFeeRate: number(0.075 * 0.01), - Balances: bbgo.BacktestAccountBalanceMap{ - "USDT": number(10_000.0), - "BTC": number(1.0), - }, - }, - }, - Symbols: []string{"BTCUSDT"}, - Sessions: []string{backtestExchangeName}, - SyncSecKLines: false, - } - - t.Logf("backtestConfig: %+v", backtestConfig) - - ctx := context.Background() - environ := bbgo.NewEnvironment() - environ.SetStartTime(startTime.Time()) - - info, err := os.Stat(sqliteDbFile) - assert.NoError(t, err) - t.Logf("sqlite: %+v", info) - - err = environ.ConfigureDatabaseDriver(ctx, "sqlite3", sqliteDbFile) - if !assert.NoError(t, err) { - return - } - - backtestService := &service.BacktestService{DB: environ.DatabaseService.DB} - defer func() { - err := environ.DatabaseService.DB.Close() - assert.NoError(t, err) - }() - - environ.BacktestService = backtestService - bbgo.SetBackTesting(backtestService) - defer bbgo.SetBackTesting(nil) - - exName, err := types.ValidExchangeName(backtestExchangeName) - if !assert.NoError(t, err) { - return - } - - t.Logf("using exchange source: %s", exName) - - publicExchange, err := exchange.NewPublic(exName) - if !assert.NoError(t, err) { - return - } - - backtestExchange, err := backtest.NewExchange(exName, publicExchange, backtestService, backtestConfig) - if !assert.NoError(t, err) { - return - } - - session := environ.AddExchange(backtestExchangeName, backtestExchange) - assert.NotNil(t, session) - - err = environ.Init(ctx) - assert.NoError(t, err) - - for _, ses := range environ.Sessions() { - userDataStream := ses.UserDataStream.(types.StandardStreamEmitter) - backtestEx := ses.Exchange.(*backtest.Exchange) - backtestEx.MarketDataStream = ses.MarketDataStream.(types.StandardStreamEmitter) - backtestEx.BindUserData(userDataStream) - } - - trader := bbgo.NewTrader(environ) - if assert.NotNil(t, trader) { - trader.DisableLogging() - } - - userConfig := &bbgo.Config{ - Backtest: backtestConfig, - ExchangeStrategies: []bbgo.ExchangeStrategyMount{ - { - Mounts: []string{backtestExchangeName}, - Strategy: strategy, - }, - }, - } - - err = trader.Configure(userConfig) - assert.NoError(t, err) - - err = trader.Run(ctx) - assert.NoError(t, err) - - allKLineIntervals, requiredInterval, backTestIntervals := backtest.CollectSubscriptionIntervals(environ) - t.Logf("requiredInterval: %s backTestIntervals: %v", requiredInterval, backTestIntervals) - - _ = allKLineIntervals - exchangeSources, err := backtest.InitializeExchangeSources(environ.Sessions(), startTime.Time(), endTime.Time(), requiredInterval, backTestIntervals...) - if !assert.NoError(t, err) { - return - } - - doneC := make(chan struct{}) - go func() { - count := 0 - exSource := exchangeSources[0] - for k := range exSource.C { - exSource.Exchange.ConsumeKLine(k, requiredInterval) - count++ - } - - err = exSource.Exchange.CloseMarketData() - assert.NoError(t, err) - - assert.Greater(t, count, 0, "kLines count must be greater than 0, please check your backtest date range and symbol settings") - - close(doneC) - }() - - <-doneC - // }}} -} diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index bae287c79d..cab368a420 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -82,6 +82,7 @@ type Exchange interface { } // ExchangeOrderQueryService provides an interface for querying the order status via order ID or client order ID +//go:generate mockgen -destination=mocks/mock_exchange_order_query.go -package=mocks . ExchangeOrderQueryService type ExchangeOrderQueryService interface { QueryOrder(ctx context.Context, q OrderQuery) (*Order, error) QueryOrderTrades(ctx context.Context, q OrderQuery) ([]Trade, error) From 482b6f5e7beb9ffd7023230caddfc5c4767f7ed3 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 15:36:40 +0800 Subject: [PATCH 0229/1392] grid2: add test case for aggregateOrderBaseFee Retry --- pkg/strategy/grid2/strategy_test.go | 68 +++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 4ea85258df..8ab5ddf67b 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -4,6 +4,7 @@ package grid2 import ( "context" + "errors" "testing" "github.com/golang/mock/gomock" @@ -394,6 +395,73 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) { assert.Equal(t, "0.01", baseFee.String()) } +func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { + s := newTestStrategy() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl) + s.orderQueryService = mockService + + ctx := context.Background() + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "3", + }).Return(nil, errors.New("api error")) + + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "3", + }).Return([]types.Trade{ + { + ID: 1, + OrderID: 3, + Exchange: "binance", + Price: number(20000.0), + Quantity: number(0.2), + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + FeeCurrency: "BTC", + Fee: number(0.2 * 0.01), + }, + { + ID: 1, + OrderID: 3, + Exchange: "binance", + Price: number(20000.0), + Quantity: number(0.8), + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + FeeCurrency: "BTC", + Fee: number(0.8 * 0.01), + }, + }, nil) + + baseFee := s.aggregateOrderBaseFee(types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(1.0), + Price: number(20000.0), + AveragePrice: number(0), + StopPrice: number(0), + Market: types.Market{}, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "binance", + GID: 1, + OrderID: 3, + Status: types.OrderStatusFilled, + ExecutedQuantity: number(1.0), + IsWorking: false, + }) + assert.Equal(t, "0.01", baseFee.String()) +} + func TestBacktestStrategy(t *testing.T) { market := types.Market{ BaseCurrency: "BTC", From 423fe521b640ac0c2512c735c290fcdbf8ecb76c Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 15:55:52 +0800 Subject: [PATCH 0230/1392] grid2: add build tag for backtest_test --- pkg/strategy/grid2/backtest_test.go | 2 ++ pkg/strategy/grid2/strategy.go | 31 +++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/backtest_test.go b/pkg/strategy/grid2/backtest_test.go index 1d6aa23071..8c75f88c9b 100644 --- a/pkg/strategy/grid2/backtest_test.go +++ b/pkg/strategy/grid2/backtest_test.go @@ -1,3 +1,5 @@ +//go:build !dnum + package grid2 import ( diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 659529f22a..7e178b9994 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -128,6 +128,10 @@ func (s *Strategy) Validate() error { return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) } + if s.GridNum == 0 { + return fmt.Errorf("gridNum can not be zero") + } + if s.FeeRate.IsZero() { s.FeeRate = fixedpoint.NewFromFloat(0.1 * 0.01) // 0.1%, 0.075% with BNB } @@ -145,10 +149,6 @@ func (s *Strategy) Validate() error { } } - if s.GridNum == 0 { - return fmt.Errorf("gridNum can not be zero") - } - if err := s.QuantityOrAmount.Validate(); err != nil { if s.QuoteInvestment.IsZero() && s.BaseInvestment.IsZero() { return err @@ -936,6 +936,22 @@ func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.Exchange return fixedpoint.Zero, fmt.Errorf("%s ticker price not found", s.Symbol) } +func (s *Strategy) checkMinimalQuoteInvestment() error { + gridNum := fixedpoint.NewFromInt(s.GridNum) + // gridSpread := s.UpperPrice.Sub(s.LowerPrice).Div(gridNum) + minimalAmountLowerPrice := fixedpoint.Max(s.LowerPrice.Mul(s.Market.MinQuantity), s.Market.MinNotional) + minimalAmountUpperPrice := fixedpoint.Max(s.UpperPrice.Mul(s.Market.MinQuantity), s.Market.MinNotional) + minimalQuoteInvestment := fixedpoint.Max(minimalAmountLowerPrice, minimalAmountUpperPrice).Mul(gridNum) + if s.QuoteInvestment.Compare(minimalQuoteInvestment) <= 0 { + return fmt.Errorf("need at least %f %s for quote investment, %f %s given", + minimalQuoteInvestment.Float64(), + s.Market.QuoteCurrency, + s.QuoteInvestment.Float64(), + s.Market.QuoteCurrency) + } + return nil +} + func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() @@ -972,6 +988,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position.Reset() } + // we need to check the minimal quote investment here, because we need the market info + if s.QuoteInvestment.Sign() > 0 { + if err := s.checkMinimalQuoteInvestment(); err != nil { + return err + } + } + s.historicalTrades = bbgo.NewTradeStore() s.historicalTrades.EnablePrune = true s.historicalTrades.BindStream(session.UserDataStream) From b4e403d6326e3327f0b45989da814076caf9d640 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 15:39:37 +0800 Subject: [PATCH 0231/1392] grid2: remove fee check from verifyOrderTrades --- pkg/strategy/grid2/strategy.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7e178b9994..ec5174e007 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -225,11 +225,6 @@ func collectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { tq := fixedpoint.Zero for _, t := range trades { - if t.Fee.IsZero() && t.FeeCurrency == "" { - s.logger.Warnf("trade fee and feeCurrency is zero: %+v", t) - return false - } - tq = tq.Add(t.Quantity) } From 0cf43ffb112a7c7f66dee7236f8a9d2be57c8e26 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 15:40:57 +0800 Subject: [PATCH 0232/1392] grid2: pull out aggregateTradesQuantity func --- pkg/strategy/grid2/strategy.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index ec5174e007..76448dcc2d 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -222,11 +222,16 @@ func collectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { return fees } -func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { +func aggregateTradesQuantity(trades []types.Trade) fixedpoint.Value { tq := fixedpoint.Zero for _, t := range trades { tq = tq.Add(t.Quantity) } + return tq +} + +func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { + tq := aggregateTradesQuantity(trades) if tq.Compare(o.Quantity) != 0 { s.logger.Warnf("order trades missing. expected: %f actual: %f", From 47759236e06e5058e517460b5e2df000919eded9 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 15:45:28 +0800 Subject: [PATCH 0233/1392] grid2: improve log --- pkg/strategy/grid2/strategy.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 76448dcc2d..793dee65aa 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -363,7 +363,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { Tag: "grid", } - s.logger.Infof("SUBMIT ORDER: %s", orderForm.String()) + s.logger.Infof("SUBMIT GRID REVERSE ORDER: %s", orderForm.String()) if createdOrders, err := s.orderExecutor.SubmitOrders(context.Background(), orderForm); err != nil { s.logger.WithError(err).Errorf("can not submit arbitrage order") @@ -372,13 +372,6 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } } -type InvestmentBudget struct { - baseInvestment fixedpoint.Value - quoteInvestment fixedpoint.Value - baseBalance fixedpoint.Value - quoteBalance fixedpoint.Value -} - func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { // check more investment budget details requiredBase = fixedpoint.Zero From b8e5bf1dddce7dac1e93ff60133dfe02f2d0272b Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 16:09:46 +0800 Subject: [PATCH 0234/1392] grid2: add test case for testing checkMinimalQuoteInvestment --- pkg/strategy/grid2/strategy.go | 13 ++++++++----- pkg/strategy/grid2/strategy_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 793dee65aa..b6258d41c9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -929,12 +929,15 @@ func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.Exchange return fixedpoint.Zero, fmt.Errorf("%s ticker price not found", s.Symbol) } +func calculateMinimalQuoteInvestment(market types.Market, lowerPrice, upperPrice fixedpoint.Value, gridNum int64) fixedpoint.Value { + num := fixedpoint.NewFromInt(gridNum) + minimalAmountLowerPrice := fixedpoint.Max(lowerPrice.Mul(market.MinQuantity), market.MinNotional) + minimalAmountUpperPrice := fixedpoint.Max(upperPrice.Mul(market.MinQuantity), market.MinNotional) + return fixedpoint.Max(minimalAmountLowerPrice, minimalAmountUpperPrice).Mul(num) +} + func (s *Strategy) checkMinimalQuoteInvestment() error { - gridNum := fixedpoint.NewFromInt(s.GridNum) - // gridSpread := s.UpperPrice.Sub(s.LowerPrice).Div(gridNum) - minimalAmountLowerPrice := fixedpoint.Max(s.LowerPrice.Mul(s.Market.MinQuantity), s.Market.MinNotional) - minimalAmountUpperPrice := fixedpoint.Max(s.UpperPrice.Mul(s.Market.MinQuantity), s.Market.MinNotional) - minimalQuoteInvestment := fixedpoint.Max(minimalAmountLowerPrice, minimalAmountUpperPrice).Mul(gridNum) + minimalQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, s.LowerPrice, s.UpperPrice, s.GridNum) if s.QuoteInvestment.Compare(minimalQuoteInvestment) <= 0 { return fmt.Errorf("need at least %f %s for quote investment, %f %s given", minimalQuoteInvestment.Float64(), diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 8ab5ddf67b..b3bf7998cf 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -263,6 +263,8 @@ func newTestStrategy() *Strategy { TickSize: number(0.01), PricePrecision: 2, VolumePrecision: 8, + MinNotional: number(10.0), + MinQuantity: number(0.001), } s := &Strategy{ @@ -462,6 +464,31 @@ func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { assert.Equal(t, "0.01", baseFee.String()) } +func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { + s := newTestStrategy() + + t.Run("10 grids", func(t *testing.T) { + s.QuoteInvestment = number(10_000) + s.GridNum = 10 + minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, s.LowerPrice, s.UpperPrice, s.GridNum) + assert.Equal(t, "20", minQuoteInvestment.String()) + + err := s.checkMinimalQuoteInvestment() + assert.NoError(t, err) + }) + + t.Run("1000 grids", func(t *testing.T) { + s.QuoteInvestment = number(10_000) + s.GridNum = 1000 + minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, s.LowerPrice, s.UpperPrice, s.GridNum) + assert.Equal(t, "20000", minQuoteInvestment.String()) + + err := s.checkMinimalQuoteInvestment() + assert.Error(t, err) + assert.EqualError(t, err, "need at least 20000.000000 USDT for quote investment, 10000.000000 USDT given") + }) +} + func TestBacktestStrategy(t *testing.T) { market := types.Market{ BaseCurrency: "BTC", From b0381fd92727a796b876952e1af594631cfdb4a3 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 16:35:52 +0800 Subject: [PATCH 0235/1392] grid2: pull out debugGridOrders func --- pkg/strategy/grid2/strategy.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b6258d41c9..ddeb411cca 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -788,10 +788,18 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return err } - // debug info + s.debugGridOrders(submitOrders, lastPrice) + + for _, order := range createdOrders { + s.logger.Info(order.String()) + } + + return nil +} + +func (s *Strategy) debugGridOrders(submitOrders []types.SubmitOrder, lastPrice fixedpoint.Value) { s.logger.Infof("GRID ORDERS: [") for i, order := range submitOrders { - if i > 0 && lastPrice.Compare(order.Price) >= 0 && lastPrice.Compare(submitOrders[i-1].Price) <= 0 { s.logger.Infof(" - LAST PRICE: %f", lastPrice.Float64()) } @@ -799,12 +807,6 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) s.logger.Info(" - ", order.String()) } s.logger.Infof("] END OF GRID ORDERS") - - for _, order := range createdOrders { - s.logger.Info(order.String()) - } - - return nil } func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoint.Value) ([]types.SubmitOrder, error) { From 46d1207adbbebf4753bbdd53fb18b1ab1f03a5b4 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 16:37:08 +0800 Subject: [PATCH 0236/1392] grid2: fix TestStrategy_checkMinimalQuoteInvestment --- pkg/strategy/grid2/strategy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index b3bf7998cf..53bde8185c 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -471,7 +471,7 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { s.QuoteInvestment = number(10_000) s.GridNum = 10 minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, s.LowerPrice, s.UpperPrice, s.GridNum) - assert.Equal(t, "20", minQuoteInvestment.String()) + assert.Equal(t, "200", minQuoteInvestment.String()) err := s.checkMinimalQuoteInvestment() assert.NoError(t, err) From e1e521cec5d8607ed6b3eba332fb8c78a1f3d822 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Dec 2022 16:38:12 +0800 Subject: [PATCH 0237/1392] grid2: add comment to the minimal quote investment test --- pkg/strategy/grid2/strategy_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 53bde8185c..adcd8de1b5 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -468,6 +468,9 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { s := newTestStrategy() t.Run("10 grids", func(t *testing.T) { + // 10_000 * 0.001 = 10USDT + // 20_000 * 0.001 = 20USDT + // hence we should have at least: 20USDT * 10 grids s.QuoteInvestment = number(10_000) s.GridNum = 10 minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, s.LowerPrice, s.UpperPrice, s.GridNum) From 02bebe8ed1050fdecfa1c50f65b672e7b451f9f4 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Dec 2022 11:44:22 +0800 Subject: [PATCH 0238/1392] grid2: use min quantity instead of max quantity --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index ddeb411cca..dd93d61785 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -606,7 +606,7 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) if maxNumberOfSellOrders > 0 { - return fixedpoint.Max(quoteSideQuantity, maxBaseQuantity), nil + return fixedpoint.Min(quoteSideQuantity, maxBaseQuantity), nil } return quoteSideQuantity, nil From 489b025702dc56e92f794831a8d989ac41b77246 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Dec 2022 12:24:52 +0800 Subject: [PATCH 0239/1392] grid2: refactor check spread --- config/grid2-max.yaml | 6 +++--- pkg/strategy/grid2/strategy.go | 39 ++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index f9c751cb42..dea3da8097 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -36,7 +36,7 @@ exchangeStrategies: symbol: BTCUSDT upperPrice: 18_000.0 lowerPrice: 16_000.0 - gridNumber: 200 + gridNumber: 100 ## compound is used for buying more inventory when the profit is made by the filled SELL order. ## when compound is disabled, fixed quantity is used for each grid order. @@ -70,12 +70,12 @@ exchangeStrategies: # amount: 10.0 ## 2) fixed quantity: it will use your balance to place orders with the fixed quantity. e.g. 0.001 BTC - # quantity: 0.001 + quantity: 0.001 ## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid. ## quoteInvestment is required, and baseInvestment is optional (could be zero) ## if you have existing BTC position and want to reuse it you can set the baseInvestment. - quoteInvestment: 10_000 + # quoteInvestment: 10_000 # baseInvestment: 1.0 feeRate: 0.075% diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index dd93d61785..b484533b06 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -136,17 +136,8 @@ func (s *Strategy) Validate() error { s.FeeRate = fixedpoint.NewFromFloat(0.1 * 0.01) // 0.1%, 0.075% with BNB } - if !s.ProfitSpread.IsZero() { - // the min fee rate from 2 maker/taker orders (with 0.1 rate for profit) - gridFeeRate := s.FeeRate.Mul(fixedpoint.NewFromFloat(2.01)) - - if s.ProfitSpread.Div(s.LowerPrice).Compare(gridFeeRate) < 0 { - return fmt.Errorf("profitSpread %f %s is too small for lower price, less than the fee rate: %s", s.ProfitSpread.Float64(), s.ProfitSpread.Div(s.LowerPrice).Percentage(), s.FeeRate.Percentage()) - } - - if s.ProfitSpread.Div(s.UpperPrice).Compare(gridFeeRate) < 0 { - return fmt.Errorf("profitSpread %f %s is too small for upper price, less than the fee rate: %s", s.ProfitSpread.Float64(), s.ProfitSpread.Div(s.UpperPrice).Percentage(), s.FeeRate.Percentage()) - } + if err := s.checkSpread(); err != nil { + return err } if err := s.QuantityOrAmount.Validate(); err != nil { @@ -171,6 +162,32 @@ func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s-%s-%d-%d-%d", ID, s.Symbol, s.GridNum, s.UpperPrice.Int(), s.LowerPrice.Int()) } +func (s *Strategy) checkSpread() error { + gridNum := fixedpoint.NewFromInt(s.GridNum) + spread := s.ProfitSpread + if spread.IsZero() { + spread = s.UpperPrice.Sub(s.LowerPrice).Div(gridNum) + } + + feeRate := s.FeeRate + if feeRate.IsZero() { + feeRate = fixedpoint.NewFromFloat(0.075 * 0.01) + } + + // the min fee rate from 2 maker/taker orders (with 0.1 rate for profit) + gridFeeRate := feeRate.Mul(fixedpoint.NewFromFloat(2.01)) + + if spread.Div(s.LowerPrice).Compare(gridFeeRate) < 0 { + return fmt.Errorf("profitSpread %f %s is too small for lower price, less than the grid fee rate: %s", spread.Float64(), spread.Div(s.LowerPrice).Percentage(), gridFeeRate.Percentage()) + } + + if spread.Div(s.UpperPrice).Compare(gridFeeRate) < 0 { + return fmt.Errorf("profitSpread %f %s is too small for upper price, less than the grid fee rate: %s", spread.Float64(), spread.Div(s.UpperPrice).Percentage(), gridFeeRate.Percentage()) + } + + return nil +} + func (s *Strategy) handleOrderCanceled(o types.Order) { s.logger.Infof("GRID ORDER CANCELED: %s", o.String()) From df6187dc988a6027808ba842d77159b9d03d4951 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Dec 2022 12:25:30 +0800 Subject: [PATCH 0240/1392] grid2: remove default fee rate --- pkg/strategy/grid2/strategy.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b484533b06..2d038c76e9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -132,10 +132,6 @@ func (s *Strategy) Validate() error { return fmt.Errorf("gridNum can not be zero") } - if s.FeeRate.IsZero() { - s.FeeRate = fixedpoint.NewFromFloat(0.1 * 0.01) // 0.1%, 0.075% with BNB - } - if err := s.checkSpread(); err != nil { return err } From 9215e401d0c3fa600a9905c2bc3a26db45c1935e Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Dec 2022 12:29:14 +0800 Subject: [PATCH 0241/1392] grid2: fix quantity, amount, quoteInvestment validation --- pkg/strategy/grid2/strategy.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2d038c76e9..a8e58f00c0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -133,17 +133,11 @@ func (s *Strategy) Validate() error { } if err := s.checkSpread(); err != nil { - return err - } - - if err := s.QuantityOrAmount.Validate(); err != nil { - if s.QuoteInvestment.IsZero() && s.BaseInvestment.IsZero() { - return err - } + return errors.Wrapf(err, "spread is too small, please try to reduce your gridNum or increase the price range (upperPrice and lowerPrice)") } - if !s.QuantityOrAmount.IsSet() && s.QuoteInvestment.IsZero() && s.BaseInvestment.IsZero() { - return fmt.Errorf("one of quantity, amount, quoteInvestment must be set") + if !s.QuantityOrAmount.IsSet() && s.QuoteInvestment.IsZero() { + return fmt.Errorf("either quantity, amount or quoteInvestment must be set") } return nil From 9d24540826b432098c64bf741e84f135f623563f Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Dec 2022 14:19:49 +0800 Subject: [PATCH 0242/1392] grid2: add order executor mock for testing reverse order --- pkg/strategy/grid2/mocks/order_executor.go | 95 ++++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 38 +++++---- pkg/strategy/grid2/strategy_test.go | 72 ++++++++++++++++ 3 files changed, 191 insertions(+), 14 deletions(-) create mode 100644 pkg/strategy/grid2/mocks/order_executor.go diff --git a/pkg/strategy/grid2/mocks/order_executor.go b/pkg/strategy/grid2/mocks/order_executor.go new file mode 100644 index 0000000000..0d25bc1d68 --- /dev/null +++ b/pkg/strategy/grid2/mocks/order_executor.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/c9s/bbgo/pkg/strategy/grid2 (interfaces: OrderExecutor) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + fixedpoint "github.com/c9s/bbgo/pkg/fixedpoint" + types "github.com/c9s/bbgo/pkg/types" + gomock "github.com/golang/mock/gomock" +) + +// MockOrderExecutor is a mock of OrderExecutor interface. +type MockOrderExecutor struct { + ctrl *gomock.Controller + recorder *MockOrderExecutorMockRecorder +} + +// MockOrderExecutorMockRecorder is the mock recorder for MockOrderExecutor. +type MockOrderExecutorMockRecorder struct { + mock *MockOrderExecutor +} + +// NewMockOrderExecutor creates a new mock instance. +func NewMockOrderExecutor(ctrl *gomock.Controller) *MockOrderExecutor { + mock := &MockOrderExecutor{ctrl: ctrl} + mock.recorder = &MockOrderExecutorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOrderExecutor) EXPECT() *MockOrderExecutorMockRecorder { + return m.recorder +} + +// ClosePosition mocks base method. +func (m *MockOrderExecutor) ClosePosition(arg0 context.Context, arg1 fixedpoint.Value, arg2 ...string) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ClosePosition", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// ClosePosition indicates an expected call of ClosePosition. +func (mr *MockOrderExecutorMockRecorder) ClosePosition(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClosePosition", reflect.TypeOf((*MockOrderExecutor)(nil).ClosePosition), varargs...) +} + +// GracefulCancel mocks base method. +func (m *MockOrderExecutor) GracefulCancel(arg0 context.Context, arg1 ...types.Order) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GracefulCancel", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// GracefulCancel indicates an expected call of GracefulCancel. +func (mr *MockOrderExecutorMockRecorder) GracefulCancel(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GracefulCancel", reflect.TypeOf((*MockOrderExecutor)(nil).GracefulCancel), varargs...) +} + +// SubmitOrders mocks base method. +func (m *MockOrderExecutor) SubmitOrders(arg0 context.Context, arg1 ...types.SubmitOrder) (types.OrderSlice, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SubmitOrders", varargs...) + ret0, _ := ret[0].(types.OrderSlice) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubmitOrders indicates an expected call of SubmitOrders. +func (mr *MockOrderExecutorMockRecorder) SubmitOrders(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitOrders", reflect.TypeOf((*MockOrderExecutor)(nil).SubmitOrders), varargs...) +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index a8e58f00c0..8cb34f488b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -28,6 +28,13 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +//go:generate mockgen -destination=mocks/order_executor.go -package=mocks . OrderExecutor +type OrderExecutor interface { + SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) + ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error + GracefulCancel(ctx context.Context, orders ...types.Order) error +} + type Strategy struct { Environment *bbgo.Environment @@ -102,7 +109,7 @@ type Strategy struct { session *bbgo.ExchangeSession orderQueryService types.ExchangeOrderQueryService - orderExecutor *bbgo.GeneralOrderExecutor + orderExecutor OrderExecutor historicalTrades *bbgo.TradeStore // groupID is the group ID used for the strategy instance for canceling orders @@ -705,6 +712,12 @@ func (s *Strategy) closeGrid(ctx context.Context) error { return nil } +func (s *Strategy) newGrid() *Grid { + grid := NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + grid.CalculateArithmeticPins() + return grid +} + // openGrid // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. @@ -715,9 +728,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return nil } - s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) - s.grid.CalculateArithmeticPins() - + s.grid = s.newGrid() s.logger.Info("OPENING GRID: ", s.grid.String()) lastPrice, err := s.getLastTradePrice(ctx, session) @@ -957,7 +968,7 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { return nil } -func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { +func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() s.session = session @@ -1004,19 +1015,18 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.historicalTrades.EnablePrune = true s.historicalTrades.BindStream(session.UserDataStream) - s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) - s.orderExecutor.BindEnvironment(s.Environment) - s.orderExecutor.BindProfitStats(s.ProfitStats) - s.orderExecutor.Bind() - - s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { + orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + orderExecutor.BindEnvironment(s.Environment) + orderExecutor.BindProfitStats(s.ProfitStats) + orderExecutor.Bind() + orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { s.GridProfitStats.AddTrade(trade) }) - s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) - - s.orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) + orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) + s.orderExecutor = orderExecutor // TODO: detect if there are previous grid orders on the order book if s.ClearOpenOrdersWhenStart { diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index adcd8de1b5..2bd8b91475 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -15,6 +15,8 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types/mocks" + + gridmocks "github.com/c9s/bbgo/pkg/strategy/grid2/mocks" ) func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { @@ -269,6 +271,7 @@ func newTestStrategy() *Strategy { s := &Strategy{ logger: logrus.NewEntry(logrus.New()), + Symbol: "BTCUSDT", Market: market, GridProfitStats: newGridProfitStats(market), UpperPrice: number(20_000), @@ -397,6 +400,75 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) { assert.Equal(t, "0.01", baseFee.String()) } +func TestStrategy_handleOrderFilled(t *testing.T) { + ctx := context.Background() + + t.Run("no fee token", func(t *testing.T) { + gridQuantity := number(0.1) + orderID := uint64(1) + + s := newTestStrategy() + s.Quantity = gridQuantity + s.grid = s.newGrid() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl) + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "1", + }).Return([]types.Trade{ + { + ID: 1, + OrderID: orderID, + Exchange: "binance", + Price: number(11000.0), + Quantity: gridQuantity, + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + FeeCurrency: "BTC", + Fee: number(gridQuantity.Float64() * 0.1 * 0.01), + }, + }, nil) + + s.orderQueryService = mockService + + expectedSubmitOrder := types.SubmitOrder{ + Symbol: "BTCUSDT", + Type: types.OrderTypeLimit, + Price: number(12_000.0), + Quantity: number(0.0999), + Side: types.SideTypeSell, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + Tag: "grid", + } + + orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) + orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{ + {SubmitOrder: expectedSubmitOrder}, + }, nil) + s.orderExecutor = orderExecutor + + s.handleOrderFilled(types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: gridQuantity, + Price: number(11000.0), + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "binance", + OrderID: orderID, + Status: types.OrderStatusFilled, + ExecutedQuantity: gridQuantity, + }) + }) +} + func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { s := newTestStrategy() From 120a22f0cdd472d69373b2d2a628938d11eb936d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Dec 2022 14:42:06 +0800 Subject: [PATCH 0243/1392] grid2: add compound mode order test --- pkg/strategy/grid2/strategy_test.go | 162 ++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 2bd8b91475..ed87ab2aba 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -467,6 +467,168 @@ func TestStrategy_handleOrderFilled(t *testing.T) { ExecutedQuantity: gridQuantity, }) }) + + t.Run("with fee token", func(t *testing.T) { + gridQuantity := number(0.1) + orderID := uint64(1) + + s := newTestStrategy() + s.Quantity = gridQuantity + s.grid = s.newGrid() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl) + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "1", + }).Return([]types.Trade{ + { + ID: 1, + OrderID: orderID, + Exchange: "binance", + Price: number(11000.0), + Quantity: gridQuantity, + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + FeeCurrency: "BTC", + Fee: fixedpoint.Zero, + }, + }, nil) + + s.orderQueryService = mockService + + expectedSubmitOrder := types.SubmitOrder{ + Symbol: "BTCUSDT", + Type: types.OrderTypeLimit, + Price: number(12_000.0), + Quantity: gridQuantity, + Side: types.SideTypeSell, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + Tag: "grid", + } + + orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) + orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{ + {SubmitOrder: expectedSubmitOrder}, + }, nil) + s.orderExecutor = orderExecutor + + s.handleOrderFilled(types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: gridQuantity, + Price: number(11000.0), + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "binance", + OrderID: orderID, + Status: types.OrderStatusFilled, + ExecutedQuantity: gridQuantity, + }) + }) + + t.Run("with fee token and compound", func(t *testing.T) { + gridQuantity := number(0.1) + orderID := uint64(1) + + s := newTestStrategy() + s.Quantity = gridQuantity + s.Compound = true + s.grid = s.newGrid() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl) + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "1", + }).Return([]types.Trade{ + { + ID: 1, + OrderID: orderID, + Exchange: "binance", + Price: number(11000.0), + Quantity: gridQuantity, + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + FeeCurrency: "BTC", + Fee: fixedpoint.Zero, + }, + }, nil) + + s.orderQueryService = mockService + + expectedSubmitOrder := types.SubmitOrder{ + Symbol: "BTCUSDT", + Type: types.OrderTypeLimit, + Price: number(12_000.0), + Quantity: gridQuantity, + Side: types.SideTypeSell, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + Tag: "grid", + } + + orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) + orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{ + {SubmitOrder: expectedSubmitOrder}, + }, nil) + + expectedSubmitOrder2 := types.SubmitOrder{ + Symbol: "BTCUSDT", + Type: types.OrderTypeLimit, + Price: number(11_000.0), + Quantity: number(0.1090909), + Side: types.SideTypeBuy, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + Tag: "grid", + } + + orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder2).Return([]types.Order{ + {SubmitOrder: expectedSubmitOrder2}, + }, nil) + s.orderExecutor = orderExecutor + + s.handleOrderFilled(types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: gridQuantity, + Price: number(11000.0), + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "binance", + OrderID: 1, + Status: types.OrderStatusFilled, + ExecutedQuantity: gridQuantity, + }) + + s.handleOrderFilled(types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: gridQuantity, + Price: number(12000.0), + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "binance", + OrderID: 2, + Status: types.OrderStatusFilled, + ExecutedQuantity: gridQuantity, + }) + + }) } func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { From b515c245058f8a54f06bcb78f1f593b59ca851ff Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Dec 2022 14:48:51 +0800 Subject: [PATCH 0244/1392] grid2: add earnBase test case --- pkg/strategy/grid2/strategy_test.go | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index ed87ab2aba..a2f73a5d7d 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -533,6 +533,95 @@ func TestStrategy_handleOrderFilled(t *testing.T) { }) }) + t.Run("with fee token and EarnBase", func(t *testing.T) { + gridQuantity := number(0.1) + orderID := uint64(1) + + s := newTestStrategy() + s.Quantity = gridQuantity + s.EarnBase = true + s.grid = s.newGrid() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl) + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "1", + }).Return([]types.Trade{ + { + ID: 1, + OrderID: orderID, + Exchange: "binance", + Price: number(11000.0), + Quantity: gridQuantity, + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + FeeCurrency: "BTC", + Fee: fixedpoint.Zero, + }, + }, nil) + + s.orderQueryService = mockService + + orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) + + expectedSubmitOrder := types.SubmitOrder{ + Symbol: "BTCUSDT", + Type: types.OrderTypeLimit, + Side: types.SideTypeSell, + Price: number(12_000.0), + Quantity: number(0.09166666), + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + Tag: "grid", + } + orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{ + {SubmitOrder: expectedSubmitOrder}, + }, nil) + + expectedSubmitOrder2 := types.SubmitOrder{ + Symbol: "BTCUSDT", + Type: types.OrderTypeLimit, + Side: types.SideTypeBuy, + Price: number(11_000.0), + Quantity: number(0.09999999), + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + Tag: "grid", + } + orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder2).Return([]types.Order{ + {SubmitOrder: expectedSubmitOrder2}, + }, nil) + + s.orderExecutor = orderExecutor + + s.handleOrderFilled(types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: gridQuantity, + Price: number(11000.0), + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "binance", + OrderID: 1, + Status: types.OrderStatusFilled, + ExecutedQuantity: gridQuantity, + }) + + s.handleOrderFilled(types.Order{ + SubmitOrder: expectedSubmitOrder, + Exchange: "binance", + OrderID: 2, + Status: types.OrderStatusFilled, + ExecutedQuantity: expectedSubmitOrder.Quantity, + }) + }) + t.Run("with fee token and compound", func(t *testing.T) { gridQuantity := number(0.1) orderID := uint64(1) From 85097840f1227f9d109d4b5466bbb46efff2d2e9 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Dec 2022 16:44:27 +0800 Subject: [PATCH 0245/1392] binance: replace /api/v3/myTrades api --- go.mod | 2 +- go.sum | 2 + .../binanceapi/get_my_trades_request.go | 27 +++ .../get_my_trades_request_requestgen.go | 218 ++++++++++++++++++ pkg/exchange/binance/exchange.go | 34 +-- 5 files changed, 265 insertions(+), 18 deletions(-) create mode 100644 pkg/exchange/binance/binanceapi/get_my_trades_request.go create mode 100644 pkg/exchange/binance/binanceapi/get_my_trades_request_requestgen.go diff --git a/go.mod b/go.mod index 75acebd4af..aa9647fba6 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ go 1.17 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Masterminds/squirrel v1.5.3 - github.com/adshao/go-binance/v2 v2.3.8 + github.com/adshao/go-binance/v2 v2.3.10 github.com/c-bata/goptuna v0.8.1 github.com/c9s/requestgen v1.3.0 github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b diff --git a/go.sum b/go.sum index 0c04a2ca09..065aec2411 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/adshao/go-binance/v2 v2.3.5 h1:WVYZecm0w8l14YoWlnKZj6xxZT2AKMTHpMQSqI github.com/adshao/go-binance/v2 v2.3.5/go.mod h1:8Pg/FGTLyAhq8QXA0IkoReKyRpoxJcK3LVujKDAZV/c= github.com/adshao/go-binance/v2 v2.3.8 h1:9VsAX4jUopnIOlzrvnKUFUf9SWB/nwPgJtUsM2dkj6A= github.com/adshao/go-binance/v2 v2.3.8/go.mod h1:Z3MCnWI0gHC4Rea8TWiF3aN1t4nV9z3CaU/TeHcKsLM= +github.com/adshao/go-binance/v2 v2.3.10 h1:iWtHD/sQ8GK6r+cSMMdOynpGI/4Q6P5LZtiEHdWOjag= +github.com/adshao/go-binance/v2 v2.3.10/go.mod h1:Z3MCnWI0gHC4Rea8TWiF3aN1t4nV9z3CaU/TeHcKsLM= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= diff --git a/pkg/exchange/binance/binanceapi/get_my_trades_request.go b/pkg/exchange/binance/binanceapi/get_my_trades_request.go new file mode 100644 index 0000000000..8986ca7935 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_my_trades_request.go @@ -0,0 +1,27 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/adshao/go-binance/v2" +) + +type Trade = binance.TradeV3 + +//go:generate requestgen -method GET -url "/api/v3/myTrades" -type GetMyTradesRequest -responseType []Trade +type GetMyTradesRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` + orderID *uint64 `param:"orderId"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + fromID *uint64 `param:"fromId"` + limit *uint64 `param:"limit"` +} + +func (c *RestClient) NewGetMyTradesRequest() *GetMyTradesRequest { + return &GetMyTradesRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_my_trades_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_my_trades_request_requestgen.go new file mode 100644 index 0000000000..316b640901 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_my_trades_request_requestgen.go @@ -0,0 +1,218 @@ +// Code generated by "requestgen -method GET -url /api/v3/myTrades -type GetMyTradesRequest -responseType []Trade"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/adshao/go-binance/v2" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMyTradesRequest) Symbol(symbol string) *GetMyTradesRequest { + g.symbol = symbol + return g +} + +func (g *GetMyTradesRequest) OrderID(orderID uint64) *GetMyTradesRequest { + g.orderID = &orderID + return g +} + +func (g *GetMyTradesRequest) StartTime(startTime time.Time) *GetMyTradesRequest { + g.startTime = &startTime + return g +} + +func (g *GetMyTradesRequest) EndTime(endTime time.Time) *GetMyTradesRequest { + g.endTime = &endTime + return g +} + +func (g *GetMyTradesRequest) FromID(fromID uint64) *GetMyTradesRequest { + g.fromID = &fromID + return g +} + +func (g *GetMyTradesRequest) Limit(limit uint64) *GetMyTradesRequest { + g.limit = &limit + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMyTradesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMyTradesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check orderID field -> json key orderId + if g.orderID != nil { + orderID := *g.orderID + + // assign parameter of orderID + params["orderId"] = orderID + } else { + } + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check fromID field -> json key fromId + if g.fromID != nil { + fromID := *g.fromID + + // assign parameter of fromID + params["fromId"] = fromID + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMyTradesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMyTradesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMyTradesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMyTradesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMyTradesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMyTradesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMyTradesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMyTradesRequest) Do(ctx context.Context) ([]binance.TradeV3, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/myTrades" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []binance.TradeV3 + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 03a7a0a8e8..c3e530e7a1 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -1499,40 +1499,40 @@ func (e *Exchange) queryFuturesTrades(ctx context.Context, symbol string, option } func (e *Exchange) querySpotTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { - var remoteTrades []*binance.TradeV3 - req := e.client.NewListTradesService(). - Symbol(symbol) - - if options.Limit > 0 { - req.Limit(int(options.Limit)) - } else { - req.Limit(1000) - } + req := e.client2.NewGetMyTradesRequest() + req.Symbol(symbol) // BINANCE uses inclusive last trade ID if options.LastTradeID > 0 { - req.FromID(int64(options.LastTradeID)) + req.FromID(options.LastTradeID) } if options.StartTime != nil && options.EndTime != nil { if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { - req.StartTime(options.StartTime.UnixMilli()) - req.EndTime(options.EndTime.UnixMilli()) + req.StartTime(*options.StartTime) + req.EndTime(*options.EndTime) } else { - req.StartTime(options.StartTime.UnixMilli()) + req.StartTime(*options.StartTime) } } else if options.StartTime != nil { - req.StartTime(options.StartTime.UnixMilli()) + req.StartTime(*options.StartTime) } else if options.EndTime != nil { - req.EndTime(options.EndTime.UnixMilli()) + req.EndTime(*options.EndTime) } - remoteTrades, err = req.Do(ctx) + if options.Limit > 0 { + req.Limit(uint64(options.Limit)) + } else { + req.Limit(1000) + } + + remoteTrades, err := req.Do(ctx) if err != nil { return nil, err } + for _, t := range remoteTrades { - localTrade, err := toGlobalTrade(*t, e.IsMargin) + localTrade, err := toGlobalTrade(t, e.IsMargin) if err != nil { log.WithError(err).Errorf("can not convert binance trade: %+v", t) continue From ae678d1b3b4e22e04af9a780bd2ec1a9687bd93f Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Dec 2022 17:09:03 +0800 Subject: [PATCH 0246/1392] binance: add workaround for the myTrades api --- pkg/exchange/binance/exchange.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index c3e530e7a1..918b85c2f0 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -1413,22 +1413,23 @@ func (e *Exchange) queryMarginTrades(ctx context.Context, symbol string, options req.Limit(1000) } + // BINANCE seems to have an API bug, we can't use both fromId and the start time/end time // BINANCE uses inclusive last trade ID if options.LastTradeID > 0 { req.FromID(int64(options.LastTradeID)) - } - - if options.StartTime != nil && options.EndTime != nil { - if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { + } else { + if options.StartTime != nil && options.EndTime != nil { + if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { + req.StartTime(options.StartTime.UnixMilli()) + req.EndTime(options.EndTime.UnixMilli()) + } else { + req.StartTime(options.StartTime.UnixMilli()) + } + } else if options.StartTime != nil { req.StartTime(options.StartTime.UnixMilli()) + } else if options.EndTime != nil { req.EndTime(options.EndTime.UnixMilli()) - } else { - req.StartTime(options.StartTime.UnixMilli()) } - } else if options.StartTime != nil { - req.StartTime(options.StartTime.UnixMilli()) - } else if options.EndTime != nil { - req.EndTime(options.EndTime.UnixMilli()) } remoteTrades, err = req.Do(ctx) From 6c0cc71c1c9d85d40dea4ca338eb7a16f3dccba9 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Dec 2022 17:28:06 +0800 Subject: [PATCH 0247/1392] binance: avoid using fromId and timeRange at the same time --- pkg/exchange/binance/exchange.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 918b85c2f0..cba2ed2749 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -1506,19 +1506,19 @@ func (e *Exchange) querySpotTrades(ctx context.Context, symbol string, options * // BINANCE uses inclusive last trade ID if options.LastTradeID > 0 { req.FromID(options.LastTradeID) - } - - if options.StartTime != nil && options.EndTime != nil { - if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { + } else { + if options.StartTime != nil && options.EndTime != nil { + if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { + req.StartTime(*options.StartTime) + req.EndTime(*options.EndTime) + } else { + req.StartTime(*options.StartTime) + } + } else if options.StartTime != nil { req.StartTime(*options.StartTime) + } else if options.EndTime != nil { req.EndTime(*options.EndTime) - } else { - req.StartTime(*options.StartTime) } - } else if options.StartTime != nil { - req.StartTime(*options.StartTime) - } else if options.EndTime != nil { - req.EndTime(*options.EndTime) } if options.Limit > 0 { From 096defc33139376735b31d1abca09c780365d6d7 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Dec 2022 17:34:24 +0800 Subject: [PATCH 0248/1392] add test flag and disable lfs in test --- .github/workflows/go.yml | 2 +- pkg/strategy/grid2/strategy_test.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c845bb3de1..313e926c5f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v2 with: - lfs: 'true' + # lfs: 'true' ssh-key: ${{ secrets.git_ssh_key }} - uses: actions/cache@v2 diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index a2f73a5d7d..9b1a545a93 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -15,6 +15,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types/mocks" + "github.com/c9s/bbgo/pkg/util" gridmocks "github.com/c9s/bbgo/pkg/strategy/grid2/mocks" ) @@ -816,6 +817,11 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { } func TestBacktestStrategy(t *testing.T) { + if v, ok := util.GetEnvVarBool("TEST_BACKTEST"); !ok || !v { + t.Skip("backtest flag is required") + return + } + market := types.Market{ BaseCurrency: "BTC", QuoteCurrency: "USDT", From df6a34f5af82b10a60ac626973a103df13f59348 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Dec 2022 21:30:33 +0800 Subject: [PATCH 0249/1392] binanceapi: adjust http timeout to 10s --- pkg/exchange/binance/binanceapi/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/binance/binanceapi/client.go b/pkg/exchange/binance/binanceapi/client.go index a587cf7480..0157131294 100644 --- a/pkg/exchange/binance/binanceapi/client.go +++ b/pkg/exchange/binance/binanceapi/client.go @@ -21,7 +21,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -const defaultHTTPTimeout = time.Second * 2 +const defaultHTTPTimeout = time.Second * 10 const RestBaseURL = "https://api.binance.com" const SandboxRestBaseURL = "https://testnet.binance.vision" const DebugRequestResponse = false @@ -37,7 +37,7 @@ var defaultTransport = &http.Transport{ MaxIdleConns: 100, MaxConnsPerHost: 100, MaxIdleConnsPerHost: 100, - //TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper), + // TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper), ExpectContinueTimeout: 0, ForceAttemptHTTP2: true, TLSClientConfig: &tls.Config{}, From 01314ea6e4cff0f657b76149b239e3528c8df744 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 12 Dec 2022 13:17:06 +0800 Subject: [PATCH 0250/1392] lfs untrack files --- .gitattributes | 0 data/bbgo_test.sql | 3 +++ data/bbgo_test.sqlite3 | 3 +++ 3 files changed, 6 insertions(+) create mode 100644 .gitattributes create mode 100644 data/bbgo_test.sql create mode 100644 data/bbgo_test.sqlite3 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..e69de29bb2 diff --git a/data/bbgo_test.sql b/data/bbgo_test.sql new file mode 100644 index 0000000000..b8179666fe --- /dev/null +++ b/data/bbgo_test.sql @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e470f6615d327da7170df0ee0d846e04e0657f8aa4fece708c259725780a450 +size 24070617 diff --git a/data/bbgo_test.sqlite3 b/data/bbgo_test.sqlite3 new file mode 100644 index 0000000000..d2647caed6 --- /dev/null +++ b/data/bbgo_test.sqlite3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b771c3d1caace778141254c37d7e90a9bfaf757ffbfefb5e7c86694503c67d9 +size 31141888 From c8098b414b92b83a2bb8b61449667fbfd4674ec3 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 12 Dec 2022 17:18:40 +0800 Subject: [PATCH 0251/1392] cmd: add rollbar support --- go.mod | 6 ++++-- go.sum | 12 ++++++++++++ pkg/cmd/root.go | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index aa9647fba6..a5141178f6 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/robfig/cron/v3 v3.0.0 github.com/sajari/regression v1.0.1 - github.com/sirupsen/logrus v1.8.1 + github.com/sirupsen/logrus v1.9.0 github.com/slack-go/slack v0.10.1 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 @@ -90,6 +90,7 @@ require ( github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/heroku/rollrus v0.2.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -115,6 +116,7 @@ require ( github.com/prometheus/procfs v0.7.3 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/rjeczalik/notify v0.9.1 // indirect + github.com/rollbar/rollbar-go v1.4.5 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/shopspring/decimal v1.2.0 // indirect @@ -134,7 +136,7 @@ require ( golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect - golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect + golang.org/x/sys v0.3.0 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.11 // indirect diff --git a/go.sum b/go.sum index 065aec2411..37585ff52c 100644 --- a/go.sum +++ b/go.sum @@ -333,6 +333,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/heroku/rollrus v0.2.0 h1:b3AgcXJKFJNUwbQOC2S69/+mxuTpe4laznem9VJdPEo= +github.com/heroku/rollrus v0.2.0/go.mod h1:B3MwEcr9nmf4xj0Sr5l9eSht7wLKMa1C+9ajgAU79ek= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -543,6 +545,7 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.2-0.20190227000051-27936f6d90f9/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= @@ -593,6 +596,9 @@ github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/rollbar/rollbar-go v1.4.5 h1:Z+5yGaZdB7MFv7t759KUR3VEkGdwHjo7Avvf3ApHTVI= +github.com/rollbar/rollbar-go v1.4.5/go.mod h1:kLQ9gP3WCRGrvJmF0ueO3wK9xWocej8GRX98D8sa39w= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= @@ -619,6 +625,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slack-go/slack v0.10.1 h1:BGbxa0kMsGEvLOEoZmYs8T1wWfoZXwmQFBb6FgYCXUA= github.com/slack-go/slack v0.10.1/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -867,6 +875,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -909,6 +918,9 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 27e22194af..5dbf6cb28e 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/heroku/rollrus" "github.com/joho/godotenv" "github.com/lestrrat-go/file-rotatelogs" "github.com/pkg/errors" @@ -44,6 +45,18 @@ var RootCmd = &cobra.Command{ log.SetLevel(log.DebugLevel) } + env := os.Getenv("BBGO_ENV") + if env == "" { + env = "development" + } + + if viper.GetString("rollbar-token") != "" { + log.AddHook(rollrus.NewHook( + viper.GetString("rollbar-token"), + env, + )) + } + if viper.GetBool("metrics") { http.Handle("/metrics", promhttp.Handler()) go func() { @@ -149,6 +162,8 @@ func init() { RootCmd.PersistentFlags().String("config", "bbgo.yaml", "config file") + RootCmd.PersistentFlags().String("rollbar-token", "", "rollbar token") + // A flag can be 'persistent' meaning that this flag will be available to // the command it's assigned to as well as every command under that command. // For global flags, assign a flag as a persistent flag on the root. From d83feec9ecedcb5a3ee3879aedb67b3de0cd04b3 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 12 Dec 2022 17:37:40 +0800 Subject: [PATCH 0252/1392] cmd: add log message for rollbar token --- pkg/cmd/root.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 5dbf6cb28e..b156842627 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -20,6 +20,7 @@ import ( "github.com/x-cray/logrus-prefixed-formatter" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/util" _ "github.com/go-sql-driver/mysql" ) @@ -50,9 +51,11 @@ var RootCmd = &cobra.Command{ env = "development" } - if viper.GetString("rollbar-token") != "" { + if token := viper.GetString("rollbar-token"); token != "" { + log.Infof("found rollbar token %q, setting up rollbar hook...", util.MaskKey(token)) + log.AddHook(rollrus.NewHook( - viper.GetString("rollbar-token"), + token, env, )) } From a6956e50b78a6098c52d987056391aeb39215f3c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 12 Dec 2022 18:23:49 +0800 Subject: [PATCH 0253/1392] strategy/linregmaker: add more logs --- config/linregmaker.yaml | 10 +++--- pkg/strategy/linregmaker/strategy.go | 54 +++++++++++++++++----------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 84bbc4950d..994e5b80e8 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -41,7 +41,7 @@ exchangeStrategies: # reverseEMA reverseEMA: interval: 1d - window: 60 + window: 5 # reverseInterval reverseInterval: 4h @@ -126,10 +126,10 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [ -0.02, 0.02 ] + domain: [ -0.000002, 0.000002 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum - range: [ 0, 0.1 ] + range: [ 0, 0.001 ] dynamicQuantityDecrease: - linRegDynamicQuantity: quantityLinReg: @@ -140,10 +140,10 @@ exchangeStrategies: # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: # from lower band -100% (-1) to upper band 100% (+1) - domain: [0.02, -0.02 ] + domain: [0.000002, -0.000002 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum - range: [ 0, 0.1 ] + range: [ 0, 0.001 ] exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index a3dc3fe4c6..7421eb1964 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -25,6 +25,7 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) // TODO: Check logic of dynamic qty +// TODO: Add tg logs func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -248,8 +249,10 @@ func (s *Strategy) isAllowOppositePosition() bool { if (s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0) || (s.mainTrendCurrent == types.DirectionDown && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0) { + log.Infof("%s allow opposite position is enabled: MainTrend %v, FastLinReg: %f, SlowLinReg: %f", s.Symbol, s.mainTrendCurrent, s.FastLinReg.Last(), s.SlowLinReg.Last()) return true } + log.Infof("%s allow opposite position is disabled: MainTrend %v, FastLinReg: %f, SlowLinReg: %f", s.Symbol, s.mainTrendCurrent, s.FastLinReg.Last(), s.SlowLinReg.Last()) return false } @@ -298,7 +301,7 @@ func (s *Strategy) updateMaxExposure(midPrice fixedpoint.Value) { func (s *Strategy) getOrderPrices(midPrice fixedpoint.Value) (askPrice fixedpoint.Value, bidPrice fixedpoint.Value) { askPrice = midPrice.Mul(fixedpoint.One.Add(s.AskSpread)) bidPrice = midPrice.Mul(fixedpoint.One.Sub(s.BidSpread)) - log.Infof("mid price:%v ask:%v bid: %v", midPrice, askPrice, bidPrice) + log.Infof("%s mid price:%v ask:%v bid: %v", s.Symbol, midPrice, askPrice, bidPrice) return askPrice, bidPrice } @@ -352,14 +355,16 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } } + log.Infof("%s caculated buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) // Faster position decrease if s.isAllowOppositePosition() { if s.mainTrendCurrent == types.DirectionUp { - sellQuantity = sellQuantity * s.FasterDecreaseRatio + sellQuantity = sellQuantity.Mul(s.FasterDecreaseRatio) } else if s.mainTrendCurrent == types.DirectionDown { - buyQuantity = buyQuantity * s.FasterDecreaseRatio + buyQuantity = buyQuantity.Mul(s.FasterDecreaseRatio) } + log.Infof("%s faster position decrease: buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) } // Reduce order qty to fit current position @@ -371,10 +376,14 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } - buyQuantity = s.adjustQuantity(buyQuantity, bidPrice) - sellQuantity = s.adjustQuantity(sellQuantity, askPrice) + if buyQuantity.Compare(fixedpoint.Zero) > 0 { + buyQuantity = s.adjustQuantity(buyQuantity, bidPrice) + } + if sellQuantity.Compare(fixedpoint.Zero) > 0 { + sellQuantity = s.adjustQuantity(sellQuantity, askPrice) + } - log.Infof("sell qty:%v buy qty: %v", sellQuantity, buyQuantity) + log.Infof("adjusted sell qty:%v buy qty: %v", sellQuantity, buyQuantity) return sellQuantity, buyQuantity } @@ -467,6 +476,7 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f canSell = false } + log.Infof("canBuy %t, canSell %t", canBuy, canSell) return canBuy, canSell } @@ -643,19 +653,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } else if closePrice.Compare(priceReverseEMA) < 0 { s.mainTrendCurrent = types.DirectionDown } - })) - - // Main interval - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - // StrategyController - if s.Status != types.StrategyStatusRunning { - return - } - - _ = s.orderExecutor.GracefulCancel(ctx) - - // closePrice is the close price of current kline - closePrice := kline.GetClose() + log.Infof("%s current trend is %v", s.Symbol, s.mainTrendCurrent) // Trend reversal if s.mainTrendCurrent != s.mainTrendPrevious { @@ -663,13 +661,28 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if !s.Position.IsDust(closePrice) && ((s.Position.IsLong() && s.mainTrendCurrent == types.DirectionDown) || (s.Position.IsShort() && s.mainTrendCurrent == types.DirectionUp)) { - log.Infof("trend reverse to %v. closing on-hand position", s.mainTrendCurrent) + log.Infof("%s trend reverse to %v. closing on-hand position", s.Symbol, s.mainTrendCurrent) + bbgo.Notify("%s trend reverse to %v. closing on-hand position", s.Symbol, s.mainTrendCurrent) if err := s.ClosePosition(ctx, fixedpoint.One); err != nil { log.WithError(err).Errorf("cannot close on-hand position of %s", s.Symbol) + bbgo.Notify("cannot close on-hand position of %s", s.Symbol) // TODO: close position failed. retry? } } } + })) + + // Main interval + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + _ = s.orderExecutor.GracefulCancel(ctx) + + // closePrice is the close price of current kline + closePrice := kline.GetClose() // midPrice for ask and bid prices var midPrice fixedpoint.Value @@ -719,6 +732,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if len(submitOrders) == 0 { return } + log.Infof("submitting order(s): %v", submitOrders) _, _ = s.orderExecutor.SubmitOrders(ctx, submitOrders...) })) From 79dcda5f5282e3014276487f5492bbb4b9c4b5cf Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 13 Dec 2022 11:06:18 +0800 Subject: [PATCH 0254/1392] strategy/linregmaker: add more trend reverse logs --- config/linregmaker.yaml | 6 +++--- pkg/strategy/linregmaker/strategy.go | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 994e5b80e8..a88da8b504 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -18,8 +18,8 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2022-05-01" - endTime: "2022-10-31" + startTime: "2022-03-15" + endTime: "2022-03-23" symbols: - BTCUSDT accounts: @@ -41,7 +41,7 @@ exchangeStrategies: # reverseEMA reverseEMA: interval: 1d - window: 5 + window: 60 # reverseInterval reverseInterval: 4h diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 7421eb1964..be5c3e20e3 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -657,12 +657,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Trend reversal if s.mainTrendCurrent != s.mainTrendPrevious { + log.Infof("%s trend reverse to %v", s.Symbol, s.mainTrendCurrent) // Close on-hand position that is not in the same direction as the new trend if !s.Position.IsDust(closePrice) && ((s.Position.IsLong() && s.mainTrendCurrent == types.DirectionDown) || (s.Position.IsShort() && s.mainTrendCurrent == types.DirectionUp)) { - log.Infof("%s trend reverse to %v. closing on-hand position", s.Symbol, s.mainTrendCurrent) - bbgo.Notify("%s trend reverse to %v. closing on-hand position", s.Symbol, s.mainTrendCurrent) + log.Infof("%s closing on-hand position due to trend reverse", s.Symbol) + bbgo.Notify("%s closing on-hand position due to trend reverse", s.Symbol) if err := s.ClosePosition(ctx, fixedpoint.One); err != nil { log.WithError(err).Errorf("cannot close on-hand position of %s", s.Symbol) bbgo.Notify("cannot close on-hand position of %s", s.Symbol) From 30f3ef2180c37fc80bf0bbd06a5c80e24198fb07 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 13 Dec 2022 12:12:46 +0800 Subject: [PATCH 0255/1392] strategy/linregmaker: add more tg notification --- pkg/strategy/linregmaker/strategy.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index be5c3e20e3..afd58cec2f 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -24,9 +24,6 @@ var two = fixedpoint.NewFromInt(2) var log = logrus.WithField("strategy", ID) -// TODO: Check logic of dynamic qty -// TODO: Add tg logs - func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } @@ -290,6 +287,7 @@ func (s *Strategy) updateMaxExposure(midPrice fixedpoint.Value) { maxExposurePosition, err := s.DynamicExposure.GetMaxExposure(midPrice.Float64(), s.mainTrendCurrent) if err != nil { log.WithError(err).Errorf("can not calculate DynamicExposure of %s, use previous MaxExposurePosition instead", s.Symbol) + bbgo.Notify("can not calculate DynamicExposure of %s, use previous MaxExposurePosition instead", s.Symbol) } else { s.MaxExposurePosition = maxExposurePosition } @@ -333,12 +331,18 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp qty, err := s.DynamicQuantityIncrease.GetQuantity() if err == nil { buyQuantity = qty + } else { + log.WithError(err).Errorf("cannot get dynamic buy qty of %s, use default qty instead", s.Symbol) + bbgo.Notify("cannot get dynamic buy qty of %s, use default qty instead", s.Symbol) } } if len(s.DynamicQuantityDecrease) > 0 { qty, err := s.DynamicQuantityDecrease.GetQuantity() if err == nil { sellQuantity = qty + } else { + log.WithError(err).Errorf("cannot get dynamic sell qty of %s, use default qty instead", s.Symbol) + bbgo.Notify("cannot get dynamic sell qty of %s, use default qty instead", s.Symbol) } } case s.mainTrendCurrent == types.DirectionDown: @@ -346,12 +350,18 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp qty, err := s.DynamicQuantityIncrease.GetQuantity() if err == nil { sellQuantity = qty + } else { + log.WithError(err).Errorf("cannot get dynamic sell qty of %s, use default qty instead", s.Symbol) + bbgo.Notify("cannot get dynamic sell qty of %s, use default qty instead", s.Symbol) } } if len(s.DynamicQuantityDecrease) > 0 { qty, err := s.DynamicQuantityDecrease.GetQuantity() if err == nil { buyQuantity = qty + } else { + log.WithError(err).Errorf("cannot get dynamic buy qty of %s, use default qty instead", s.Symbol) + bbgo.Notify("cannot get dynamic buy qty of %s, use default qty instead", s.Symbol) } } } @@ -658,6 +668,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Trend reversal if s.mainTrendCurrent != s.mainTrendPrevious { log.Infof("%s trend reverse to %v", s.Symbol, s.mainTrendCurrent) + bbgo.Notify("%s trend reverse to %v", s.Symbol, s.mainTrendCurrent) // Close on-hand position that is not in the same direction as the new trend if !s.Position.IsDust(closePrice) && ((s.Position.IsLong() && s.mainTrendCurrent == types.DirectionDown) || @@ -667,7 +678,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if err := s.ClosePosition(ctx, fixedpoint.One); err != nil { log.WithError(err).Errorf("cannot close on-hand position of %s", s.Symbol) bbgo.Notify("cannot close on-hand position of %s", s.Symbol) - // TODO: close position failed. retry? } } } From ff334ca13df510cc1622d0a6d6494e393eaf9aaf Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 13 Dec 2022 17:16:30 +0800 Subject: [PATCH 0256/1392] strategy/linregmaker: calculated allowed margin when leveraged --- config/linregmaker.yaml | 6 +++-- pkg/strategy/linregmaker/strategy.go | 39 ++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index a88da8b504..80c2ddf552 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -18,8 +18,8 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2022-03-15" - endTime: "2022-03-23" + startTime: "2022-05-01" + endTime: "2022-10-31" symbols: - BTCUSDT accounts: @@ -38,6 +38,8 @@ exchangeStrategies: # interval is how long do you want to update your order price and quantity interval: 1m + leverage: 1 + # reverseEMA reverseEMA: interval: 1d diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index afd58cec2f..89f300f9b7 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -32,10 +32,14 @@ type Strategy struct { Environment *bbgo.Environment StandardIndicatorSet *bbgo.StandardIndicatorSet Market types.Market + ctx context.Context // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` + // Leverage uses the account net value to calculate the allowed margin + Leverage fixedpoint.Value `json:"leverage"` + types.IntervalWindow // ReverseEMA is used to determine the long-term trend. @@ -408,20 +412,23 @@ func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { balances := s.session.GetAccount().Balances() baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] + lastPrice, _ := s.session.LastPrice(s.Symbol) if bbgo.IsBackTesting { if !hasQuoteBalance { baseQty = fixedpoint.Zero quoteQty = fixedpoint.Zero } else { - lastPrice, _ := s.session.LastPrice(s.Symbol) baseQty = quoteBalance.Available.Div(lastPrice) quoteQty = quoteBalance.Available } - } else if s.session.Margin || s.session.IsolatedMargin { - - } else if s.session.Futures || s.session.IsolatedFutures { - + } else if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { + quoteQ, err := bbgo.CalculateQuoteQuantity(s.ctx, s.session, s.Market.QuoteCurrency, s.Leverage) + if err != nil { + quoteQ = fixedpoint.Zero + } + quoteQty = quoteQ + baseQty = quoteQ.Div(lastPrice) } else { if !hasBaseBalance { baseQty = fixedpoint.Zero @@ -479,11 +486,21 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f // Check against account balance baseQty, quoteQty := s.getAllowedBalance() - if buyQuantity.Compare(quoteQty.Div(bidPrice)) > 0 { - canBuy = false - } - if sellQuantity.Compare(baseQty) > 0 { - canSell = false + if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { // Leveraged + if quoteQty.Compare(fixedpoint.Zero) <= 0 { + if s.Position.IsLong() { + canBuy = false + } else if s.Position.IsShort() { + canSell = false + } + } + } else { + if buyQuantity.Compare(quoteQty.Div(bidPrice)) > 0 { // Spot + canBuy = false + } + if sellQuantity.Compare(baseQty) > 0 { + canSell = false + } } log.Infof("canBuy %t, canSell %t", canBuy, canSell) @@ -491,7 +508,6 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f } // getOrderForms returns buy and sell order form for submission -// TODO: Simplify func (s *Strategy) getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice fixedpoint.Value) (buyOrder types.SubmitOrder, sellOrder types.SubmitOrder) { sellOrder = types.SubmitOrder{ Symbol: s.Symbol, @@ -563,6 +579,7 @@ func (s *Strategy) getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice f func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { // initial required information s.session = session + s.ctx = ctx // Calculate group id for orders instanceID := s.InstanceID() From c6f9b0feed8792cbc5a8e662f5b6494fa49564dd Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 13 Dec 2022 17:37:47 +0800 Subject: [PATCH 0257/1392] strategy/linregmaker: update config --- config/linregmaker.yaml | 62 +++++++++++++++++----------- pkg/strategy/linregmaker/strategy.go | 3 +- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 80c2ddf552..1f320f1816 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -38,33 +38,45 @@ exchangeStrategies: # interval is how long do you want to update your order price and quantity interval: 1m + # leverage uses the account net value to calculate the allowed margin leverage: 1 - # reverseEMA + # reverseEMA is used to determine the long-term trend. + # Above the ReverseEMA is the long trend and vise versa. + # All the opposite trend position will be closed upon the trend change reverseEMA: interval: 1d window: 60 - # reverseInterval + # reverseInterval is the interval to check trend reverse against ReverseEMA. Close price of this interval crossing + # the ReverseEMA triggers main trend change. reverseInterval: 4h - # fastLinReg + # fastLinReg is to determine the short-term trend. + # Buy/sell orders are placed if the FastLinReg and the ReverseEMA trend are in the same direction, and only orders + # that reduce position are placed if the FastLinReg and the ReverseEMA trend are in different directions. fastLinReg: interval: 1m window: 30 - # slowLinReg + # slowLinReg is to determine the midterm trend. + # When the SlowLinReg and the ReverseEMA trend are in different directions, creation of opposite position is + # allowed. slowLinReg: interval: 1m window: 120 - # allowOppositePosition + # allowOppositePosition if true, the creation of opposite position is allowed when both fast and slow LinReg are in + # the opposite direction to main trend allowOppositePosition: true - # fasterDecreaseRatio + # fasterDecreaseRatio the quantity of decreasing position orders are multiplied by this ratio when both fast and + # slow LinReg are in the opposite direction to main trend fasterDecreaseRatio: 2 - # neutralBollinger + # neutralBollinger is the smaller range of the bollinger band + # If price is in this band, it usually means the price is oscillating. + # If price goes out of this band, we tend to not place sell orders or buy orders neutralBollinger: interval: "15m" window: 21 @@ -73,9 +85,13 @@ exchangeStrategies: # tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band. tradeInBand: true - # spread + # spread is the price spread from the middle price. + # For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) + # For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) + # Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 spread: 0.1% - # dynamicSpread + # dynamicSpread enables the automatic adjustment to bid and ask spread. + # Overrides Spread, BidSpread, and AskSpread dynamicSpread: amplitude: # delete other scaling strategy if this is defined # window is the window of the SMAs of spreads @@ -87,8 +103,7 @@ exchangeStrategies: exp: # from down to up domain: [ 0.0001, 0.005 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum + # the spread range range: [ 0.001, 0.002 ] bidSpreadScale: byPercentage: @@ -96,12 +111,15 @@ exchangeStrategies: exp: # from down to up domain: [ 0.0001, 0.005 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum + # the spread range range: [ 0.001, 0.002 ] + # maxExposurePosition is the maximum position you can hold + # 10 means you can hold 10 ETH long/short position by maximum #maxExposurePosition: 10 - DynamicExposure: + # dynamicExposure is used to define the exposure position range with the given percentage. + # When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically + dynamicExposure: bollBandExposure: interval: "1h" window: 21 @@ -112,12 +130,13 @@ exchangeStrategies: exp: # from lower band -100% (-1) to upper band 100% (+1) domain: [ -1, 1 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum - range: [ 0.1, 10 ] + # when in down band, holds 0.1 by maximum + # when in up band, holds 1 by maximum + range: [ 0.1, 1 ] # quantity is the base order quantity for your buy/sell order. quantity: 0.1 + # dynamicQuantityIncrease calculates the increase position order quantity dynamically dynamicQuantityIncrease: - linRegDynamicQuantity: quantityLinReg: @@ -125,13 +144,10 @@ exchangeStrategies: window: 20 dynamicQuantityLinRegScale: byPercentage: - # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: - # from lower band -100% (-1) to upper band 100% (+1) domain: [ -0.000002, 0.000002 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum range: [ 0, 0.001 ] + # dynamicQuantityDecrease calculates the decrease position order quantity dynamically dynamicQuantityDecrease: - linRegDynamicQuantity: quantityLinReg: @@ -139,12 +155,8 @@ exchangeStrategies: window: 20 dynamicQuantityLinRegScale: byPercentage: - # log means we want to use log scale, you can replace "log" with "linear" for linear scale linear: - # from lower band -100% (-1) to upper band 100% (+1) domain: [0.000002, -0.000002 ] - # when in down band, holds 1.0 by maximum - # when in up band, holds 0.05 by maximum range: [ 0, 0.001 ] exits: diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 89f300f9b7..57e28dbbbf 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -17,6 +17,8 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +// TODO: Docs + const ID = "linregmaker" var notionModifier = fixedpoint.NewFromFloat(1.1) @@ -403,7 +405,6 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } // getAllowedBalance returns the allowed qty of orders -// TODO LATER: Check max qty of margin and futures func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { // Default baseQty = fixedpoint.PosInf From 2ecdae65306cdc59a8571127bf6a58d001a1eea2 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 13 Dec 2022 17:49:31 +0800 Subject: [PATCH 0258/1392] strategy/linregmaker: remove wrong test file --- pkg/strategy/linregmaker/strategy_test.go | 69 ----------------------- 1 file changed, 69 deletions(-) delete mode 100644 pkg/strategy/linregmaker/strategy_test.go diff --git a/pkg/strategy/linregmaker/strategy_test.go b/pkg/strategy/linregmaker/strategy_test.go deleted file mode 100644 index 317464ad0b..0000000000 --- a/pkg/strategy/linregmaker/strategy_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package linregmaker - -import ( - "testing" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -func Test_calculateBandPercentage(t *testing.T) { - type args struct { - up float64 - down float64 - sma float64 - midPrice float64 - } - tests := []struct { - name string - args args - want fixedpoint.Value - }{ - { - name: "positive boundary", - args: args{ - up: 2000.0, - sma: 1500.0, - down: 1000.0, - midPrice: 2000.0, - }, - want: fixedpoint.NewFromFloat(1.0), - }, - { - name: "inside positive boundary", - args: args{ - up: 2000.0, - sma: 1500.0, - down: 1000.0, - midPrice: 1600.0, - }, - want: fixedpoint.NewFromFloat(0.2), // 20% - }, - { - name: "negative boundary", - args: args{ - up: 2000.0, - sma: 1500.0, - down: 1000.0, - midPrice: 1000.0, - }, - want: fixedpoint.NewFromFloat(-1.0), - }, - { - name: "out of negative boundary", - args: args{ - up: 2000.0, - sma: 1500.0, - down: 1000.0, - midPrice: 800.0, - }, - want: fixedpoint.NewFromFloat(-1.4), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := calculateBandPercentage(tt.args.up, tt.args.down, tt.args.sma, tt.args.midPrice); fixedpoint.NewFromFloat(got) != tt.want { - t.Errorf("calculateBandPercentage() = %v, want %v", got, tt.want) - } - }) - } -} From 2b8a5fe7552b358ef5c20069f2e0484d11235b6c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 14 Dec 2022 11:52:15 +0800 Subject: [PATCH 0259/1392] strategy/linregmaker: fix faster decrease logic --- pkg/strategy/linregmaker/strategy.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 57e28dbbbf..d7342050e1 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -374,14 +374,12 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp log.Infof("%s caculated buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) // Faster position decrease - if s.isAllowOppositePosition() { - if s.mainTrendCurrent == types.DirectionUp { - sellQuantity = sellQuantity.Mul(s.FasterDecreaseRatio) - } else if s.mainTrendCurrent == types.DirectionDown { - buyQuantity = buyQuantity.Mul(s.FasterDecreaseRatio) - } - log.Infof("%s faster position decrease: buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) + if s.mainTrendCurrent == types.DirectionUp && s.SlowLinReg.Last() < 0 { + sellQuantity = sellQuantity.Mul(s.FasterDecreaseRatio) + } else if s.mainTrendCurrent == types.DirectionDown && s.SlowLinReg.Last() > 0 { + buyQuantity = buyQuantity.Mul(s.FasterDecreaseRatio) } + log.Infof("%s faster position decrease: buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) // Reduce order qty to fit current position if !s.isAllowOppositePosition() { From d510c37e911fe7fb50a5b8372ecb24179dac90d3 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 14 Dec 2022 12:28:39 +0800 Subject: [PATCH 0260/1392] improve/dynamic_quantity: fix dynamic qty logic --- pkg/dynamicmetric/dynamic_quantity.go | 18 ++++++++++++------ pkg/strategy/linregmaker/strategy.go | 8 ++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/dynamicmetric/dynamic_quantity.go b/pkg/dynamicmetric/dynamic_quantity.go index 70e44797d2..fa3182adac 100644 --- a/pkg/dynamicmetric/dynamic_quantity.go +++ b/pkg/dynamicmetric/dynamic_quantity.go @@ -19,10 +19,10 @@ func (d *DynamicQuantitySet) Initialize(symbol string, session *bbgo.ExchangeSes } // GetQuantity returns the quantity -func (d *DynamicQuantitySet) GetQuantity() (fixedpoint.Value, error) { +func (d *DynamicQuantitySet) GetQuantity(reverse bool) (fixedpoint.Value, error) { quantity := fixedpoint.Zero for i := range *d { - v, err := (*d)[i].getQuantity() + v, err := (*d)[i].getQuantity(reverse) if err != nil { return fixedpoint.Zero, err } @@ -50,10 +50,10 @@ func (d *DynamicQuantity) IsEnabled() bool { } // getQuantity returns quantity -func (d *DynamicQuantity) getQuantity() (fixedpoint.Value, error) { +func (d *DynamicQuantity) getQuantity(reverse bool) (fixedpoint.Value, error) { switch { case d.LinRegDynamicQuantity != nil: - return d.LinRegDynamicQuantity.getQuantity() + return d.LinRegDynamicQuantity.getQuantity(reverse) default: return fixedpoint.Zero, errors.New("dynamic quantity is not enabled") } @@ -84,8 +84,14 @@ func (d *DynamicQuantityLinReg) initialize(symbol string, session *bbgo.Exchange } // getQuantity returns quantity -func (d *DynamicQuantityLinReg) getQuantity() (fixedpoint.Value, error) { - v, err := d.DynamicQuantityLinRegScale.Scale(d.QuantityLinReg.LastRatio()) +func (d *DynamicQuantityLinReg) getQuantity(reverse bool) (fixedpoint.Value, error) { + var linregRatio float64 + if reverse { + linregRatio = -d.QuantityLinReg.LastRatio() + } else { + linregRatio = d.QuantityLinReg.LastRatio() + } + v, err := d.DynamicQuantityLinRegScale.Scale(linregRatio) if err != nil { return fixedpoint.Zero, err } diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index d7342050e1..478f6e168d 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -334,7 +334,7 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp switch { case s.mainTrendCurrent == types.DirectionUp: if len(s.DynamicQuantityIncrease) > 0 { - qty, err := s.DynamicQuantityIncrease.GetQuantity() + qty, err := s.DynamicQuantityIncrease.GetQuantity(false) if err == nil { buyQuantity = qty } else { @@ -343,7 +343,7 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } if len(s.DynamicQuantityDecrease) > 0 { - qty, err := s.DynamicQuantityDecrease.GetQuantity() + qty, err := s.DynamicQuantityDecrease.GetQuantity(false) if err == nil { sellQuantity = qty } else { @@ -353,7 +353,7 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } case s.mainTrendCurrent == types.DirectionDown: if len(s.DynamicQuantityIncrease) > 0 { - qty, err := s.DynamicQuantityIncrease.GetQuantity() + qty, err := s.DynamicQuantityIncrease.GetQuantity(true) if err == nil { sellQuantity = qty } else { @@ -362,7 +362,7 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } if len(s.DynamicQuantityDecrease) > 0 { - qty, err := s.DynamicQuantityDecrease.GetQuantity() + qty, err := s.DynamicQuantityDecrease.GetQuantity(true) if err == nil { buyQuantity = qty } else { From 8b1d19124f72cde0ca39e6707c4f11e7a0e875ce Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 14 Dec 2022 14:42:56 +0800 Subject: [PATCH 0261/1392] strategy/linregmaker: allow using amount for order qty calculation --- config/linregmaker.yaml | 14 +++++++++----- pkg/strategy/linregmaker/strategy.go | 21 +++++++++++++++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 1f320f1816..57b89ec8a7 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -135,7 +135,11 @@ exchangeStrategies: range: [ 0.1, 1 ] # quantity is the base order quantity for your buy/sell order. - quantity: 0.1 + quantity: 0.001 + # amount: fixed amount instead of qty + #amount: 10 + # useDynamicQuantityAsAmount calculates amount instead of quantity + useDynamicQuantityAsAmount: false # dynamicQuantityIncrease calculates the increase position order quantity dynamically dynamicQuantityIncrease: - linRegDynamicQuantity: @@ -145,8 +149,8 @@ exchangeStrategies: dynamicQuantityLinRegScale: byPercentage: linear: - domain: [ -0.000002, 0.000002 ] - range: [ 0, 0.001 ] + domain: [ -0.0001, 0.00005 ] + range: [ 0, 0.02 ] # dynamicQuantityDecrease calculates the decrease position order quantity dynamically dynamicQuantityDecrease: - linRegDynamicQuantity: @@ -156,8 +160,8 @@ exchangeStrategies: dynamicQuantityLinRegScale: byPercentage: linear: - domain: [0.000002, -0.000002 ] - range: [ 0, 0.001 ] + domain: [ -0.00005, 0.0001 ] + range: [ 0.02, 0 ] exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 478f6e168d..ea6d7e9a3f 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -120,6 +120,9 @@ type Strategy struct { // DynamicQuantityDecrease calculates the decrease position order quantity dynamically DynamicQuantityDecrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityDecrease"` + // UseDynamicQuantityAsAmount calculates amount instead of quantity + UseDynamicQuantityAsAmount bool `json:"useDynamicQuantityAsAmount"` + session *bbgo.ExchangeSession // ExitMethods are various TP/SL methods @@ -371,15 +374,25 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } } } - log.Infof("%s caculated buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) + if s.UseDynamicQuantityAsAmount { + log.Infof("caculated %s buy amount %v, sell amount %v", s.Symbol, buyQuantity, sellQuantity) + qtyAmount := bbgo.QuantityOrAmount{Amount: buyQuantity} + buyQuantity = qtyAmount.CalculateQuantity(bidPrice) + qtyAmount.Amount = sellQuantity + sellQuantity = qtyAmount.CalculateQuantity(askPrice) + log.Infof("convert %s amount to buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) + } else { + log.Infof("caculated %s buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) + } // Faster position decrease if s.mainTrendCurrent == types.DirectionUp && s.SlowLinReg.Last() < 0 { sellQuantity = sellQuantity.Mul(s.FasterDecreaseRatio) + log.Infof("faster %s position decrease: sell qty %v", s.Symbol, sellQuantity) } else if s.mainTrendCurrent == types.DirectionDown && s.SlowLinReg.Last() > 0 { buyQuantity = buyQuantity.Mul(s.FasterDecreaseRatio) + log.Infof("faster %s position decrease: buy qty %v", s.Symbol, buyQuantity) } - log.Infof("%s faster position decrease: buy qty %v, sell qty %v", s.Symbol, buyQuantity, sellQuantity) // Reduce order qty to fit current position if !s.isAllowOppositePosition() { @@ -749,10 +762,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Submit orders var submitOrders []types.SubmitOrder - if canSell { + if canSell && sellOrder.Quantity.Compare(fixedpoint.Zero) > 0 { submitOrders = append(submitOrders, sellOrder) } - if canBuy { + if canBuy && buyOrder.Quantity.Compare(fixedpoint.Zero) > 0 { submitOrders = append(submitOrders, buyOrder) } From 532d4745647d7bef0738fe1420d90e2d64f337db Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 14:57:25 +0800 Subject: [PATCH 0262/1392] grid2: pull out processFilledOrder method --- pkg/strategy/grid2/strategy.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 8cb34f488b..03f0684e4c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -197,7 +197,6 @@ func (s *Strategy) handleOrderCanceled(o types.Order) { } } -// TODO: consider order fee func (s *Strategy) calculateProfit(o types.Order, buyPrice, buyQuantity fixedpoint.Value) *GridProfit { if s.EarnBase { // sell quantity - buy quantity @@ -300,14 +299,7 @@ func (s *Strategy) aggregateOrderBaseFee(o types.Order) fixedpoint.Value { return fixedpoint.Zero } -// handleOrderFilled is called when an order status is FILLED -func (s *Strategy) handleOrderFilled(o types.Order) { - if s.grid == nil { - return - } - - s.logger.Infof("GRID ORDER FILLED: %s", o.String()) - +func (s *Strategy) processFilledOrder(o types.Order) { // check order fee newSide := types.SideTypeSell newPrice := o.Price @@ -386,6 +378,16 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } } +// handleOrderFilled is called when an order status is FILLED +func (s *Strategy) handleOrderFilled(o types.Order) { + if s.grid == nil { + return + } + + s.logger.Infof("GRID ORDER FILLED: %s", o.String()) + s.processFilledOrder(o) +} + func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { // check more investment budget details requiredBase = fixedpoint.Zero From aa4130ed304ba07dc4e34bb94f676d01dd1f1384 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 14:58:31 +0800 Subject: [PATCH 0263/1392] grid2: add PlainText method on GridProfit struct --- pkg/strategy/grid2/profit.go | 4 ++++ pkg/strategy/grid2/strategy.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/profit.go b/pkg/strategy/grid2/profit.go index 245a7a586a..17a00307d0 100644 --- a/pkg/strategy/grid2/profit.go +++ b/pkg/strategy/grid2/profit.go @@ -18,3 +18,7 @@ type GridProfit struct { func (p *GridProfit) String() string { return fmt.Sprintf("GRID PROFIT: %f %s @ %s orderID %d", p.Profit.Float64(), p.Currency, p.Time.String(), p.Order.OrderID) } + +func (p *GridProfit) PlainText() string { + return fmt.Sprintf("Grid profit: %f %s @ %s orderID %d", p.Profit.Float64(), p.Currency, p.Time.String(), p.Order.OrderID) +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 03f0684e4c..b9e9c46c4c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -315,7 +315,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { if o.Side == types.SideTypeBuy { baseSellQuantityReduction = s.aggregateOrderBaseFee(o) - s.logger.Infof("base fee: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) + s.logger.Infof("buy order base fee: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) newQuantity = newQuantity.Sub(baseSellQuantityReduction) } From 16df170ca3143d9cf0a885af3da4db55de0c801f Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Dec 2022 16:30:51 +0800 Subject: [PATCH 0264/1392] grid2: pull out order tag --- pkg/strategy/grid2/strategy.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b9e9c46c4c..30a9003259 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -17,6 +17,8 @@ import ( const ID = "grid2" +const orderTag = "grid2" + var log = logrus.WithField("strategy", ID) var maxNumberOfOrderTradesQueryTries = 10 @@ -366,7 +368,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { Side: newSide, TimeInForce: types.TimeInForceGTC, Quantity: newQuantity, - Tag: "grid", + Tag: orderTag, } s.logger.Infof("SUBMIT GRID REVERSE ORDER: %s", orderForm.String()) @@ -899,7 +901,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin Quantity: quantity, Market: s.Market, TimeInForce: types.TimeInForceGTC, - Tag: "grid", + Tag: orderTag, }) quoteQuantity := quantity.Mul(price) usedQuote = usedQuote.Add(quoteQuantity) From a6a4be987800a7a741f73befad9c8cffcd308bc0 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Dec 2022 16:32:28 +0800 Subject: [PATCH 0265/1392] grid2: sync order tag --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 30a9003259..0535523097 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -866,7 +866,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin Quantity: quantity, Market: s.Market, TimeInForce: types.TimeInForceGTC, - Tag: "grid2", + Tag: orderTag, }) usedBase = usedBase.Add(quantity) } else if i > 0 { @@ -881,7 +881,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin Quantity: quantity, Market: s.Market, TimeInForce: types.TimeInForceGTC, - Tag: "grid2", + Tag: orderTag, }) quoteQuantity := quantity.Mul(price) usedQuote = usedQuote.Add(quoteQuantity) From 7a35a652c35bcfe892cd4198f49ec54c09df7d35 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 15:33:26 +0800 Subject: [PATCH 0266/1392] grid2: add SlackAttachment on grid profit --- pkg/strategy/grid2/profit.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pkg/strategy/grid2/profit.go b/pkg/strategy/grid2/profit.go index 17a00307d0..0c86692969 100644 --- a/pkg/strategy/grid2/profit.go +++ b/pkg/strategy/grid2/profit.go @@ -2,9 +2,13 @@ package grid2 import ( "fmt" + "strconv" "time" + "github.com/slack-go/slack" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/style" "github.com/c9s/bbgo/pkg/types" ) @@ -22,3 +26,23 @@ func (p *GridProfit) String() string { func (p *GridProfit) PlainText() string { return fmt.Sprintf("Grid profit: %f %s @ %s orderID %d", p.Profit.Float64(), p.Currency, p.Time.String(), p.Order.OrderID) } + +func (p *GridProfit) SlackAttachment() slack.Attachment { + title := fmt.Sprintf("Grid Profit %s %s", style.PnLSignString(p.Profit), p.Currency) + return slack.Attachment{ + Title: title, + Color: "warning", + Fields: []slack.AttachmentField{ + { + Title: "OrderID", + Value: strconv.FormatUint(p.Order.OrderID, 10), + Short: true, + }, + { + Title: "Time", + Value: p.Time.String(), + Short: true, + }, + }, + } +} From a7c8da7e882f82947de7225b9ce34cecebe4f462 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 15:39:16 +0800 Subject: [PATCH 0267/1392] grid2: add SlackAttachment on profit stats --- pkg/strategy/grid2/profit_stats.go | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/pkg/strategy/grid2/profit_stats.go b/pkg/strategy/grid2/profit_stats.go index e1eed06ad8..9a0d1aaa97 100644 --- a/pkg/strategy/grid2/profit_stats.go +++ b/pkg/strategy/grid2/profit_stats.go @@ -1,7 +1,13 @@ package grid2 import ( + "fmt" + "strconv" + + "github.com/slack-go/slack" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/style" "github.com/c9s/bbgo/pkg/types" ) @@ -58,3 +64,62 @@ func (s *GridProfitStats) AddProfit(profit *GridProfit) { s.ProfitEntries = append(s.ProfitEntries, profit) } + +func (s *GridProfitStats) SlackAttachment() slack.Attachment { + var fields = []slack.AttachmentField{ + { + Title: "Arbitrage Count", + Value: strconv.Itoa(s.ArbitrageCount), + Short: true, + }, + } + + if !s.FloatProfit.IsZero() { + fields = append(fields, slack.AttachmentField{ + Title: "Float Profit", + Value: style.PnLSignString(s.FloatProfit), + Short: true, + }) + } + + if !s.GridProfit.IsZero() { + fields = append(fields, slack.AttachmentField{ + Title: "Total Grid Profit", + Value: style.PnLSignString(s.GridProfit), + Short: true, + }) + } + + if !s.TotalQuoteProfit.IsZero() { + fields = append(fields, slack.AttachmentField{ + Title: "Total Quote Profit", + Value: style.PnLSignString(s.TotalQuoteProfit), + Short: true, + }) + } + + if !s.TotalBaseProfit.IsZero() { + fields = append(fields, slack.AttachmentField{ + Title: "Total Base Profit", + Value: style.PnLSignString(s.TotalBaseProfit), + Short: true, + }) + } + + if len(s.TotalFee) > 0 { + for feeCurrency, fee := range s.TotalFee { + fields = append(fields, slack.AttachmentField{ + Title: fmt.Sprintf("Fee (%s)", feeCurrency), + Value: fee.String() + " " + feeCurrency, + Short: true, + }) + + } + } + + return slack.Attachment{ + Title: "Grid Profit Stats", + Color: "warning", + Fields: fields, + } +} From 19478b1fbc0913a43ba8ca77497c965fbef6f564 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 15:39:48 +0800 Subject: [PATCH 0268/1392] grid2: add profit stats since field --- pkg/strategy/grid2/profit_stats.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/profit_stats.go b/pkg/strategy/grid2/profit_stats.go index 9a0d1aaa97..8147f9ab85 100644 --- a/pkg/strategy/grid2/profit_stats.go +++ b/pkg/strategy/grid2/profit_stats.go @@ -3,6 +3,7 @@ package grid2 import ( "fmt" "strconv" + "time" "github.com/slack-go/slack" @@ -22,6 +23,7 @@ type GridProfitStats struct { Volume fixedpoint.Value `json:"volume,omitempty"` Market types.Market `json:"market,omitempty"` ProfitEntries []*GridProfit `json:"profitEntries,omitempty"` + Since *time.Time `json:"since,omitempty"` } func newGridProfitStats(market types.Market) *GridProfitStats { From 1002c13062251e829cf18f6a823801de7a6577a9 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:04:43 +0800 Subject: [PATCH 0269/1392] feature/dynamic_exposure: move to risk package --- pkg/{dynamicmetric => risk}/dynamic_exposure.go | 2 +- pkg/strategy/linregmaker/strategy.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename pkg/{dynamicmetric => risk}/dynamic_exposure.go (99%) diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/risk/dynamic_exposure.go similarity index 99% rename from pkg/dynamicmetric/dynamic_exposure.go rename to pkg/risk/dynamic_exposure.go index d150e11d95..d3eb899e2a 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/risk/dynamic_exposure.go @@ -1,4 +1,4 @@ -package dynamicmetric +package risk import ( "github.com/c9s/bbgo/pkg/bbgo" diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index ea6d7e9a3f..669133a3dc 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/c9s/bbgo/pkg/dynamicmetric" + "github.com/c9s/bbgo/pkg/risk" "sync" "github.com/c9s/bbgo/pkg/indicator" @@ -110,7 +111,7 @@ type Strategy struct { // DynamicExposure is used to define the exposure position range with the given percentage. // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically - DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` + DynamicExposure risk.DynamicExposure `json:"dynamicExposure"` bbgo.QuantityOrAmount From 4c98bed76fe726fa75cd5f99caf25140821a35a7 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:08:14 +0800 Subject: [PATCH 0270/1392] feature/dynamic_quantity: add comment for getQuantity() --- pkg/dynamicmetric/dynamic_quantity.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/dynamicmetric/dynamic_quantity.go b/pkg/dynamicmetric/dynamic_quantity.go index fa3182adac..4fc2224942 100644 --- a/pkg/dynamicmetric/dynamic_quantity.go +++ b/pkg/dynamicmetric/dynamic_quantity.go @@ -84,6 +84,7 @@ func (d *DynamicQuantityLinReg) initialize(symbol string, session *bbgo.Exchange } // getQuantity returns quantity +// If reverse is true, the LinReg slope ratio is reversed, ie -0.01 becomes 0.01. This is for short orders. func (d *DynamicQuantityLinReg) getQuantity(reverse bool) (fixedpoint.Value, error) { var linregRatio float64 if reverse { From 5c7c125c99524978e83704def3849ac031b3bd12 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:09:47 +0800 Subject: [PATCH 0271/1392] feature/dynamic_spread: move to risk package --- pkg/{dynamicmetric => risk}/dynamic_spread.go | 2 +- pkg/strategy/linregmaker/strategy.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename pkg/{dynamicmetric => risk}/dynamic_spread.go (99%) diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/risk/dynamic_spread.go similarity index 99% rename from pkg/dynamicmetric/dynamic_spread.go rename to pkg/risk/dynamic_spread.go index 905f49bc34..9714d1baf3 100644 --- a/pkg/dynamicmetric/dynamic_spread.go +++ b/pkg/risk/dynamic_spread.go @@ -1,4 +1,4 @@ -package dynamicmetric +package risk import ( "github.com/pkg/errors" diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 669133a3dc..bcc537ca64 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -103,7 +103,7 @@ type Strategy struct { // DynamicSpread enables the automatic adjustment to bid and ask spread. // Overrides Spread, BidSpread, and AskSpread - DynamicSpread dynamicmetric.DynamicSpread `json:"dynamicSpread,omitempty"` + DynamicSpread risk.DynamicSpread `json:"dynamicSpread,omitempty"` // MaxExposurePosition is the maximum position you can hold // 10 means you can hold 10 ETH long/short position by maximum From 5b017cd361d88ae4c0844cea6cc3b9c080cba43c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:12:04 +0800 Subject: [PATCH 0272/1392] feature/dynamic_spread: rename DynamicSpreadAmp to DynamicAmpSpread --- pkg/risk/dynamic_spread.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/risk/dynamic_spread.go b/pkg/risk/dynamic_spread.go index 9714d1baf3..3776d83515 100644 --- a/pkg/risk/dynamic_spread.go +++ b/pkg/risk/dynamic_spread.go @@ -12,7 +12,7 @@ import ( type DynamicSpread struct { // AmpSpread calculates spreads based on kline amplitude - AmpSpread *DynamicSpreadAmp `json:"amplitude"` + AmpSpread *DynamicAmpSpread `json:"amplitude"` // WeightedBollWidthRatioSpread calculates spreads based on two Bollinger Bands WeightedBollWidthRatioSpread *DynamicSpreadBollWidthRatio `json:"weightedBollWidth"` @@ -57,7 +57,7 @@ func (ds *DynamicSpread) GetBidSpread() (bidSpread float64, err error) { } // DynamicSpreadAmp uses kline amplitude to calculate spreads -type DynamicSpreadAmp struct { +type DynamicAmpSpread struct { types.IntervalWindow // AskSpreadScale is used to define the ask spread range with the given percentage. @@ -71,7 +71,7 @@ type DynamicSpreadAmp struct { } // initialize amplitude dynamic spread and preload SMAs -func (ds *DynamicSpreadAmp) initialize(symbol string, session *bbgo.ExchangeSession) { +func (ds *DynamicAmpSpread) initialize(symbol string, session *bbgo.ExchangeSession) { ds.dynamicBidSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}} ds.dynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}} @@ -95,7 +95,7 @@ func (ds *DynamicSpreadAmp) initialize(symbol string, session *bbgo.ExchangeSess } // update amplitude dynamic spread with kline -func (ds *DynamicSpreadAmp) update(kline types.KLine) { +func (ds *DynamicAmpSpread) update(kline types.KLine) { // ampl is the amplitude of kline ampl := (kline.GetHigh().Float64() - kline.GetLow().Float64()) / kline.GetOpen().Float64() @@ -112,7 +112,7 @@ func (ds *DynamicSpreadAmp) update(kline types.KLine) { } } -func (ds *DynamicSpreadAmp) getAskSpread() (askSpread float64, err error) { +func (ds *DynamicAmpSpread) getAskSpread() (askSpread float64, err error) { if ds.AskSpreadScale != nil && ds.dynamicAskSpread.Length() >= ds.Window { askSpread, err = ds.AskSpreadScale.Scale(ds.dynamicAskSpread.Last()) if err != nil { @@ -126,7 +126,7 @@ func (ds *DynamicSpreadAmp) getAskSpread() (askSpread float64, err error) { return 0, errors.New("incomplete dynamic spread settings or not enough data yet") } -func (ds *DynamicSpreadAmp) getBidSpread() (bidSpread float64, err error) { +func (ds *DynamicAmpSpread) getBidSpread() (bidSpread float64, err error) { if ds.BidSpreadScale != nil && ds.dynamicBidSpread.Length() >= ds.Window { bidSpread, err = ds.BidSpreadScale.Scale(ds.dynamicBidSpread.Last()) if err != nil { From 754f8da5d43900f3e1624293bf0c93f323714e71 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:23:43 +0800 Subject: [PATCH 0273/1392] strategy/linregmaker: move private fields to the end of the struct --- pkg/strategy/linregmaker/strategy.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index bcc537ca64..54f8c3a951 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -32,11 +32,6 @@ func init() { } type Strategy struct { - Environment *bbgo.Environment - StandardIndicatorSet *bbgo.StandardIndicatorSet - Market types.Market - ctx context.Context - // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -124,8 +119,6 @@ type Strategy struct { // UseDynamicQuantityAsAmount calculates amount instead of quantity UseDynamicQuantityAsAmount bool `json:"useDynamicQuantityAsAmount"` - session *bbgo.ExchangeSession - // ExitMethods are various TP/SL methods ExitMethods bbgo.ExitMethodSet `json:"exits"` @@ -134,6 +127,13 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + Environment *bbgo.Environment + StandardIndicatorSet *bbgo.StandardIndicatorSet + Market types.Market + ctx context.Context + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor groupID uint32 From 095eb9c134fbf54acb846eaf326bac5fe90026f9 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:25:56 +0800 Subject: [PATCH 0274/1392] feature/dynamic_exposure: undo move dynamic_exposure and dynamic_spread --- pkg/{risk => dynamicmetric}/dynamic_exposure.go | 2 +- pkg/{risk => dynamicmetric}/dynamic_spread.go | 2 +- pkg/strategy/linregmaker/strategy.go | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) rename pkg/{risk => dynamicmetric}/dynamic_exposure.go (99%) rename pkg/{risk => dynamicmetric}/dynamic_spread.go (99%) diff --git a/pkg/risk/dynamic_exposure.go b/pkg/dynamicmetric/dynamic_exposure.go similarity index 99% rename from pkg/risk/dynamic_exposure.go rename to pkg/dynamicmetric/dynamic_exposure.go index d3eb899e2a..d150e11d95 100644 --- a/pkg/risk/dynamic_exposure.go +++ b/pkg/dynamicmetric/dynamic_exposure.go @@ -1,4 +1,4 @@ -package risk +package dynamicmetric import ( "github.com/c9s/bbgo/pkg/bbgo" diff --git a/pkg/risk/dynamic_spread.go b/pkg/dynamicmetric/dynamic_spread.go similarity index 99% rename from pkg/risk/dynamic_spread.go rename to pkg/dynamicmetric/dynamic_spread.go index 3776d83515..2b2e3d1493 100644 --- a/pkg/risk/dynamic_spread.go +++ b/pkg/dynamicmetric/dynamic_spread.go @@ -1,4 +1,4 @@ -package risk +package dynamicmetric import ( "github.com/pkg/errors" diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 54f8c3a951..8c6f031e07 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "github.com/c9s/bbgo/pkg/dynamicmetric" - "github.com/c9s/bbgo/pkg/risk" "sync" "github.com/c9s/bbgo/pkg/indicator" @@ -98,7 +97,7 @@ type Strategy struct { // DynamicSpread enables the automatic adjustment to bid and ask spread. // Overrides Spread, BidSpread, and AskSpread - DynamicSpread risk.DynamicSpread `json:"dynamicSpread,omitempty"` + DynamicSpread dynamicmetric.DynamicSpread `json:"dynamicSpread,omitempty"` // MaxExposurePosition is the maximum position you can hold // 10 means you can hold 10 ETH long/short position by maximum @@ -106,7 +105,7 @@ type Strategy struct { // DynamicExposure is used to define the exposure position range with the given percentage. // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically - DynamicExposure risk.DynamicExposure `json:"dynamicExposure"` + DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` bbgo.QuantityOrAmount From c0598a05f6302f80a103dee21fb06e15bfdc2152 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 17:47:34 +0800 Subject: [PATCH 0275/1392] grid2: add slack attachment footer --- pkg/strategy/grid2/profit_stats.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/profit_stats.go b/pkg/strategy/grid2/profit_stats.go index 8147f9ab85..d19bf6fbd9 100644 --- a/pkg/strategy/grid2/profit_stats.go +++ b/pkg/strategy/grid2/profit_stats.go @@ -51,6 +51,11 @@ func (s *GridProfitStats) AddTrade(trade types.Trade) { } else { s.TotalFee[trade.FeeCurrency] = trade.Fee } + + if s.Since == nil { + t := trade.Time.Time() + s.Since = &t + } } func (s *GridProfitStats) AddProfit(profit *GridProfit) { @@ -115,13 +120,18 @@ func (s *GridProfitStats) SlackAttachment() slack.Attachment { Value: fee.String() + " " + feeCurrency, Short: true, }) - } } + footer := "Total grid profit stats" + if s.Since != nil { + footer += fmt.Sprintf(" since %s", s.Since.String()) + } + return slack.Attachment{ Title: "Grid Profit Stats", Color: "warning", Fields: fields, + Footer: footer, } } From e39b94cf51868830a849384accfd590bddb60297 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Dec 2022 17:50:05 +0800 Subject: [PATCH 0276/1392] bbgo/standard_indicator_set: embed BOLL's SMA initialization into the constructor literal --- pkg/bbgo/standard_indicator_set.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/bbgo/standard_indicator_set.go b/pkg/bbgo/standard_indicator_set.go index 9228567265..e291cccc6f 100644 --- a/pkg/bbgo/standard_indicator_set.go +++ b/pkg/bbgo/standard_indicator_set.go @@ -140,9 +140,8 @@ func (s *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth} inc, ok := s.iwbIndicators[iwb] if !ok { - inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth} + inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth, SMA: &indicator.SMA{IntervalWindow: iw}} s.initAndBind(inc, iw.Interval) - inc.SMA = &indicator.SMA{IntervalWindow: iw} if debugBOLL { inc.OnUpdate(func(sma float64, upBand float64, downBand float64) { From bc8d7e99683f058d0a5447ecbc9985c2aff7dd1b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 18:09:43 +0800 Subject: [PATCH 0277/1392] grid2: add skipSpreadCheck option --- config/grid2-max.yaml | 3 +++ pkg/strategy/grid2/profit_stats.go | 3 ++- pkg/strategy/grid2/strategy.go | 8 ++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index dea3da8097..addd52bfef 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -83,3 +83,6 @@ exchangeStrategies: resetPositionWhenStart: true clearOpenOrdersWhenStart: false keepOrdersWhenShutdown: false + + ## skipSpreadCheck skips the minimal spread check for the grid profit + # skipSpreadCheck: true diff --git a/pkg/strategy/grid2/profit_stats.go b/pkg/strategy/grid2/profit_stats.go index d19bf6fbd9..40001369ed 100644 --- a/pkg/strategy/grid2/profit_stats.go +++ b/pkg/strategy/grid2/profit_stats.go @@ -128,8 +128,9 @@ func (s *GridProfitStats) SlackAttachment() slack.Attachment { footer += fmt.Sprintf(" since %s", s.Since.String()) } + title := fmt.Sprintf("%s Grid Profit Stats", s.Symbol) return slack.Attachment{ - Title: "Grid Profit Stats", + Title: title, Color: "warning", Fields: fields, Footer: footer, diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 0535523097..dc88aa77d8 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -103,6 +103,8 @@ type Strategy struct { // it makes sure that your grid configuration is profitable. FeeRate fixedpoint.Value `json:"feeRate"` + SkipSpreadCheck bool `json:"skipSpreadCheck"` + GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"` ProfitStats *types.ProfitStats `persistence:"profit_stats"` Position *types.Position `persistence:"position"` @@ -141,8 +143,10 @@ func (s *Strategy) Validate() error { return fmt.Errorf("gridNum can not be zero") } - if err := s.checkSpread(); err != nil { - return errors.Wrapf(err, "spread is too small, please try to reduce your gridNum or increase the price range (upperPrice and lowerPrice)") + if !s.SkipSpreadCheck { + if err := s.checkSpread(); err != nil { + return errors.Wrapf(err, "spread is too small, please try to reduce your gridNum or increase the price range (upperPrice and lowerPrice)") + } } if !s.QuantityOrAmount.IsSet() && s.QuoteInvestment.IsZero() { From ac8186d43d682c73f27fc6a4ed6c856d9bb2d686 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 18:30:28 +0800 Subject: [PATCH 0278/1392] grid2: debug submitOrder before sending them to the api --- config/grid2-max.yaml | 6 +++++- pkg/strategy/grid2/strategy.go | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index addd52bfef..ad74586c7f 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -52,8 +52,12 @@ exchangeStrategies: ## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000 # triggerPrice: 16_900.0 + ## stopLossPrice is used for closing the grid and sell all the inventory to stop loss. + ## (optional) stopLossPrice: 16_000.0 + ## takeProfitPrice is used for closing the grid and sell all the inventory at higher price to take profit + ## (optional) takeProfitPrice: 20_000.0 ## profitSpread is the profit spread of the arbitrage order (sell order) @@ -85,4 +89,4 @@ exchangeStrategies: keepOrdersWhenShutdown: false ## skipSpreadCheck skips the minimal spread check for the grid profit - # skipSpreadCheck: true + skipSpreadCheck: true diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index dc88aa77d8..5aa4e0cb3b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -809,13 +809,13 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return err } + s.debugGridOrders(submitOrders, lastPrice) + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...) if err2 != nil { return err } - s.debugGridOrders(submitOrders, lastPrice) - for _, order := range createdOrders { s.logger.Info(order.String()) } From a340cd321b3ee871e4255aaf291adbedfd6eaa87 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 18:38:57 +0800 Subject: [PATCH 0279/1392] max: add submit order limiter --- pkg/exchange/max/exchange.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 36f352b5a5..8f436a00f1 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -25,6 +25,7 @@ var closedOrderQueryLimiter = rate.NewLimiter(rate.Every(1*time.Second), 1) var tradeQueryLimiter = rate.NewLimiter(rate.Every(3*time.Second), 1) var accountQueryLimiter = rate.NewLimiter(rate.Every(3*time.Second), 1) var marketDataLimiter = rate.NewLimiter(rate.Every(2*time.Second), 10) +var submitOrderLimiter = rate.NewLimiter(rate.Every(300*time.Millisecond), 10) var log = logrus.WithField("exchange", "max") @@ -486,6 +487,10 @@ func (e *Exchange) Withdraw(ctx context.Context, asset string, amount fixedpoint } func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) { + if err := submitOrderLimiter.Wait(ctx); err != nil { + return nil, err + } + walletType := maxapi.WalletTypeSpot if e.MarginSettings.IsMargin { walletType = maxapi.WalletTypeMargin From f4529171f37a00b27cc51324e70bbdbe11db599a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 18:39:06 +0800 Subject: [PATCH 0280/1392] config: turn on notification for grid2 --- config/grid2-max.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index ad74586c7f..3895bf2ac1 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -5,8 +5,8 @@ notifications: errorChannel: "bbgo-error" switches: trade: false - orderUpdate: false - submitOrder: false + orderUpdate: true + submitOrder: true sessions: max: From 1964763f5807314a8b4ce3cc614ba200598c9866 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 18:41:04 +0800 Subject: [PATCH 0281/1392] grid2: add more logs --- pkg/strategy/grid2/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5aa4e0cb3b..75021f28a6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -816,6 +816,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return err } + s.logger.Infof("grid orders submitted:") for _, order := range createdOrders { s.logger.Info(order.String()) } From c2133a17124ef2aaedfcfd2fcd247675bd3a6794 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 18:42:25 +0800 Subject: [PATCH 0282/1392] grid2: call bbgo sync api to sync profit stats --- pkg/strategy/grid2/strategy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 75021f28a6..5a9a06cb69 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1034,7 +1034,11 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) - orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) + orderExecutor.ActiveMakerOrders().OnFilled(func(o types.Order) { + s.handleOrderFilled(o) + bbgo.Sync(context.Background(), s) + }) + s.orderExecutor = orderExecutor // TODO: detect if there are previous grid orders on the order book From 051aa19989bebf84aa8020523787695bd58b6f49 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 18:47:45 +0800 Subject: [PATCH 0283/1392] grid2: add newOrderUpdateHandler and send profit to notification --- pkg/strategy/grid2/strategy.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5a9a06cb69..6044e37d91 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -349,6 +349,9 @@ func (s *Strategy) processFilledOrder(o types.Order) { s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) s.GridProfitStats.AddProfit(profit) + bbgo.Notify(profit) + bbgo.Notify(s.GridProfitStats) + case types.SideTypeBuy: newSide = types.SideTypeSell if !s.ProfitSpread.IsZero() { @@ -652,6 +655,13 @@ func (s *Strategy) newTriggerPriceHandler(ctx context.Context, session *bbgo.Exc }) } +func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.ExchangeSession) func(o types.Order) { + return func(o types.Order) { + s.handleOrderFilled(o) + bbgo.Sync(ctx, s) + } +} + func (s *Strategy) newStopLossPriceHandler(ctx context.Context, session *bbgo.ExchangeSession) types.KLineCallback { return types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { if s.StopLossPrice.Compare(k.Low) < 0 { @@ -1034,10 +1044,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) - orderExecutor.ActiveMakerOrders().OnFilled(func(o types.Order) { - s.handleOrderFilled(o) - bbgo.Sync(context.Background(), s) - }) + orderExecutor.ActiveMakerOrders().OnFilled(s.newOrderUpdateHandler(ctx, session)) s.orderExecutor = orderExecutor From fcd7a20b784c2007b6c31b29812e64190fe8639a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 18:54:02 +0800 Subject: [PATCH 0284/1392] bbgo,grid2: add place order error log --- pkg/bbgo/order_executor_general.go | 4 ++++ pkg/strategy/grid2/strategy.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 40b63c9eee..751366b2b8 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -218,6 +218,10 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. } createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, formattedOrders...) + if err != nil { + log.WithError(err).Errorf("place order error, will retry orders: %v", errIdx) + } + if len(errIdx) > 0 { createdOrders2, err2 := BatchRetryPlaceOrder(ctx, e.session.Exchange, errIdx, formattedOrders...) if err2 != nil { diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6044e37d91..25c04c3aef 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -826,11 +826,11 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return err } - s.logger.Infof("grid orders submitted:") for _, order := range createdOrders { s.logger.Info(order.String()) } + s.logger.Infof("ALL GRID ORDERS SUBMITTED") return nil } From 78f10212b91ff455f680078d074b9679bf192850 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 18:57:21 +0800 Subject: [PATCH 0285/1392] grid2: fix base fee format --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 25c04c3aef..83a039576d 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -321,7 +321,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { if o.Side == types.SideTypeBuy { baseSellQuantityReduction = s.aggregateOrderBaseFee(o) - s.logger.Infof("buy order base fee: %f %s", baseSellQuantityReduction.Float64(), s.Market.BaseCurrency) + s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) newQuantity = newQuantity.Sub(baseSellQuantityReduction) } From bbc47bb63ac63b2c544dd2e9cbe0804aea351c85 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 19:06:34 +0800 Subject: [PATCH 0286/1392] grid2: remove todo item --- pkg/strategy/grid2/strategy.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 83a039576d..730d365a4d 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -343,8 +343,6 @@ func (s *Strategy) processFilledOrder(o types.Order) { newQuantity = fixedpoint.Max(orderQuoteQuantity.Div(newPrice), s.Market.MinQuantity) } - // calculate profit - // TODO: send profit notification profit := s.calculateProfit(o, newPrice, newQuantity) s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) s.GridProfitStats.AddProfit(profit) From fa73b0e7f7b09485591d7986d4f33ff3b3e6c357 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Dec 2022 19:20:15 +0800 Subject: [PATCH 0287/1392] grid2: add warning message --- pkg/strategy/grid2/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 730d365a4d..95cfbec82f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -388,6 +388,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { // handleOrderFilled is called when an order status is FILLED func (s *Strategy) handleOrderFilled(o types.Order) { if s.grid == nil { + s.logger.Warn("grid is not opened yet, skip order update event") return } From d5e37f03e29a915a20e4d5f610ca91abeff640eb Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Dec 2022 11:51:52 +0800 Subject: [PATCH 0288/1392] feature/dynamic_*: move dynamic_* to risk/dynamicrisk package --- .../dynamicrisk}/dynamic_exposure.go | 2 +- .../dynamicrisk}/dynamic_quantity.go | 2 +- .../dynamicrisk}/dynamic_spread.go | 2 +- pkg/strategy/linregmaker/strategy.go | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) rename pkg/{dynamicmetric => risk/dynamicrisk}/dynamic_exposure.go (99%) rename pkg/{dynamicmetric => risk/dynamicrisk}/dynamic_quantity.go (99%) rename pkg/{dynamicmetric => risk/dynamicrisk}/dynamic_spread.go (99%) diff --git a/pkg/dynamicmetric/dynamic_exposure.go b/pkg/risk/dynamicrisk/dynamic_exposure.go similarity index 99% rename from pkg/dynamicmetric/dynamic_exposure.go rename to pkg/risk/dynamicrisk/dynamic_exposure.go index d150e11d95..71eb121557 100644 --- a/pkg/dynamicmetric/dynamic_exposure.go +++ b/pkg/risk/dynamicrisk/dynamic_exposure.go @@ -1,4 +1,4 @@ -package dynamicmetric +package dynamicrisk import ( "github.com/c9s/bbgo/pkg/bbgo" diff --git a/pkg/dynamicmetric/dynamic_quantity.go b/pkg/risk/dynamicrisk/dynamic_quantity.go similarity index 99% rename from pkg/dynamicmetric/dynamic_quantity.go rename to pkg/risk/dynamicrisk/dynamic_quantity.go index 4fc2224942..d2d1e8c0ee 100644 --- a/pkg/dynamicmetric/dynamic_quantity.go +++ b/pkg/risk/dynamicrisk/dynamic_quantity.go @@ -1,4 +1,4 @@ -package dynamicmetric +package dynamicrisk import ( "github.com/c9s/bbgo/pkg/bbgo" diff --git a/pkg/dynamicmetric/dynamic_spread.go b/pkg/risk/dynamicrisk/dynamic_spread.go similarity index 99% rename from pkg/dynamicmetric/dynamic_spread.go rename to pkg/risk/dynamicrisk/dynamic_spread.go index 2b2e3d1493..2b5cbbeb89 100644 --- a/pkg/dynamicmetric/dynamic_spread.go +++ b/pkg/risk/dynamicrisk/dynamic_spread.go @@ -1,4 +1,4 @@ -package dynamicmetric +package dynamicrisk import ( "github.com/pkg/errors" diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 8c6f031e07..a0d2f27300 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -3,7 +3,7 @@ package linregmaker import ( "context" "fmt" - "github.com/c9s/bbgo/pkg/dynamicmetric" + "github.com/c9s/bbgo/pkg/risk/dynamicrisk" "sync" "github.com/c9s/bbgo/pkg/indicator" @@ -97,7 +97,7 @@ type Strategy struct { // DynamicSpread enables the automatic adjustment to bid and ask spread. // Overrides Spread, BidSpread, and AskSpread - DynamicSpread dynamicmetric.DynamicSpread `json:"dynamicSpread,omitempty"` + DynamicSpread dynamicrisk.DynamicSpread `json:"dynamicSpread,omitempty"` // MaxExposurePosition is the maximum position you can hold // 10 means you can hold 10 ETH long/short position by maximum @@ -105,15 +105,15 @@ type Strategy struct { // DynamicExposure is used to define the exposure position range with the given percentage. // When DynamicExposure is set, your MaxExposurePosition will be calculated dynamically - DynamicExposure dynamicmetric.DynamicExposure `json:"dynamicExposure"` + DynamicExposure dynamicrisk.DynamicExposure `json:"dynamicExposure"` bbgo.QuantityOrAmount // DynamicQuantityIncrease calculates the increase position order quantity dynamically - DynamicQuantityIncrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityIncrease"` + DynamicQuantityIncrease dynamicrisk.DynamicQuantitySet `json:"dynamicQuantityIncrease"` // DynamicQuantityDecrease calculates the decrease position order quantity dynamically - DynamicQuantityDecrease dynamicmetric.DynamicQuantitySet `json:"dynamicQuantityDecrease"` + DynamicQuantityDecrease dynamicrisk.DynamicQuantitySet `json:"dynamicQuantityDecrease"` // UseDynamicQuantityAsAmount calculates amount instead of quantity UseDynamicQuantityAsAmount bool `json:"useDynamicQuantityAsAmount"` From 811be7893351d7c87c6065d7c35f14a9f7a1245a Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 17 Dec 2022 11:57:32 +0800 Subject: [PATCH 0289/1392] grid2: update log message --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 95cfbec82f..6c80df5ec9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -381,7 +381,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { if createdOrders, err := s.orderExecutor.SubmitOrders(context.Background(), orderForm); err != nil { s.logger.WithError(err).Errorf("can not submit arbitrage order") } else { - s.logger.Infof("order created: %+v", createdOrders) + s.logger.Infof("GRID REVERSE ORDER IS CREATED: %+v", createdOrders) } } From 6f2664b03e9cef4f2088aa613e333e4783361e30 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Dec 2022 18:51:39 +0800 Subject: [PATCH 0290/1392] grid2: fix test for orderTag --- pkg/strategy/grid2/strategy_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 9b1a545a93..3225859b84 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -444,7 +444,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Side: types.SideTypeSell, TimeInForce: types.TimeInForceGTC, Market: s.Market, - Tag: "grid", + Tag: orderTag, } orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) @@ -509,7 +509,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Side: types.SideTypeSell, TimeInForce: types.TimeInForceGTC, Market: s.Market, - Tag: "grid", + Tag: orderTag, } orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) @@ -577,7 +577,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Quantity: number(0.09166666), TimeInForce: types.TimeInForceGTC, Market: s.Market, - Tag: "grid", + Tag: orderTag, } orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{ {SubmitOrder: expectedSubmitOrder}, @@ -591,7 +591,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Quantity: number(0.09999999), TimeInForce: types.TimeInForceGTC, Market: s.Market, - Tag: "grid", + Tag: orderTag, } orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder2).Return([]types.Order{ {SubmitOrder: expectedSubmitOrder2}, @@ -664,7 +664,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Side: types.SideTypeSell, TimeInForce: types.TimeInForceGTC, Market: s.Market, - Tag: "grid", + Tag: orderTag, } orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) @@ -680,7 +680,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Side: types.SideTypeBuy, TimeInForce: types.TimeInForceGTC, Market: s.Market, - Tag: "grid", + Tag: orderTag, } orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder2).Return([]types.Order{ From 30f471db00f866edf6d1d20209f05eb62850ba00 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Dec 2022 19:00:14 +0800 Subject: [PATCH 0291/1392] binance: fix execution report parsing --- pkg/exchange/binance/parse.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index 3be3564f33..edbd2504cb 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -75,7 +75,9 @@ type ExecutionReportEvent struct { OrderPrice fixedpoint.Value `json:"p"` StopPrice fixedpoint.Value `json:"P"` - IsOnBook bool `json:"w"` + IsOnBook bool `json:"w"` + WorkingTime types.MillisecondTimestamp `json:"W"` + TrailingTime types.MillisecondTimestamp `json:"D"` IsMaker bool `json:"m"` Ignore bool `json:"M"` From 756bfe24021b4df5366e4086c7b8b799646374fd Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Dec 2022 19:00:35 +0800 Subject: [PATCH 0292/1392] types: print message body when message parse error --- pkg/types/stream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/types/stream.go b/pkg/types/stream.go index ffef7ffeb4..c1f9a8f766 100644 --- a/pkg/types/stream.go +++ b/pkg/types/stream.go @@ -242,7 +242,7 @@ func (s *StandardStream) Read(ctx context.Context, conn *websocket.Conn, cancel if s.parser != nil { e, err = s.parser(message) if err != nil { - log.WithError(err).Errorf("websocket event parse error") + log.WithError(err).Errorf("websocket event parse error, message: %s", message) continue } } From 5d6669fde44f7f0cfae03c34598d74af6445671d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Dec 2022 19:00:52 +0800 Subject: [PATCH 0293/1392] config: update grid2 config for sync --- config/grid2.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/config/grid2.yaml b/config/grid2.yaml index 6d240ae42e..aed8bc28da 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -13,6 +13,26 @@ sessions: exchange: binance envVarPrefix: binance +sync: + # userDataStream is used to sync the trading data in real-time + # it uses the websocket connection to insert the trades + userDataStream: + trades: false + filledOrders: false + + # since is the start date of your trading data + since: 2019-01-01 + + # sessions is the list of session names you want to sync + # by default, BBGO sync all your available sessions. + sessions: + - binance + + # symbols is the list of symbols you want to sync + # by default, BBGO try to guess your symbols by your existing account balances. + symbols: + - BTCUSDT + # example command: # go run ./cmd/bbgo backtest --config config/grid2.yaml --base-asset-baseline backtest: From 565193ed6923428b92760bb122451e91a71d2a98 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Dec 2022 20:03:22 +0800 Subject: [PATCH 0294/1392] config: add multi-session example --- config/multi-session.yaml | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 config/multi-session.yaml diff --git a/config/multi-session.yaml b/config/multi-session.yaml new file mode 100644 index 0000000000..79f7a20ec3 --- /dev/null +++ b/config/multi-session.yaml @@ -0,0 +1,40 @@ +--- +sessions: + binance_margin_linkusdt: + exchange: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: LINKUSDT + + binance_cross_margin: + exchange: binance + margin: true + # + # The following API key/secret will be used: + # BINANCE2_API_KEY= + # BINANCE2_API_SECRET= + envVarPrefix: binance2 + +exchangeStrategies: + +- on: binance_margin_linkusdt + support: + symbol: LINKUSDT + interval: 1m + minVolume: 2_000 + marginOrderSideEffect: borrow + + scaleQuantity: + byVolume: + exp: + domain: [ 1_000, 200_000 ] + range: [ 3.0, 5.0 ] + + maxBaseAssetBalance: 1000.0 + minQuoteAssetBalance: 2000.0 + + targets: + - profitPercentage: 0.02 + quantityPercentage: 0.5 + marginOrderSideEffect: repay + From c230695a4478f3ba648b1592328678dba0e80b81 Mon Sep 17 00:00:00 2001 From: Yo-An Lin Date: Mon, 19 Dec 2022 20:08:06 +0800 Subject: [PATCH 0295/1392] Update multi-session.yaml --- config/multi-session.yaml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/config/multi-session.yaml b/config/multi-session.yaml index 79f7a20ec3..1ec7f47d8a 100644 --- a/config/multi-session.yaml +++ b/config/multi-session.yaml @@ -11,9 +11,19 @@ sessions: margin: true # # The following API key/secret will be used: - # BINANCE2_API_KEY= - # BINANCE2_API_SECRET= - envVarPrefix: binance2 + # BINANCE_CROSS_MARGIN_API_KEY=____YOUR_MARGIN_API_KEY____ + # BINANCE_CROSS_MARGIN_API_SECRET=____YOUR_MARGIN_API_SECRET____ + envVarPrefix: BINANCE_CROSS_MARGIN + + binance: + exchange: binance + # + # The following API key/secret will be used: + # BINANCE_SPOT_API_KEY=____YOUR_SPOT_API_KEY____ + # BINANCE_SPOT_API_SECRET=____YOUR_SPOT_API_SECRET____ + envVarPrefix: BINANCE_SPOT + + exchangeStrategies: From 1fd431df5fc28d01b2c3c0acc320790a5c8a7a9a Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 20 Dec 2022 15:38:56 +0800 Subject: [PATCH 0296/1392] doc: add back-test report viewer doc link --- doc/topics/back-testing.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/topics/back-testing.md b/doc/topics/back-testing.md index 058a2a8d60..2e7d3b7878 100644 --- a/doc/topics/back-testing.md +++ b/doc/topics/back-testing.md @@ -76,7 +76,6 @@ godotenv -f .env.local -- go run ./cmd/bbgo backtest --config config/grid.yaml - ## See Also -If you want to test the max draw down (MDD) you can adjust the start date to somewhere near 2020-03-12 - -See +* [apps/backtest-report](../../apps/backtest-report) - BBGO's built-in backtest report viewer +* [MDD](https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp) - If you want to test the max draw down (MDD) you can adjust the start date to somewhere near 2020-03-12. From 330be79ec6fd60f8a2746d4c68f20de385431a98 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 20 Dec 2022 15:56:38 +0800 Subject: [PATCH 0297/1392] grid2: add recoverGrid --- pkg/strategy/grid2/strategy.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6c80df5ec9..c829ae3f8b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" "sync" + "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -986,6 +987,33 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { return nil } +func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { + historyService, ok := session.Exchange.(types.ExchangeTradeHistoryService) + if !ok { + return nil + } + + orders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } + + firstOrderTime := orders[0].CreationTime.Time() + for _, o := range orders { + if o.CreationTime.Before(firstOrderTime) { + firstOrderTime = o.CreationTime.Time() + } + } + + closedOrders, err := historyService.QueryClosedOrders(ctx, s.Symbol, firstOrderTime, time.Now(), 0) + if err != nil { + return err + } + + _ = closedOrders + return nil +} + func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() From bbe58f51cf818f069f66b8230b949ac980d3b2b5 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 20 Dec 2022 16:05:50 +0800 Subject: [PATCH 0298/1392] doc: simplify backtest command example --- apps/backtest-report/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backtest-report/README.md b/apps/backtest-report/README.md index 332787c08d..bc79c74e6c 100644 --- a/apps/backtest-report/README.md +++ b/apps/backtest-report/README.md @@ -19,7 +19,7 @@ Create a symlink to your back-test report output directory: Generate some back-test reports: ``` -(cd ../.. && go run ./cmd/bbgo backtest --config bollmaker_ethusdt.yaml --debug --session binance --output output --subdir) +(cd ../.. && go run ./cmd/bbgo backtest --config bollmaker_ethusdt.yaml --debug --output output --subdir) ``` Start the development server: From 130cf2468ddf0b425f107ce883277846c259f054 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 20 Dec 2022 17:33:40 +0800 Subject: [PATCH 0299/1392] types: implement lookup method --- pkg/types/ordermap.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pkg/types/ordermap.go b/pkg/types/ordermap.go index 651f3d8c7b..fbd1928013 100644 --- a/pkg/types/ordermap.go +++ b/pkg/types/ordermap.go @@ -16,17 +16,29 @@ func (m OrderMap) Backup() (orderForms []SubmitOrder) { return orderForms } +// Add the order the the map func (m OrderMap) Add(o Order) { m[o.OrderID] = o } -// Update only updates the order when the order exists in the map +// Update only updates the order when the order ID exists in the map func (m OrderMap) Update(o Order) { if _, ok := m[o.OrderID]; ok { m[o.OrderID] = o } } +func (m OrderMap) Lookup(f func(o Order) bool) *Order { + for _, order := range m { + if f(order) { + // copy and return + o := order + return &o + } + } + return nil +} + func (m OrderMap) Remove(orderID uint64) { delete(m, orderID) } @@ -153,6 +165,12 @@ func (m *SyncOrderMap) Exists(orderID uint64) (exists bool) { return exists } +func (m *SyncOrderMap) Lookup(f func(o Order) bool) *Order { + m.Lock() + defer m.Unlock() + return m.orders.Lookup(f) +} + func (m *SyncOrderMap) Len() int { m.Lock() defer m.Unlock() From f92ba9cbf1b81edf6ff3ca28de5063a43a818928 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 20 Dec 2022 17:33:53 +0800 Subject: [PATCH 0300/1392] grid2: implement recover func loading --- pkg/bbgo/activeorderbook.go | 4 +++ pkg/strategy/grid2/strategy.go | 54 ++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index f38b3dcd0a..7edab47050 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -295,3 +295,7 @@ func (b *ActiveOrderBook) NumOfOrders() int { func (b *ActiveOrderBook) Orders() types.OrderSlice { return b.orders.Orders() } + +func (b *ActiveOrderBook) Lookup(f func(o types.Order) bool) *types.Order { + return b.orders.Lookup(f) +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c829ae3f8b..b3b322c694 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -988,29 +988,71 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { } func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { - historyService, ok := session.Exchange.(types.ExchangeTradeHistoryService) - if !ok { + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) + if !implemented { return nil } - orders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { return err } - firstOrderTime := orders[0].CreationTime.Time() - for _, o := range orders { + // no open orders, the grid is not placed yet + if len(openOrders) == 0 { + return nil + } + + firstOrderTime := openOrders[0].CreationTime.Time() + for _, o := range openOrders { if o.CreationTime.Before(firstOrderTime) { firstOrderTime = o.CreationTime.Time() } } + // Allocate a local order book + orderBook := bbgo.NewActiveOrderBook(s.Symbol) + + // Add all open orders to the local order book + gridPriceMap := make(map[string]fixedpoint.Value) + for _, pin := range s.grid.Pins { + price := fixedpoint.Value(pin) + gridPriceMap[price.String()] = price + } + + // Ensure that orders are grid orders + // The price must be at the grid pin + for _, openOrder := range openOrders { + if _, exists := gridPriceMap[openOrder.Price.String()]; exists { + orderBook.Add(openOrder) + } + } + closedOrders, err := historyService.QueryClosedOrders(ctx, s.Symbol, firstOrderTime, time.Now(), 0) if err != nil { return err } - _ = closedOrders + // types.SortOrdersAscending() + // for each closed order, if it's newer than the open order's update time, we will update it. + for _, closedOrder := range closedOrders { + // skip non-grid order prices + if _, ok := gridPriceMap[closedOrder.Price.String()]; !ok { + continue + } + + existingOrder := orderBook.Lookup(func(o types.Order) bool { + return o.Price.Compare(closedOrder.Price) == 0 + }) + + if existingOrder == nil { + orderBook.Add(closedOrder) + } else { + // Compare update time and create time + orderBook.Update(closedOrder) + } + } + return nil } From 441e5d867b253380243ab436c93551fff66192ea Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 20 Dec 2022 17:34:20 +0800 Subject: [PATCH 0301/1392] grid2: add todo mark --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b3b322c694..f6e0e1cf03 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1048,7 +1048,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio if existingOrder == nil { orderBook.Add(closedOrder) } else { - // Compare update time and create time + // TODO: Compare update time and create time orderBook.Update(closedOrder) } } From 12c7f6585ad0025f90721582038c9ca779cb5ac5 Mon Sep 17 00:00:00 2001 From: chechia Date: Tue, 20 Dec 2022 17:53:19 +0800 Subject: [PATCH 0302/1392] feature: push to quay.io --- .github/workflows/docker.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4337241653..3d8e49a998 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,6 +25,7 @@ jobs: # list of Docker images to use as base name for tags images: | yoanlin/bbgo + quay.io/yoanlin/bbgo # generate Docker tags based on the following events/attributes tags: | type=schedule @@ -43,6 +44,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to Quay Container Registry + uses: docker/login-action@v2 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_TOKEN }} - name: Build and push id: docker_build uses: docker/build-push-action@v2 From 38461167badeeae0bde8a75f30425751e84df6a4 Mon Sep 17 00:00:00 2001 From: zenix Date: Fri, 9 Dec 2022 17:52:42 +0900 Subject: [PATCH 0303/1392] feature: add tsi and klinger oscillator, fix wdrift div 0 issue --- pkg/indicator/klingeroscillator.go | 118 +++++++++++++++++++ pkg/indicator/klingeroscillator_callbacks.go | 15 +++ pkg/indicator/tsi.go | 107 +++++++++++++++++ pkg/indicator/tsi_callbacks.go | 15 +++ pkg/indicator/tsi_test.go | 1 + pkg/indicator/wdrift.go | 10 +- 6 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 pkg/indicator/klingeroscillator.go create mode 100644 pkg/indicator/klingeroscillator_callbacks.go create mode 100644 pkg/indicator/tsi.go create mode 100644 pkg/indicator/tsi_callbacks.go create mode 100644 pkg/indicator/tsi_test.go diff --git a/pkg/indicator/klingeroscillator.go b/pkg/indicator/klingeroscillator.go new file mode 100644 index 0000000000..69376f5916 --- /dev/null +++ b/pkg/indicator/klingeroscillator.go @@ -0,0 +1,118 @@ +package indicator + +import "github.com/c9s/bbgo/pkg/types" + +// Refer: Klinger Oscillator +// Refer URL: https://www.investopedia.com/terms/k/klingeroscillator.asp +// Explanation: +// The Klinger Oscillator is a technical indicator that was developed by Stephen Klinger. +// It is based on the assumption that there is a relationship between money flow and price movement in the stock market. +// The Klinger Oscillator is calculated by taking the difference between a 34-period and 55-period moving average. +// Usually the indicator is using together with a 9-period or 13-period of moving average as the signal line. +// This indicator is often used to identify potential turning points in the market, as well as to confirm the strength of a trend. +//go:generate callbackgen -type KlingerOscillator +type KlingerOscillator struct { + types.SeriesBase + types.IntervalWindow + Fast *EWMA + Slow *EWMA + VF VolumeForce + + updateCallbacks []func(value float64) +} + +func (inc *KlingerOscillator) Length() int { + if inc.Fast == nil || inc.Slow == nil { + return 0 + } + return inc.Fast.Length() +} + +func (inc *KlingerOscillator) Last() float64 { + if inc.Fast == nil || inc.Slow == nil { + return 0 + } + return inc.Fast.Last() - inc.Slow.Last() +} +func (inc *KlingerOscillator) Index(i int) float64 { + if inc.Fast == nil || inc.Slow == nil { + return 0 + } + return inc.Fast.Index(i) - inc.Slow.Index(i) +} + +func (inc *KlingerOscillator) Update(high, low, cloze, volume float64) { + if inc.Fast == nil { + inc.SeriesBase.Series = inc + inc.Fast = &EWMA{IntervalWindow: types.IntervalWindow{Window: 34, Interval: inc.Interval}} + inc.Slow = &EWMA{IntervalWindow: types.IntervalWindow{Window: 55, Interval: inc.Interval}} + } + inc.VF.Update(high, low, cloze, volume) + inc.Fast.Update(inc.VF.Value) + inc.Slow.Update(inc.VF.Value) +} + +var _ types.SeriesExtend = &KlingerOscillator{} + +func (inc *KlingerOscillator) PushK(k types.KLine) { + inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64(), k.Volume.Float64()) +} + +func (inc *KlingerOscillator) CalculateAndUpdate(allKLines []types.KLine) { + if inc.Fast == nil { + for _, k := range allKLines { + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *KlingerOscillator) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *KlingerOscillator) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +// Utility to hold the state of calculation +type VolumeForce struct { + dm float64 + cm float64 + trend float64 + lastSum float64 + Value float64 +} + +func (inc *VolumeForce) Update(high, low, cloze, volume float64) { + if inc.Value == 0 { + inc.dm = high - low + inc.cm = inc.dm + inc.trend = 1. + inc.lastSum = high + low + cloze + inc.Value = volume * 100. + return + } + trend := 1. + if high+low+cloze <= inc.lastSum { + trend = -1. + } + dm := high - low + if inc.trend == trend { + inc.cm = inc.cm + dm + } else { + inc.cm = inc.dm + dm + } + inc.trend = trend + inc.lastSum = high + low + cloze + inc.dm = dm + inc.Value = volume * (2.*(inc.dm/inc.cm) - 1.) * trend * 100. +} diff --git a/pkg/indicator/klingeroscillator_callbacks.go b/pkg/indicator/klingeroscillator_callbacks.go new file mode 100644 index 0000000000..3f03f97ea0 --- /dev/null +++ b/pkg/indicator/klingeroscillator_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type KlingerOscillator"; DO NOT EDIT. + +package indicator + +import () + +func (inc *KlingerOscillator) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *KlingerOscillator) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/tsi.go b/pkg/indicator/tsi.go new file mode 100644 index 0000000000..dd62945c16 --- /dev/null +++ b/pkg/indicator/tsi.go @@ -0,0 +1,107 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: True Strength Index +// Refer URL: https://www.investopedia.com/terms/t/tsi.asp +//go:generate callbackgen -type TSI +type TSI struct { + types.SeriesBase + types.IntervalWindow + PrevValue float64 + Values floats.Slice + Pcs *EWMA + Pcds *EWMA + Apcs *EWMA + Apcds *EWMA + updateCallbacks []func(value float64) +} + +func (inc *TSI) Update(value float64) { + if inc.Pcs == nil { + inc.Pcs = &EWMA{ + IntervalWindow: types.IntervalWindow{ + Window: 25, + Interval: inc.Interval, + }, + } + inc.Pcds = &EWMA{ + IntervalWindow: types.IntervalWindow{ + Window: 13, + Interval: inc.Interval, + }, + } + inc.Apcs = &EWMA{ + IntervalWindow: types.IntervalWindow{ + Window: 25, + Interval: inc.Interval, + }, + } + inc.Apcds = &EWMA{ + IntervalWindow: types.IntervalWindow{ + Window: 13, + Interval: inc.Interval, + }, + } + inc.SeriesBase.Series = inc + } + if inc.PrevValue == 0 { + inc.PrevValue = value + return + } + pc := value - inc.PrevValue + inc.Pcs.Update(pc) + inc.Pcds.Update(inc.Pcs.Last()) + apc := math.Abs(pc) + inc.Apcs.Update(apc) + inc.Apcds.Update(inc.Apcs.Last()) + tsi := (inc.Pcds.Last() / inc.Apcds.Last()) * 100. + inc.Values.Push(tsi) + if inc.Values.Length() > MaxNumOfEWMA { + inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] + } + inc.PrevValue = value +} + +func (inc *TSI) Last() float64 { + return inc.Values.Last() +} + +func (inc *TSI) Index(i int) float64 { + return inc.Values.Index(i) +} + +func (inc *TSI) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + +var _ types.SeriesExtend = &TSI{} + +func (inc *TSI) CalculateAndUpdate(allKLines []types.KLine) { + if inc.PrevValue == 0 { + for _, k := range allKLines { + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *TSI) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + inc.CalculateAndUpdate(window) +} + +func (inc *TSI) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/tsi_callbacks.go b/pkg/indicator/tsi_callbacks.go new file mode 100644 index 0000000000..4c5155f9e1 --- /dev/null +++ b/pkg/indicator/tsi_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type TSI"; DO NOT EDIT. + +package indicator + +import () + +func (inc *TSI) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *TSI) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/tsi_test.go b/pkg/indicator/tsi_test.go new file mode 100644 index 0000000000..722d45a367 --- /dev/null +++ b/pkg/indicator/tsi_test.go @@ -0,0 +1 @@ +package indicator diff --git a/pkg/indicator/wdrift.go b/pkg/indicator/wdrift.go index e6150ee40a..61060a7470 100644 --- a/pkg/indicator/wdrift.go +++ b/pkg/indicator/wdrift.go @@ -23,23 +23,23 @@ type WeightedDrift struct { } func (inc *WeightedDrift) Update(value float64, weight float64) { - win := 10 - if inc.Window > win { - win = inc.Window + if weight == 0 { + inc.LastValue = value + return } if inc.chng == nil { inc.SeriesBase.Series = inc if inc.MA == nil { inc.MA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}} } - inc.Weight = types.NewQueue(win) + inc.Weight = types.NewQueue(inc.Window) inc.chng = types.NewQueue(inc.Window) inc.LastValue = value inc.Weight.Update(weight) return } inc.Weight.Update(weight) - base := inc.Weight.Lowest(win) + base := inc.Weight.Lowest(inc.Window) multiplier := int(weight / base) var chng float64 if value == 0 { From 2b6261651300e87340ec82fc70e36dd3a774b66a Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 13 Dec 2022 14:02:38 +0900 Subject: [PATCH 0304/1392] doc: add description about indicators generated by chatgpt --- pkg/indicator/alma.go | 14 ++++- pkg/indicator/atrp.go | 8 +++ pkg/indicator/cci.go | 9 +++ pkg/indicator/dema.go | 7 +++ pkg/indicator/dmi.go | 11 +++- pkg/indicator/drift.go | 8 +++ pkg/indicator/emv.go | 5 ++ pkg/indicator/fisher.go | 9 +++ pkg/indicator/gma.go | 7 +++ pkg/indicator/hull.go | 6 ++ pkg/indicator/macd.go | 16 ++++-- pkg/indicator/obv.go | 7 +++ pkg/indicator/rma.go | 8 +++ pkg/indicator/rsi.go | 13 +++-- pkg/indicator/stoch.go | 14 +++-- pkg/indicator/supertrend.go | 11 ++++ pkg/indicator/tema.go | 8 +++ pkg/indicator/till.go | 9 +++ pkg/indicator/vidya.go | 7 +++ pkg/indicator/volumeprofile.go | 102 +++++++++++++++++++++++++++++++++ pkg/indicator/vwap.go | 22 ++++--- pkg/indicator/vwma.go | 24 ++++---- pkg/indicator/zlema.go | 6 ++ 23 files changed, 293 insertions(+), 38 deletions(-) create mode 100644 pkg/indicator/volumeprofile.go diff --git a/pkg/indicator/alma.go b/pkg/indicator/alma.go index 2558344a34..f27de2b704 100644 --- a/pkg/indicator/alma.go +++ b/pkg/indicator/alma.go @@ -10,6 +10,14 @@ import ( // Refer: Arnaud Legoux Moving Average // Refer: https://capital.com/arnaud-legoux-moving-average // Also check https://github.com/DaveSkender/Stock.Indicators/blob/main/src/a-d/Alma/Alma.cs +// +// The Arnaud Legoux Moving Average (ALMA) is a technical analysis indicator that is used to smooth price data and reduce the lag associated +// with traditional moving averages. It was developed by Arnaud Legoux and is based on the weighted moving average, with the weighting factors +// determined using a Gaussian function. The ALMA is calculated by taking the weighted moving average of the input data using weighting factors +// that are based on the standard deviation of the data and the specified length of the moving average. This resulting average is then plotted +// on the price chart as a line, which can be used to make predictions about future price movements. The ALMA is typically more responsive to +// changes in the underlying data than a simple moving average, but may be less reliable in trending markets. +// // @param offset: Gaussian applied to the combo line. 1->ema, 0->sma // @param sigma: the standard deviation applied to the combo line. This makes the combo line sharper //go:generate callbackgen -type ALMA @@ -20,9 +28,9 @@ type ALMA struct { Sigma int // required: recommend to be 5 weight []float64 sum float64 - input []float64 - Values floats.Slice - UpdateCallbacks []func(value float64) + input []float64 + Values floats.Slice + UpdateCallbacks []func(value float64) } const MaxNumOfALMA = 5_000 diff --git a/pkg/indicator/atrp.go b/pkg/indicator/atrp.go index 03c4f2071e..8d473942ca 100644 --- a/pkg/indicator/atrp.go +++ b/pkg/indicator/atrp.go @@ -11,6 +11,14 @@ import ( // ATRP is the average true range percentage // See also https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/atrp // +// The Average True Range Percentage (ATRP) is a technical analysis indicator that measures the volatility of a security's price. It is +// calculated by dividing the Average True Range (ATR) of the security by its closing price, and then multiplying the result by 100 to convert +// it to a percentage. The ATR is a measure of the range of a security's price, taking into account gaps between trading periods and any limit +// moves (sharp price movements that are allowed under certain exchange rules). The ATR is typically smoothed using a moving average to make it +// more responsive to changes in the underlying price data. The ATRP is a useful indicator for traders because it provides a way to compare the +// volatility of different securities, regardless of their individual prices. It can also be used to identify potential entry and exit points +// for trades based on changes in the security's volatility. +// // Calculation: // // ATRP = (Average True Range / Close) * 100 diff --git a/pkg/indicator/cci.go b/pkg/indicator/cci.go index ee3190d0a3..64d4427b25 100644 --- a/pkg/indicator/cci.go +++ b/pkg/indicator/cci.go @@ -10,6 +10,15 @@ import ( // Refer: Commodity Channel Index // Refer URL: http://www.andrewshamlet.net/2017/07/08/python-tutorial-cci // with modification of ddof=0 to let standard deviation to be divided by N instead of N-1 +// +// The Commodity Channel Index (CCI) is a technical analysis indicator that is used to identify potential overbought or oversold conditions +// in a security's price. It was originally developed for use in commodity markets, but can be applied to any security that has a sufficient +// amount of price data. The CCI is calculated by taking the difference between the security's typical price (the average of its high, low, and +// closing prices) and its moving average, and then dividing the result by the mean absolute deviation of the typical price. This resulting value +// is then plotted as a line on the price chart, with values above +100 indicating overbought conditions and values below -100 indicating +// oversold conditions. The CCI can be used by traders to identify potential entry and exit points for trades, or to confirm other technical +// analysis signals. + //go:generate callbackgen -type CCI type CCI struct { types.SeriesBase diff --git a/pkg/indicator/dema.go b/pkg/indicator/dema.go index 601cb8380b..107923de52 100644 --- a/pkg/indicator/dema.go +++ b/pkg/indicator/dema.go @@ -7,6 +7,13 @@ import ( // Refer: Double Exponential Moving Average // Refer URL: https://investopedia.com/terms/d/double-exponential-moving-average.asp +// +// The Double Exponential Moving Average (DEMA) is a technical analysis indicator that is used to smooth price data and reduce the lag +// associated with traditional moving averages. It is calculated by taking the exponentially weighted moving average of the input data, +// and then taking the exponentially weighted moving average of that result. This double-smoothing process helps to eliminate much of the noise +// in the original data and provides a more accurate representation of the underlying trend. The DEMA line is then plotted on the price chart, +// which can be used to make predictions about future price movements. The DEMA is typically more responsive to changes in the underlying data +// than a simple moving average, but may be less reliable in trending markets. //go:generate callbackgen -type DEMA type DEMA struct { diff --git a/pkg/indicator/dmi.go b/pkg/indicator/dmi.go index d3352bfc55..853b9deb36 100644 --- a/pkg/indicator/dmi.go +++ b/pkg/indicator/dmi.go @@ -10,8 +10,15 @@ import ( // Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/trend/adx.py // // Directional Movement Index -// an indicator developed by J. Welles Wilder in 1978 that identifies in which -// direction the price of an asset is moving. +// +// The Directional Movement Index (DMI) is a technical analysis indicator that is used to identify the direction and strength of a trend +// in a security's price. It was developed by J. Welles Wilder and is based on the concept of the +DI and -DI lines, which measure the strength +// of upward and downward price movements, respectively. The DMI is calculated by taking the difference between the +DI and -DI lines, and then +// smoothing the result using a moving average. This resulting line is called the Average Directional Index (ADX), and is used to identify whether +// a security is trending or not. If the ADX is above a certain threshold, typically 20, it indicates that the security is in a strong trend, +// and if it is below that threshold it indicates that the security is in a sideways or choppy market. The DMI can be used by traders to confirm +// the direction and strength of a trend, or to identify potential entry and exit points for trades. + //go:generate callbackgen -type DMI type DMI struct { types.IntervalWindow diff --git a/pkg/indicator/drift.go b/pkg/indicator/drift.go index 12006191fc..ac3df837aa 100644 --- a/pkg/indicator/drift.go +++ b/pkg/indicator/drift.go @@ -10,6 +10,14 @@ import ( // Refer: https://tradingview.com/script/aDymGrFx-Drift-Study-Inspired-by-Monte-Carlo-Simulations-with-BM-KL/ // Brownian Motion's drift factor // could be used in Monte Carlo Simulations +// +// In the context of Brownian motion, drift can be measured by calculating the simple moving average (SMA) of the logarithm +// of the price changes of a security over a specified period of time. This SMA can be used to identify the long-term trend +// or bias in the random movement of the security's price. A security with a positive drift is said to be trending upwards, +// while a security with a negative drift is said to be trending downwards. Drift can be used by traders to identify potential +// entry and exit points for trades, or to confirm other technical analysis signals. +// It is typically used in conjunction with other indicators to provide a more comprehensive view of the security's price. + //go:generate callbackgen -type Drift type Drift struct { types.SeriesBase diff --git a/pkg/indicator/emv.go b/pkg/indicator/emv.go index cded064a88..a3a761a5a1 100644 --- a/pkg/indicator/emv.go +++ b/pkg/indicator/emv.go @@ -6,6 +6,11 @@ import ( // Refer: Ease of Movement // Refer URL: https://www.investopedia.com/terms/e/easeofmovement.asp +// The Ease of Movement (EOM) is a technical analysis indicator that is used to measure the relationship between the volume of a security +// and its price movement. It is calculated by dividing the difference between the high and low prices of the security by the total +// volume of the security over a specified period of time, and then multiplying the result by a constant factor. This resulting value is +// then plotted on the price chart as a line, which can be used to make predictions about future price movements. The EOM is typically +// used to identify periods of high or low trading activity, and can be used to confirm other technical analysis signals. //go:generate callbackgen -type EMV type EMV struct { diff --git a/pkg/indicator/fisher.go b/pkg/indicator/fisher.go index 98678afd82..fcd492c955 100644 --- a/pkg/indicator/fisher.go +++ b/pkg/indicator/fisher.go @@ -7,6 +7,15 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +// Fisher Transform +// +// The Fisher Transform is a technical analysis indicator that is used to identify potential turning points in the price of a security. +// It is based on the idea that prices tend to be normally distributed, with most price movements being small and relatively insignificant. +// The Fisher Transform converts this normal distribution into a symmetrical, Gaussian distribution, with a peak at zero and a range of -1 to +1. +// This transformation allows for more accurate identification of price extremes, which can be used to make predictions about potential trend reversals. +// The Fisher Transform is calculated by taking the natural logarithm of the ratio of the security's current price to its moving average, +// and then double-smoothing the result. This resulting line is called the Fisher Transform line, and can be plotted on the price chart +// along with the security's price. //go:generate callbackgen -type FisherTransform type FisherTransform struct { types.SeriesBase diff --git a/pkg/indicator/gma.go b/pkg/indicator/gma.go index c1fb3aa117..8b86733002 100644 --- a/pkg/indicator/gma.go +++ b/pkg/indicator/gma.go @@ -7,6 +7,13 @@ import ( ) // Geometric Moving Average +// +// The Geometric Moving Average (GMA) is a technical analysis indicator that uses the geometric mean of a set of data points instead of +// the simple arithmetic mean used by traditional moving averages. It is calculated by taking the nth root of the product of the last n +// data points, where n is the length of the moving average. The resulting average is then plotted on the price chart as a line, which can +// be used to make predictions about future price movements. Because the GMA gives more weight to recent data points, it is typically more +// responsive to changes in the underlying data than a simple moving average. + //go:generate callbackgen -type GMA type GMA struct { types.SeriesBase diff --git a/pkg/indicator/hull.go b/pkg/indicator/hull.go index df3e8c5690..214d2620d3 100644 --- a/pkg/indicator/hull.go +++ b/pkg/indicator/hull.go @@ -8,6 +8,12 @@ import ( // Refer: Hull Moving Average // Refer URL: https://fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/hull-moving-average +// +// The Hull Moving Average (HMA) is a technical analysis indicator that uses a weighted moving average to reduce the lag in simple moving averages. +// It was developed by Alan Hull, who sought to create a moving average that was both fast and smooth. The HMA is calculated by first taking +// the weighted moving average of the input data using a weighting factor of W, where W is the square root of the length of the moving average. +// The result is then double-smoothed by taking the weighted moving average of this result using a weighting factor of W/2. This final average +// forms the HMA line, which can be used to make predictions about future price movements. //go:generate callbackgen -type HULL type HULL struct { types.SeriesBase diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index e8146bd173..93ed7c0cc8 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -7,13 +7,17 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -/* -macd implements moving average convergence divergence indicator +// macd implements moving average convergence divergence indicator +// +// Moving Average Convergence Divergence (MACD) +// - https://www.investopedia.com/terms/m/macd.asp +// - https://school.stockcharts.com/doku.php?id=technical_indicators:macd-histogram +// The Moving Average Convergence Divergence (MACD) is a technical analysis indicator that is used to measure the relationship between +// two moving averages of a security's price. It is calculated by subtracting the longer-term moving average from the shorter-term moving +// average, and then plotting the resulting value on the price chart as a line. This line is known as the MACD line, and is typically +// used to identify potential buy or sell signals. The MACD is typically used in conjunction with a signal line, which is a moving average +// of the MACD line, to generate more accurate buy and sell signals. -Moving Average Convergence Divergence (MACD) -- https://www.investopedia.com/terms/m/macd.asp -- https://school.stockcharts.com/doku.php?id=technical_indicators:macd-histogram -*/ type MACDConfig struct { types.IntervalWindow // 9 diff --git a/pkg/indicator/obv.go b/pkg/indicator/obv.go index 4d13fbfa21..4ca032445d 100644 --- a/pkg/indicator/obv.go +++ b/pkg/indicator/obv.go @@ -12,6 +12,13 @@ obv implements on-balance volume indicator On-Balance Volume (OBV) Definition - https://www.investopedia.com/terms/o/onbalancevolume.asp + +On-Balance Volume (OBV) is a technical analysis indicator that uses volume information to predict changes in stock price. +The idea behind OBV is that volume precedes price: when the OBV is rising, it means that buyers are becoming more aggressive and +that the stock price is likely to follow suit. When the OBV is falling, it indicates that sellers are becoming more aggressive and +that the stock price is likely to decrease. OBV is calculated by adding the volume on days when the stock price closes higher and +subtracting the volume on days when the stock price closes lower. This running total forms the OBV line, which can then be used +to make predictions about future stock price movements. */ //go:generate callbackgen -type OBV type OBV struct { diff --git a/pkg/indicator/rma.go b/pkg/indicator/rma.go index 63d7514ed8..6785a54ec4 100644 --- a/pkg/indicator/rma.go +++ b/pkg/indicator/rma.go @@ -13,6 +13,14 @@ const MaxNumOfRMATruncateSize = 500 // Running Moving Average // Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/overlap/rma.py#L5 // Refer: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html#pandas-dataframe-ewm +// +// The Running Moving Average (RMA) is a technical analysis indicator that is used to smooth price data and reduce the lag associated +// with traditional moving averages. It is calculated by taking the weighted moving average of the input data, with the weighting factors +// determined by the length of the moving average. The resulting average is then plotted on the price chart as a line, which can be used to +// make predictions about future price movements. The RMA is typically more responsive to changes in the underlying data than a simple +// moving average, but may be less reliable in trending markets. It is often used in conjunction with other technical analysis indicators +// to confirm signals or provide additional information about the security's price. + //go:generate callbackgen -type RMA type RMA struct { types.SeriesBase diff --git a/pkg/indicator/rsi.go b/pkg/indicator/rsi.go index 92060f455a..8689abbdf6 100644 --- a/pkg/indicator/rsi.go +++ b/pkg/indicator/rsi.go @@ -8,11 +8,16 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -/* -rsi implements Relative Strength Index (RSI) +// rsi implements Relative Strength Index (RSI) +// https://www.investopedia.com/terms/r/rsi.asp +// +// The Relative Strength Index (RSI) is a technical analysis indicator that is used to measure the strength of a security's price. It is +// calculated by taking the average of the gains and losses of the security over a specified period of time, and then dividing the average gain +// by the average loss. This resulting value is then plotted as a line on the price chart, with values above 70 indicating overbought conditions +// and values below 30 indicating oversold conditions. The RSI can be used by traders to identify potential entry and exit points for trades, +// or to confirm other technical analysis signals. It is typically used in conjunction with other indicators to provide a more comprehensive +// view of the security's price. -https://www.investopedia.com/terms/r/rsi.asp -*/ //go:generate callbackgen -type RSI type RSI struct { types.SeriesBase diff --git a/pkg/indicator/stoch.go b/pkg/indicator/stoch.go index aa86fea8b6..538dd951c7 100644 --- a/pkg/indicator/stoch.go +++ b/pkg/indicator/stoch.go @@ -9,12 +9,16 @@ import ( const DPeriod int = 3 -/* -stoch implements stochastic oscillator indicator +// Stochastic Oscillator +// - https://www.investopedia.com/terms/s/stochasticoscillator.asp +// +// The Stochastic Oscillator is a technical analysis indicator that is used to identify potential overbought or oversold conditions +// in a security's price. It is calculated by taking the current closing price of the security and comparing it to the high and low prices +// over a specified period of time. This comparison is then plotted as a line on the price chart, with values above 80 indicating overbought +// conditions and values below 20 indicating oversold conditions. The Stochastic Oscillator can be used by traders to identify potential +// entry and exit points for trades, or to confirm other technical analysis signals. It is typically used in conjunction with other indicators +// to provide a more comprehensive view of the security's price. -Stochastic Oscillator -- https://www.investopedia.com/terms/s/stochasticoscillator.asp -*/ //go:generate callbackgen -type STOCH type STOCH struct { types.IntervalWindow diff --git a/pkg/indicator/supertrend.go b/pkg/indicator/supertrend.go index 6d15e19a2c..44501dd4bf 100644 --- a/pkg/indicator/supertrend.go +++ b/pkg/indicator/supertrend.go @@ -12,6 +12,17 @@ import ( var logst = logrus.WithField("indicator", "supertrend") +// The Super Trend is a technical analysis indicator that is used to identify potential buy and sell signals in a security's price. It is +// calculated by combining the exponential moving average (EMA) and the average true range (ATR) of the security's price, and then plotting +// the resulting value on the price chart as a line. The Super Trend line is typically used to identify potential entry and exit points +// for trades, and can be used to confirm other technical analysis signals. It is typically more responsive to changes in the underlying +// data than other trend-following indicators, but may be less reliable in trending markets. It is important to note that the Super Trend is a +// lagging indicator, which means that it may not always provide accurate or timely signals. +// +// To use Super Trend, identify potential entry and exit points for trades by looking for crossovers or divergences between the Super Trend line +// and the security's price. For example, a buy signal may be generated when the Super Trend line crosses above the security's price, while a sell +// signal may be generated when the Super Trend line crosses below the security's price. + //go:generate callbackgen -type Supertrend type Supertrend struct { types.SeriesBase diff --git a/pkg/indicator/tema.go b/pkg/indicator/tema.go index 3481775249..a3a7024092 100644 --- a/pkg/indicator/tema.go +++ b/pkg/indicator/tema.go @@ -7,6 +7,14 @@ import ( // Refer: Triple Exponential Moving Average (TEMA) // URL: https://investopedia.com/terms/t/triple-exponential-moving-average.asp +// +// The Triple Exponential Moving Average (TEMA) is a technical analysis indicator that is used to smooth price data and reduce the lag +// associated with traditional moving averages. It is calculated by taking the exponentially weighted moving average of the input data, +// and then taking the exponentially weighted moving average of that result, and then taking the exponentially weighted moving average of +// that result. This triple-smoothing process helps to eliminate much of the noise in the original data and provides a more accurate +// representation of the underlying trend. The TEMA line is then plotted on the price chart, which can be used to make predictions about +// future price movements. The TEMA is typically more responsive to changes in the underlying data than a simple moving average, but may be +// less reliable in trending markets. //go:generate callbackgen -type TEMA type TEMA struct { diff --git a/pkg/indicator/till.go b/pkg/indicator/till.go index 6c1860b2e3..c7fca34dcb 100644 --- a/pkg/indicator/till.go +++ b/pkg/indicator/till.go @@ -8,6 +8,15 @@ const defaultVolumeFactor = 0.7 // Refer: Tillson T3 Moving Average // Refer URL: https://tradingpedia.com/forex-trading-indicator/t3-moving-average-indicator/ +// +// The Tillson T3 Moving Average (T3) is a technical analysis indicator that is used to smooth price data and reduce the lag associated +// with traditional moving averages. It was developed by Tim Tillson and is based on the exponential moving average, with the weighting +// factors determined using a modified version of the cubic polynomial. The T3 is calculated by taking the weighted moving average of the +// input data using weighting factors that are based on the standard deviation of the data and the specified length of the moving average. +// This resulting average is then plotted on the price chart as a line, which can be used to make predictions about future price movements. +// The T3 is typically more responsive to changes in the underlying data than a simple moving average, but may be less reliable in trending +// markets. + //go:generate callbackgen -type TILL type TILL struct { types.SeriesBase diff --git a/pkg/indicator/vidya.go b/pkg/indicator/vidya.go index 023e39a666..311e47f4fb 100644 --- a/pkg/indicator/vidya.go +++ b/pkg/indicator/vidya.go @@ -9,6 +9,13 @@ import ( // Refer: Variable Index Dynamic Average // Refer URL: https://metatrader5.com/en/terminal/help/indicators/trend_indicators/vida +// The Variable Index Dynamic Average (VIDYA) is a technical analysis indicator that is used to smooth price data and reduce the lag +// associated with traditional moving averages. It is calculated by taking the weighted moving average of the input data, with the +// weighting factors determined using a variable index that is based on the standard deviation of the data and the specified length of +// the moving average. This resulting average is then plotted on the price chart as a line, which can be used to make predictions about +// future price movements. The VIDYA is typically more responsive to changes in the underlying data than a simple moving average, but may +// be less reliable in trending markets. + //go:generate callbackgen -type VIDYA type VIDYA struct { types.SeriesBase diff --git a/pkg/indicator/volumeprofile.go b/pkg/indicator/volumeprofile.go new file mode 100644 index 0000000000..4d487f39fa --- /dev/null +++ b/pkg/indicator/volumeprofile.go @@ -0,0 +1,102 @@ +package indicator + +import ( + "math" + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +type trade struct { + price float64 + volume float64 // +: buy, -: sell + timestamp types.Time +} + +// The Volume Profile is a technical analysis tool that is used to visualize the distribution of trading volume at different price levels +// in a security. It is typically plotted as a histogram or heatmap on the price chart, with the x-axis representing the price levels and +// the y-axis representing the trading volume. The Volume Profile can be used to identify areas of support and resistance, as well as +// potential entry and exit points for trades. +type VolumeProfile struct { + types.IntervalWindow + Delta float64 + profile map[float64]float64 + trades []trade + minPrice float64 + maxPrice float64 +} + +func (inc *VolumeProfile) Update(price, volume float64, timestamp types.Time) { + if inc.minPrice == 0 { + inc.minPrice = math.Inf(1) + } + if inc.maxPrice == 0 { + inc.maxPrice = math.Inf(-1) + } + inc.profile[math.Round(price/inc.Delta)] += volume + filter := timestamp.Time().Add(-time.Duration(inc.Window) * inc.Interval.Duration()) + var i int + for i = 0; i < len(inc.trades); i++ { + td := inc.trades[i] + if td.timestamp.After(filter) { + break + } + inc.profile[math.Round(td.price/inc.Delta)] -= td.volume + } + inc.trades = inc.trades[i : len(inc.trades)-1] + inc.trades = append(inc.trades, trade{ + price: price, + volume: volume, + timestamp: timestamp, + }) + for k, _ := range inc.profile { + if k < inc.minPrice { + inc.minPrice = k + } + if k > inc.maxPrice { + inc.maxPrice = k + } + } +} + +// The Point of Control (POC) is a term used in the context of Volume Profile analysis. It refers to the price level at which the most +// volume has been traded in a security over a specified period of time. The POC is typically identified by looking for the highest +// peak in the Volume Profile, and is considered to be an important level of support or resistance. It can be used by traders to +// identify potential entry and exit points for trades, or to confirm other technical analysis signals. + +// Get Resistence Level by finding PoC +func (inc *VolumeProfile) PointOfControlAboveEqual(price float64, limit ...float64) (resultPrice float64, vol float64) { + filter := inc.maxPrice + if len(limit) > 0 { + filter = limit[0] + } + start := math.Round(price / inc.Delta) + vol = math.Inf(-1) + for ; start <= filter; start += inc.Delta { + abs := math.Abs(inc.profile[start]) + if vol < abs { + vol = abs + resultPrice = start + } + + } + return resultPrice, vol +} + +// Get Support Level by finding PoC +func (inc *VolumeProfile) PointOfControlBelowEqual(price float64, limit ...float64) (resultPrice float64, vol float64) { + filter := inc.minPrice + if len(limit) > 0 { + filter = limit[0] + } + start := math.Round(price / inc.Delta) + vol = math.Inf(-1) + for ; start >= filter; start -= inc.Delta { + abs := math.Abs(inc.profile[start]) + if vol < abs { + vol = abs + resultPrice = start + } + } + return resultPrice, vol +} diff --git a/pkg/indicator/vwap.go b/pkg/indicator/vwap.go index 5cb36c847f..805def8285 100644 --- a/pkg/indicator/vwap.go +++ b/pkg/indicator/vwap.go @@ -7,15 +7,21 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -/* -vwap implements the volume weighted average price (VWAP) indicator: +// vwap implements the volume weighted average price (VWAP) indicator: +// +// Volume Weighted Average Price (VWAP) Definition +// - https://www.investopedia.com/terms/v/vwap.asp +// +// Volume-Weighted Average Price (VWAP) Explained +// - https://academy.binance.com/en/articles/volume-weighted-average-price-vwap-explained +// +// The Volume Weighted Average Price (VWAP) is a technical analysis indicator that is used to measure the average price of a security +// over a specified period of time, with the weighting factors determined by the volume of the security. It is calculated by taking the +// sum of the product of the price and volume for each trade, and then dividing that sum by the total volume of the security over the +// specified period of time. This resulting average is then plotted on the price chart as a line, which can be used to make predictions +// about future price movements. The VWAP is typically more accurate than other simple moving averages, as it takes into account the +// volume of the security, but may be less reliable in markets with low trading volume. -Volume Weighted Average Price (VWAP) Definition -- https://www.investopedia.com/terms/v/vwap.asp - -Volume-Weighted Average Price (VWAP) Explained -- https://academy.binance.com/en/articles/volume-weighted-average-price-vwap-explained -*/ //go:generate callbackgen -type VWAP type VWAP struct { types.SeriesBase diff --git a/pkg/indicator/vwma.go b/pkg/indicator/vwma.go index 9998bbc49b..a8a328e20f 100644 --- a/pkg/indicator/vwma.go +++ b/pkg/indicator/vwma.go @@ -7,16 +7,21 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -/* -vwma implements the volume weighted moving average (VWMA) indicator: +// vwma implements the volume weighted moving average (VWMA) indicator: +// +// Calculation: +// pv = element-wise multiplication of close prices and volumes +// VWMA = SMA(pv, window) / SMA(volumes, window) +// +// Volume Weighted Moving Average +//- https://www.motivewave.com/studies/volume_weighted_moving_average.htm +// +// The Volume Weighted Moving Average (VWMA) is a technical analysis indicator that is used to smooth price data and reduce the lag +// associated with traditional moving averages. It is calculated by taking the weighted moving average of the input data, with the +// weighting factors determined by the volume of the security. This resulting average is then plotted on the price chart as a line, +// which can be used to make predictions about future price movements. The VWMA is typically more accurate than other simple moving +// averages, as it takes into account the volume of the security, but may be less reliable in markets with low trading volume. -Calculation: - pv = element-wise multiplication of close prices and volumes - VWMA = SMA(pv, window) / SMA(volumes, window) - -Volume Weighted Moving Average -- https://www.motivewave.com/studies/volume_weighted_moving_average.htm -*/ //go:generate callbackgen -type VWMA type VWMA struct { types.SeriesBase @@ -79,7 +84,6 @@ func (inc *VWMA) PushK(k types.KLine) { inc.Update(k.Close.Float64(), k.Volume.Float64()) } - func (inc *VWMA) CalculateAndUpdate(allKLines []types.KLine) { if len(allKLines) < inc.Window { return diff --git a/pkg/indicator/zlema.go b/pkg/indicator/zlema.go index 3f6f4b08fd..5c7279f619 100644 --- a/pkg/indicator/zlema.go +++ b/pkg/indicator/zlema.go @@ -7,6 +7,12 @@ import ( // Refer: Zero Lag Exponential Moving Average // Refer URL: https://en.wikipedia.org/wiki/Zero_lag_exponential_moving_average +// +// The Zero Lag Exponential Moving Average (ZLEMA) is a technical analysis indicator that is used to smooth price data and reduce the +// lag associated with traditional moving averages. It is calculated by taking the exponentially weighted moving average of the input +// data, and then applying a digital filter to the resulting average to eliminate any remaining lag. This filtered average is then +// plotted on the price chart as a line, which can be used to make predictions about future price movements. The ZLEMA is typically more +// responsive to changes in the underlying data than a simple moving average, but may be less reliable in trending markets. //go:generate callbackgen -type ZLEMA type ZLEMA struct { From c0f82977b0111ca31a9f232c144d308778b264dc Mon Sep 17 00:00:00 2001 From: zenix Date: Fri, 16 Dec 2022 16:39:49 +0900 Subject: [PATCH 0305/1392] feature: add psar --- pkg/indicator/psar.go | 109 ++++++++++++++++++++++++++++++++ pkg/indicator/psar_callbacks.go | 15 +++++ 2 files changed, 124 insertions(+) create mode 100644 pkg/indicator/psar.go create mode 100644 pkg/indicator/psar_callbacks.go diff --git a/pkg/indicator/psar.go b/pkg/indicator/psar.go new file mode 100644 index 0000000000..740f7ad8d1 --- /dev/null +++ b/pkg/indicator/psar.go @@ -0,0 +1,109 @@ +package indicator + +import ( + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +// Parabolic SAR(Stop and Reverse) +// Refer: https://www.investopedia.com/terms/p/parabolicindicator.asp +// The parabolic SAR indicator, developed by J. Wells Wilder, is used by traders to determine +// trend direction and potential reversals in price. The indicator uses a trailing stop and +// reverse method called "SAR," or stop and reverse, to identify suitable exit and entry points. +// Traders also refer to the indicator as to the parabolic stop and reverse, parabolic SAR, or PSAR. +// +// The parabolic SAR indicator appears on a chart as a series of dots, either above or below an asset's +// price, depending on the direction the price is moving. A dot is placed below the price when it is +// trending upward, and above the price when it is trending downward. + +//go:generate callbackgen -type PSAR +type PSAR struct { + types.SeriesBase + types.IntervalWindow + Input *types.Queue + Value *types.Queue // Stop and Reverse + AF float64 // Acceleration Factor + + EndTime time.Time + UpdateCallbacks []func(value float64) +} + +func (inc *PSAR) Last() float64 { + if inc.Value == nil { + return 0 + } + return inc.Value.Last() +} + +func (inc *PSAR) Length() int { + if inc.Value == nil { + return 0 + } + return inc.Value.Length() +} + +func (inc *PSAR) Update(value float64) { + if inc.Input == nil { + inc.SeriesBase.Series = inc + inc.Input = types.NewQueue(inc.Window) + inc.Value = types.NewQueue(inc.Window) + inc.AF = 0.02 + } + inc.Input.Update(value) + if inc.Input.Length() == inc.Window { + pprev := inc.Value.Index(1) + ppsar := inc.Value.Last() + if value > ppsar { // rising formula + high := inc.Input.Highest(inc.Window) + inc.Value.Update(ppsar + inc.AF*(high-ppsar)) + if high == value { + inc.AF += 0.02 + if inc.AF > 0.2 { + inc.AF = 0.2 + } + } + if pprev > ppsar { // reverse + inc.AF = 0.02 + } + } else { // falling formula + low := inc.Input.Lowest(inc.Window) + inc.Value.Update(ppsar - inc.AF*(ppsar-low)) + if low == value { + inc.AF += 0.02 + if inc.AF > 0.2 { + inc.AF = 0.2 + } + } + if pprev < ppsar { // reverse + inc.AF = 0.02 + } + } + } +} + +var _ types.SeriesExtend = &PSAR{} + +func (inc *PSAR) CalculateAndUpdate(kLines []types.KLine) { + for _, k := range kLines { + if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { + continue + } + inc.Update(k.Close.Float64()) + } + + inc.EmitUpdate(inc.Last()) + inc.EndTime = kLines[len(kLines)-1].EndTime.Time() +} + +func (inc *PSAR) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *PSAR) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/psar_callbacks.go b/pkg/indicator/psar_callbacks.go new file mode 100644 index 0000000000..37d48377ec --- /dev/null +++ b/pkg/indicator/psar_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type PSAR"; DO NOT EDIT. + +package indicator + +import () + +func (inc *PSAR) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *PSAR) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} From 75caa6565eee3c52bcce9ac0044b0b4bb1174a88 Mon Sep 17 00:00:00 2001 From: zenix Date: Mon, 19 Dec 2022 18:11:08 +0900 Subject: [PATCH 0306/1392] feature: add sar indicator --- pkg/indicator/psar.go | 101 ++++++++++++++++++++++++------------- pkg/indicator/psar_test.go | 41 +++++++++++++++ 2 files changed, 107 insertions(+), 35 deletions(-) create mode 100644 pkg/indicator/psar_test.go diff --git a/pkg/indicator/psar.go b/pkg/indicator/psar.go index 740f7ad8d1..95e4822c9d 100644 --- a/pkg/indicator/psar.go +++ b/pkg/indicator/psar.go @@ -1,12 +1,14 @@ package indicator import ( + "math" "time" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) -// Parabolic SAR(Stop and Reverse) +// Parabolic SAR(Stop and Reverse) / SAR // Refer: https://www.investopedia.com/terms/p/parabolicindicator.asp // The parabolic SAR indicator, developed by J. Wells Wilder, is used by traders to determine // trend direction and potential reversals in price. The indicator uses a trailing stop and @@ -21,63 +23,92 @@ import ( type PSAR struct { types.SeriesBase types.IntervalWindow - Input *types.Queue - Value *types.Queue // Stop and Reverse - AF float64 // Acceleration Factor + High *types.Queue + Low *types.Queue + Values floats.Slice // Stop and Reverse + AF float64 // Acceleration Factor + EP float64 + Falling bool EndTime time.Time UpdateCallbacks []func(value float64) } func (inc *PSAR) Last() float64 { - if inc.Value == nil { + if len(inc.Values) == 0 { return 0 } - return inc.Value.Last() + return inc.Values.Last() } func (inc *PSAR) Length() int { - if inc.Value == nil { - return 0 - } - return inc.Value.Length() + return len(inc.Values) +} + +func (inc *PSAR) falling() bool { + up := inc.High.Last() - inc.High.Index(1) + dn := inc.Low.Index(1) - inc.Low.Last() + return (dn > up) && (dn > 0) } -func (inc *PSAR) Update(value float64) { - if inc.Input == nil { +func (inc *PSAR) Update(high, low float64) { + if inc.High == nil { inc.SeriesBase.Series = inc - inc.Input = types.NewQueue(inc.Window) - inc.Value = types.NewQueue(inc.Window) + inc.High = types.NewQueue(inc.Window) + inc.Low = types.NewQueue(inc.Window) + inc.Values = floats.Slice{} inc.AF = 0.02 + inc.High.Update(high) + inc.Low.Update(low) + return } - inc.Input.Update(value) - if inc.Input.Length() == inc.Window { - pprev := inc.Value.Index(1) - ppsar := inc.Value.Last() - if value > ppsar { // rising formula - high := inc.Input.Highest(inc.Window) - inc.Value.Update(ppsar + inc.AF*(high-ppsar)) - if high == value { - inc.AF += 0.02 - if inc.AF > 0.2 { - inc.AF = 0.2 + isFirst := inc.High.Length() < inc.Window + inc.High.Update(high) + inc.Low.Update(low) + if !isFirst { + ppsar := inc.Values.Last() + if inc.Falling { // falling formula + psar := ppsar - inc.AF*(ppsar-inc.EP) + h := inc.High.Shift(1).Highest(2) + inc.Values.Push(math.Max(psar, h)) + if low < inc.EP { + inc.EP = low + if inc.AF <= 0.18 { + inc.AF += 0.02 } } - if pprev > ppsar { // reverse + if high > psar { // reverse inc.AF = 0.02 + inc.Values[len(inc.Values)-1] = inc.EP + inc.EP = high + inc.Falling = false } - } else { // falling formula - low := inc.Input.Lowest(inc.Window) - inc.Value.Update(ppsar - inc.AF*(ppsar-low)) - if low == value { - inc.AF += 0.02 - if inc.AF > 0.2 { - inc.AF = 0.2 + } else { // rising formula + psar := ppsar + inc.AF*(inc.EP-ppsar) + l := inc.Low.Shift(1).Lowest(2) + inc.Values.Push(math.Min(psar, l)) + if high > inc.EP { + inc.EP = high + if inc.AF <= 0.18 { + inc.AF += 0.02 } } - if pprev < ppsar { // reverse + if low < psar { // reverse inc.AF = 0.02 + inc.Values[len(inc.Values)-1] = inc.EP + inc.EP = low + inc.Falling = true } + + } + } else { + inc.Falling = inc.falling() + if inc.Falling { + inc.Values.Push(inc.High.Index(1)) + inc.EP = inc.Low.Index(1) + } else { + inc.Values.Push(inc.Low.Index(1)) + inc.EP = inc.High.Index(1) } } } @@ -89,7 +120,7 @@ func (inc *PSAR) CalculateAndUpdate(kLines []types.KLine) { if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { continue } - inc.Update(k.Close.Float64()) + inc.Update(k.High.Float64(), k.Low.Float64()) } inc.EmitUpdate(inc.Last()) diff --git a/pkg/indicator/psar_test.go b/pkg/indicator/psar_test.go new file mode 100644 index 0000000000..eae9d513fb --- /dev/null +++ b/pkg/indicator/psar_test.go @@ -0,0 +1,41 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +/* +import pandas as pd +import pandas_ta as ta + +data = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]) +out = ta.psar(data, data) +print(out) +*/ + +func Test_PSAR(t *testing.T) { + var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []fixedpoint.Value + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + psar := PSAR{ + IntervalWindow: types.IntervalWindow{Window: 2}, + } + var klines []types.KLine + for _, v := range input { + klines = append(klines, types.KLine{ + High: v, + Low: v, + }) + } + psar.CalculateAndUpdate(klines) + assert.Equal(t, psar.Length(), 29) + assert.Equal(t, psar.AF, 0.04) + assert.Equal(t, psar.Last(), 0.16) +} From 2811dbb580084920cf75a4ee94e9918143adf2a4 Mon Sep 17 00:00:00 2001 From: zenix Date: Mon, 19 Dec 2022 19:27:07 +0900 Subject: [PATCH 0307/1392] fix: tsi, add test --- pkg/indicator/tsi.go | 30 +++++++++++++++++++++--------- pkg/indicator/tsi_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/pkg/indicator/tsi.go b/pkg/indicator/tsi.go index dd62945c16..57f4e5456d 100644 --- a/pkg/indicator/tsi.go +++ b/pkg/indicator/tsi.go @@ -12,7 +12,9 @@ import ( //go:generate callbackgen -type TSI type TSI struct { types.SeriesBase - types.IntervalWindow + types.Interval + FastWindow int + SlowWindow int PrevValue float64 Values floats.Slice Pcs *EWMA @@ -24,48 +26,58 @@ type TSI struct { func (inc *TSI) Update(value float64) { if inc.Pcs == nil { + if inc.FastWindow == 0 { + inc.FastWindow = 13 + } + if inc.SlowWindow == 0 { + inc.SlowWindow = 25 + } inc.Pcs = &EWMA{ IntervalWindow: types.IntervalWindow{ - Window: 25, + Window: inc.SlowWindow, Interval: inc.Interval, }, } inc.Pcds = &EWMA{ IntervalWindow: types.IntervalWindow{ - Window: 13, + Window: inc.FastWindow, Interval: inc.Interval, }, } inc.Apcs = &EWMA{ IntervalWindow: types.IntervalWindow{ - Window: 25, + Window: inc.SlowWindow, Interval: inc.Interval, }, } inc.Apcds = &EWMA{ IntervalWindow: types.IntervalWindow{ - Window: 13, + Window: inc.FastWindow, Interval: inc.Interval, }, } inc.SeriesBase.Series = inc - } - if inc.PrevValue == 0 { inc.PrevValue = value return } pc := value - inc.PrevValue + inc.PrevValue = value inc.Pcs.Update(pc) - inc.Pcds.Update(inc.Pcs.Last()) apc := math.Abs(pc) inc.Apcs.Update(apc) + + inc.Pcds.Update(inc.Pcs.Last()) inc.Apcds.Update(inc.Apcs.Last()) + tsi := (inc.Pcds.Last() / inc.Apcds.Last()) * 100. inc.Values.Push(tsi) if inc.Values.Length() > MaxNumOfEWMA { inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] } - inc.PrevValue = value +} + +func (inc *TSI) Length() int { + return inc.Values.Length() } func (inc *TSI) Last() float64 { diff --git a/pkg/indicator/tsi_test.go b/pkg/indicator/tsi_test.go index 722d45a367..8ab639e567 100644 --- a/pkg/indicator/tsi_test.go +++ b/pkg/indicator/tsi_test.go @@ -1 +1,34 @@ package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/stretchr/testify/assert" +) + +/* +import pandas as pd + +data = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]) +ma1 = data.diff(1).ewm(span=25, adjust=False).mean() +ma2 = ma1.ewm(span=13, adjust=False).mean() +ma3 = data.diff(1).abs().ewm(span=25, adjust=False).mean() +ma4 = ma3.ewm(span=13, adjust=False).mean() +print(ma2/ma4*100.) +*/ + +func Test_TSI(t *testing.T) { + var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []fixedpoint.Value + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + tsi := TSI{} + klines := buildKLines(input) + tsi.CalculateAndUpdate(klines) + assert.Equal(t, tsi.Length(), 29) + Delta := 1.5e-2 + assert.InDelta(t, tsi.Last(), 22.89, Delta) +} From 1ca79db4e5d0b545e4a4f03ae49e57929b4bbc78 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 22 Dec 2022 13:35:04 +0900 Subject: [PATCH 0308/1392] fix: volume profile --- pkg/indicator/volumeprofile.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/indicator/volumeprofile.go b/pkg/indicator/volumeprofile.go index 4d487f39fa..df8903fe03 100644 --- a/pkg/indicator/volumeprofile.go +++ b/pkg/indicator/volumeprofile.go @@ -33,22 +33,26 @@ func (inc *VolumeProfile) Update(price, volume float64, timestamp types.Time) { if inc.maxPrice == 0 { inc.maxPrice = math.Inf(-1) } + if inc.profile == nil { + inc.profile = make(map[float64]float64) + } inc.profile[math.Round(price/inc.Delta)] += volume filter := timestamp.Time().Add(-time.Duration(inc.Window) * inc.Interval.Duration()) + inc.trades = append(inc.trades, trade{ + price: price, + volume: volume, + timestamp: timestamp, + }) var i int for i = 0; i < len(inc.trades); i++ { td := inc.trades[i] if td.timestamp.After(filter) { + inc.trades = inc.trades[i : len(inc.trades)-1] break } inc.profile[math.Round(td.price/inc.Delta)] -= td.volume } - inc.trades = inc.trades[i : len(inc.trades)-1] - inc.trades = append(inc.trades, trade{ - price: price, - volume: volume, - timestamp: timestamp, - }) + for k, _ := range inc.profile { if k < inc.minPrice { inc.minPrice = k From 5b4be1f9fc4b0baf1ffe122466df83c1bc4d9a28 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 22 Dec 2022 13:14:25 +0800 Subject: [PATCH 0309/1392] max: drop unused toMaxSubmitOrder --- pkg/exchange/max/exchange.go | 61 ------------------------------------ 1 file changed, 61 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 8f436a00f1..6debfdc7bd 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -384,67 +384,6 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err return err2 } -func toMaxSubmitOrder(o types.SubmitOrder) (*maxapi.SubmitOrder, error) { - symbol := toLocalSymbol(o.Symbol) - orderType, err := toLocalOrderType(o.Type) - if err != nil { - return nil, err - } - - // case IOC type - if orderType == maxapi.OrderTypeLimit && o.TimeInForce == types.TimeInForceIOC { - orderType = maxapi.OrderTypeIOCLimit - } - - var quantityString string - if o.Market.Symbol != "" { - quantityString = o.Market.FormatQuantity(o.Quantity) - } else { - quantityString = o.Quantity.String() - } - - maxOrder := maxapi.SubmitOrder{ - Market: symbol, - Side: toLocalSideType(o.Side), - OrderType: orderType, - Volume: quantityString, - } - - if o.GroupID > 0 { - maxOrder.GroupID = o.GroupID - } - - clientOrderID := NewClientOrderID(o.ClientOrderID) - if len(clientOrderID) > 0 { - maxOrder.ClientOID = clientOrderID - } - - switch o.Type { - case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker: - var priceInString string - if o.Market.Symbol != "" { - priceInString = o.Market.FormatPrice(o.Price) - } else { - priceInString = o.Price.String() - } - maxOrder.Price = priceInString - } - - // set stop price field for limit orders - switch o.Type { - case types.OrderTypeStopLimit, types.OrderTypeStopMarket: - var priceInString string - if o.Market.Symbol != "" { - priceInString = o.Market.FormatPrice(o.StopPrice) - } else { - priceInString = o.StopPrice.String() - } - maxOrder.StopPrice = priceInString - } - - return &maxOrder, nil -} - func (e *Exchange) Withdraw(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *types.WithdrawalOptions) error { asset = toLocalCurrency(asset) From c59c8638be4482d8bb90ec048155f02ef3471ddc Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 22 Dec 2022 13:21:39 +0800 Subject: [PATCH 0310/1392] grid2: find lastOrderTime and firstOrderTime range --- pkg/strategy/grid2/strategy.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f6e0e1cf03..8b0204dfe0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1003,10 +1003,19 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio return nil } + lastOrderID := uint64(0) firstOrderTime := openOrders[0].CreationTime.Time() + lastOrderTime := firstOrderTime for _, o := range openOrders { - if o.CreationTime.Before(firstOrderTime) { - firstOrderTime = o.CreationTime.Time() + if o.OrderID > lastOrderID { + lastOrderID = o.OrderID + } + + createTime := o.CreationTime.Time() + if createTime.Before(firstOrderTime) { + firstOrderTime = createTime + } else if createTime.After(lastOrderTime) { + lastOrderTime = createTime } } @@ -1028,7 +1037,9 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio } } - closedOrders, err := historyService.QueryClosedOrders(ctx, s.Symbol, firstOrderTime, time.Now(), 0) + // Note that for MAX Exchange, the order history API only uses fromID parameter to query history order. + // The time range does not matter. + closedOrders, err := historyService.QueryClosedOrders(ctx, s.Symbol, firstOrderTime, time.Now(), lastOrderID) if err != nil { return err } From 1ea72099ed1c1f29a4504ac6a9cae4332fc95802 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 22 Dec 2022 14:10:35 +0800 Subject: [PATCH 0311/1392] service: fix margin sync with asset condition --- pkg/service/margin.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/service/margin.go b/pkg/service/margin.go index 1794712f4e..563167c0db 100644 --- a/pkg/service/margin.go +++ b/pkg/service/margin.go @@ -34,7 +34,7 @@ func (s *MarginService) Sync(ctx context.Context, ex types.Exchange, asset strin tasks := []SyncTask{ { - Select: SelectLastMarginLoans(ex.Name(), 100), + Select: SelectLastMarginLoans(ex.Name(), asset, 100), Type: types.MarginLoan{}, BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { query := &batch.MarginLoanBatchQuery{ @@ -51,7 +51,7 @@ func (s *MarginService) Sync(ctx context.Context, ex types.Exchange, asset strin LogInsert: true, }, { - Select: SelectLastMarginRepays(ex.Name(), 100), + Select: SelectLastMarginRepays(ex.Name(), asset, 100), Type: types.MarginRepay{}, BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { query := &batch.MarginRepayBatchQuery{ @@ -68,7 +68,7 @@ func (s *MarginService) Sync(ctx context.Context, ex types.Exchange, asset strin LogInsert: true, }, { - Select: SelectLastMarginInterests(ex.Name(), 100), + Select: SelectLastMarginInterests(ex.Name(), asset, 100), Type: types.MarginInterest{}, BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { query := &batch.MarginInterestBatchQuery{ @@ -114,26 +114,26 @@ func (s *MarginService) Sync(ctx context.Context, ex types.Exchange, asset strin return nil } -func SelectLastMarginLoans(ex types.ExchangeName, limit uint64) sq.SelectBuilder { +func SelectLastMarginLoans(ex types.ExchangeName, asset string, limit uint64) sq.SelectBuilder { return sq.Select("*"). From("margin_loans"). - Where(sq.Eq{"exchange": ex}). + Where(sq.Eq{"exchange": ex, "asset": asset}). OrderBy("time DESC"). Limit(limit) } -func SelectLastMarginRepays(ex types.ExchangeName, limit uint64) sq.SelectBuilder { +func SelectLastMarginRepays(ex types.ExchangeName, asset string, limit uint64) sq.SelectBuilder { return sq.Select("*"). From("margin_repays"). - Where(sq.Eq{"exchange": ex}). + Where(sq.Eq{"exchange": ex, "asset": asset}). OrderBy("time DESC"). Limit(limit) } -func SelectLastMarginInterests(ex types.ExchangeName, limit uint64) sq.SelectBuilder { +func SelectLastMarginInterests(ex types.ExchangeName, asset string, limit uint64) sq.SelectBuilder { return sq.Select("*"). From("margin_interests"). - Where(sq.Eq{"exchange": ex}). + Where(sq.Eq{"exchange": ex, "asset": asset}). OrderBy("time DESC"). Limit(limit) } From bf87d04d57ded5d95847613ea393d3e019e53f3b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 22 Dec 2022 19:07:55 +0800 Subject: [PATCH 0312/1392] binance: change rate limit unit to minute --- pkg/exchange/binance/exchange.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index cba2ed2749..aea4e1459f 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -56,11 +56,11 @@ func init() { _ = types.FuturesExchange(&Exchange{}) if n, ok := util.GetEnvVarInt("BINANCE_ORDER_RATE_LIMITER"); ok { - orderLimiter = rate.NewLimiter(rate.Limit(n), 2) + orderLimiter = rate.NewLimiter(rate.Every(time.Duration(n)*time.Minute), 2) } if n, ok := util.GetEnvVarInt("BINANCE_QUERY_TRADES_RATE_LIMITER"); ok { - queryTradeLimiter = rate.NewLimiter(rate.Limit(n), 2) + queryTradeLimiter = rate.NewLimiter(rate.Every(time.Duration(n)*time.Minute), 2) } } From 006ac7c2b3b80a5a2abd8b6e33cbfaf466a6f825 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 13:08:05 +0800 Subject: [PATCH 0313/1392] update command doc files --- doc/commands/bbgo.md | 3 ++- doc/commands/bbgo_account.md | 3 ++- doc/commands/bbgo_backtest.md | 5 +++-- doc/commands/bbgo_balances.md | 3 ++- doc/commands/bbgo_build.md | 7 ++++--- doc/commands/bbgo_cancel-order.md | 3 ++- doc/commands/bbgo_deposits.md | 3 ++- doc/commands/bbgo_execute-order.md | 3 ++- doc/commands/bbgo_get-order.md | 3 ++- doc/commands/bbgo_hoptimize.md | 3 ++- doc/commands/bbgo_kline.md | 3 ++- doc/commands/bbgo_list-orders.md | 3 ++- doc/commands/bbgo_margin.md | 3 ++- doc/commands/bbgo_margin_interests.md | 3 ++- doc/commands/bbgo_margin_loans.md | 3 ++- doc/commands/bbgo_margin_repays.md | 3 ++- doc/commands/bbgo_market.md | 3 ++- doc/commands/bbgo_optimize.md | 3 ++- doc/commands/bbgo_orderbook.md | 3 ++- doc/commands/bbgo_orderupdate.md | 3 ++- doc/commands/bbgo_pnl.md | 3 ++- doc/commands/bbgo_run.md | 3 ++- doc/commands/bbgo_submit-order.md | 3 ++- doc/commands/bbgo_sync.md | 3 ++- doc/commands/bbgo_trades.md | 3 ++- doc/commands/bbgo_tradeupdate.md | 3 ++- doc/commands/bbgo_transfer-history.md | 3 ++- doc/commands/bbgo_userdatastream.md | 3 ++- doc/commands/bbgo_version.md | 3 ++- 29 files changed, 61 insertions(+), 32 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index a3663da90e..3c1870faaf 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -24,6 +24,7 @@ bbgo [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -59,4 +60,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 355a3898cd..1b09b89060 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -31,6 +31,7 @@ bbgo account [--session SESSION] [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -42,4 +43,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 7eb6727d55..35a7b3f31a 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -10,6 +10,7 @@ bbgo backtest [flags] ``` --base-asset-baseline use base asset performance as the competitive baseline performance + --config string strategy config file (default "config/bbgo.yaml") --force force execution without confirm -h, --help help for backtest --output string the report output directory @@ -28,7 +29,6 @@ bbgo backtest [flags] ``` --binance-api-key string binance api key --binance-api-secret string binance api secret - --config string config file (default "bbgo.yaml") --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") @@ -40,6 +40,7 @@ bbgo backtest [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -51,4 +52,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index 9eda2ef7c1..ca8b2e13e9 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -30,6 +30,7 @@ bbgo balances [--session SESSION] [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -41,4 +42,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index 50ce89e1ef..eb08e098a3 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -9,7 +9,8 @@ bbgo build [flags] ### Options ``` - -h, --help help for build + --config string config file (default "bbgo.yaml") + -h, --help help for build ``` ### Options inherited from parent commands @@ -17,7 +18,6 @@ bbgo build [flags] ``` --binance-api-key string binance api key --binance-api-secret string binance api secret - --config string config file (default "bbgo.yaml") --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") @@ -29,6 +29,7 @@ bbgo build [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -40,4 +41,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index 198539955d..88b03d2868 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -39,6 +39,7 @@ bbgo cancel-order [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -50,4 +51,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index b29d4fe06e..c2646bb6b1 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -31,6 +31,7 @@ bbgo deposits [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -42,4 +43,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index aa565a429d..2ebf65916d 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -38,6 +38,7 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -49,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index e222bc8985..5055a522ab 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -32,6 +32,7 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -43,4 +44,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index 44f3cd46f9..ce543ab4f9 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -35,6 +35,7 @@ bbgo hoptimize [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -46,4 +47,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index b984f122d3..70a1c1d03d 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -32,6 +32,7 @@ bbgo kline [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -43,4 +44,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index fd92f31224..0baf138910 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -31,6 +31,7 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -42,4 +43,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index 30566f3ff2..b98cdc801e 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -25,6 +25,7 @@ margin related history --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -39,4 +40,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index d5cce499d4..e29b7cbe0b 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -31,6 +31,7 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -42,4 +43,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 5e28f3ebdd..94b44e8371 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -31,6 +31,7 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -42,4 +43,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index f0cb88ed97..b45e35593b 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -31,6 +31,7 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -42,4 +43,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index c6758b56eb..cf37ce1847 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -30,6 +30,7 @@ bbgo market [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -41,4 +42,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index 6fbec07c07..c399d60508 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -34,6 +34,7 @@ bbgo optimize [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -45,4 +46,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index ce4bdb6c1b..461eb307a8 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -32,6 +32,7 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -43,4 +44,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index b0f7fcd15b..8e60f072d1 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -30,6 +30,7 @@ bbgo orderupdate [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -41,4 +42,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index 679837d710..126952bd31 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -39,6 +39,7 @@ bbgo pnl [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -50,4 +51,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 7c06a00b7b..2195bd12e8 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -41,6 +41,7 @@ bbgo run [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -52,4 +53,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index e66dda886b..10515e48da 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -36,6 +36,7 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -47,4 +48,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index f677b28f94..3182868067 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -32,6 +32,7 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -43,4 +44,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index 5da8e79b9d..f0681016b7 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -32,6 +32,7 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -43,4 +44,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index a631f44f55..592b4251ab 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -30,6 +30,7 @@ bbgo tradeupdate --session=[exchange_name] [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -41,4 +42,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 374830eb17..2145c23ba1 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -32,6 +32,7 @@ bbgo transfer-history [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -43,4 +44,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index f84fca3b6c..d08b5ef7cf 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -30,6 +30,7 @@ bbgo userdatastream [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -41,4 +42,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index 9e2872a5d5..22cf1b4da0 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -29,6 +29,7 @@ bbgo version [flags] --metrics enable prometheus metrics --metrics-port string prometheus http server port (default "9090") --no-dotenv disable built-in dotenv + --rollbar-token string rollbar token --slack-channel string slack trading channel (default "dev-bbgo") --slack-error-channel string slack error channel (default "bbgo-error") --slack-token string slack token @@ -40,4 +41,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 12-Oct-2022 +###### Auto generated by spf13/cobra on 23-Dec-2022 From 28beca18e177a8a8128493c70e1d36aa2dd9c821 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 13:08:06 +0800 Subject: [PATCH 0314/1392] bump version to v1.43.0 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 549f3697d3..e76f3da2c5 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.42.0-7204e255-dev" +const Version = "v1.43.0-8bcfb78b-dev" -const VersionGitRef = "7204e255" +const VersionGitRef = "8bcfb78b" diff --git a/pkg/version/version.go b/pkg/version/version.go index 76ec8cbfba..8a8217347d 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.42.0-7204e255" +const Version = "v1.43.0-8bcfb78b" -const VersionGitRef = "7204e255" +const VersionGitRef = "8bcfb78b" From 3e87cbb83a5fa2d2b65aa93946fabbf8c33ecfac Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 13:08:06 +0800 Subject: [PATCH 0315/1392] add v1.43.0 release note --- doc/release/v1.43.0.md | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 doc/release/v1.43.0.md diff --git a/doc/release/v1.43.0.md b/doc/release/v1.43.0.md new file mode 100644 index 0000000000..589387b07a --- /dev/null +++ b/doc/release/v1.43.0.md @@ -0,0 +1,62 @@ +## All + +- Removed FTX code. + +## Fixes + +- Fixed binance websocket execution report message parsing. +- Fixed margin history sync query. +- Fixed rebalance backtest +- Fixed SerialMarketDataStore together with backtests. +- Fixed order executor for avoid checking base balance for futures. + +## Features + +- Added cancel order for exit roi take profit and loss +- Added rollbar support. +- Added docker image to quay.io. +- Added FastSubmitOrders method to order exeuctor. +- Added new optimizer / hoptimizer object types. +- Added aggTrade for binance + +## Strategies + +- Added grid2 strategy. +- Added LinReg maker strategy. +- Updated supertrend config. +- Improved IRR strategy. (see PRs for details) +- Improved Drift strategy. (see PRs for details) + + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.42.0...main) + + - [#1030](https://github.com/c9s/bbgo/pull/1030): strategy: grid2: recover functions. + - [#1031](https://github.com/c9s/bbgo/pull/1031): feature: push to quay.io + - [#1027](https://github.com/c9s/bbgo/pull/1027): strategy: LinReg Maker + - [#1028](https://github.com/c9s/bbgo/pull/1028): strategy: grid2: improve notification support + - [#1025](https://github.com/c9s/bbgo/pull/1025): feature: add rollbar support + - [#1024](https://github.com/c9s/bbgo/pull/1024): fix: binance my trades api + - [#1022](https://github.com/c9s/bbgo/pull/1022): strategy: grid2: more refactoring, fix bugs and add more tests + - [#1021](https://github.com/c9s/bbgo/pull/1021): strategy: grid2: add test case for aggregateOrderBaseFee + - [#1020](https://github.com/c9s/bbgo/pull/1020): strategy: grid2: run backtest in test and add more details + - [#1019](https://github.com/c9s/bbgo/pull/1019): strategy: grid2: profit spread, prune historical trades . etc + - [#1018](https://github.com/c9s/bbgo/pull/1018): strategy: grid2 [part2] -- reverse order and arb profit calculation + - [#1017](https://github.com/c9s/bbgo/pull/1017): feature: add sync_time.sh utility + - [#1006](https://github.com/c9s/bbgo/pull/1006): strategy: grid2 [part 1] - initializing grid orders + - [#1016](https://github.com/c9s/bbgo/pull/1016): doc: add series extend documentation + - [#1013](https://github.com/c9s/bbgo/pull/1013): feature: bbgo completion + - [#1014](https://github.com/c9s/bbgo/pull/1014): all: remove ftx + - [#1011](https://github.com/c9s/bbgo/pull/1011): strategy/supertrend: update supertrend config + - [#1008](https://github.com/c9s/bbgo/pull/1008): improve: speed-up live trade + - [#1009](https://github.com/c9s/bbgo/pull/1009): optimizer / hoptimizer add new object + - [#1004](https://github.com/c9s/bbgo/pull/1004): strategy: irr rollback to original nirr and consume kline + - [#989](https://github.com/c9s/bbgo/pull/989): strategy: irr: a mean reversion based on box of klines in same direction + - [#1000](https://github.com/c9s/bbgo/pull/1000): fix: rebalance: fix backtest + - [#997](https://github.com/c9s/bbgo/pull/997): fix: SerialMarketDataStore together with backtests + - [#1001](https://github.com/c9s/bbgo/pull/1001): add cancel order for exit roi take profit and loss + - [#996](https://github.com/c9s/bbgo/pull/996): fix/general-order-executor: do not check for base balance for futures + - [#995](https://github.com/c9s/bbgo/pull/995): feature: telegram notify to become async + - [#993](https://github.com/c9s/bbgo/pull/993): fix: indicator timeframe 1s + - [#994](https://github.com/c9s/bbgo/pull/994): feature: add aggTrade for binance + - [#991](https://github.com/c9s/bbgo/pull/991): fix/risk: remove balance check in CalculateBaseQuantity() + - [#990](https://github.com/c9s/bbgo/pull/990): fix: change variable names From 74f93f80fada8813760faaeac8d072d1bdd8b9ea Mon Sep 17 00:00:00 2001 From: Yo-An Lin Date: Fri, 23 Dec 2022 13:14:47 +0800 Subject: [PATCH 0316/1392] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d7bad59f27..9d04b7ccd7 100644 --- a/README.md +++ b/README.md @@ -362,6 +362,10 @@ Check out the strategy directory [strategy](pkg/strategy) for all built-in strat - `flashcrash` strategy implements a strategy that catches the flashcrash [flashcrash](pkg/strategy/flashcrash) - `marketcap` strategy implements a strategy that rebalances the portfolio based on the market capitalization [marketcap](pkg/strategy/marketcap). See [document](./doc/strategy/marketcap.md). +- `pivotshort` - shorting focused strategy. +- `irr` - return rate strategy. +- `drift` - drift strategy. +- `grid2` - the second-generation grid strategy. To run these built-in strategies, just modify the config file to make the configuration suitable for you, for example if you want to run From c721274adc8d4aa5d9671bf14c7015a1b9e0ca89 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 12:56:19 +0800 Subject: [PATCH 0317/1392] grid2: implement scanMissingGridOrders --- pkg/strategy/grid2/strategy.go | 80 ++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 8b0204dfe0..841ec370d9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -20,6 +20,8 @@ const ID = "grid2" const orderTag = "grid2" +type PriceMap map[string]fixedpoint.Value + var log = logrus.WithField("strategy", ID) var maxNumberOfOrderTradesQueryTries = 10 @@ -1023,7 +1025,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio orderBook := bbgo.NewActiveOrderBook(s.Symbol) // Add all open orders to the local order book - gridPriceMap := make(map[string]fixedpoint.Value) + gridPriceMap := make(PriceMap) for _, pin := range s.grid.Pins { price := fixedpoint.Value(pin) gridPriceMap[price.String()] = price @@ -1039,32 +1041,74 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio // Note that for MAX Exchange, the order history API only uses fromID parameter to query history order. // The time range does not matter. - closedOrders, err := historyService.QueryClosedOrders(ctx, s.Symbol, firstOrderTime, time.Now(), lastOrderID) - if err != nil { - return err - } + startTime := firstOrderTime + endTime := time.Now() + + // a simple guard, in reality, this startTime is not possible to exceed the endTime + // because the queries closed orders might still in the range. + for startTime.Before(endTime) { + closedOrders, err := historyService.QueryClosedOrders(ctx, s.Symbol, startTime, endTime, lastOrderID) + if err != nil { + return err + } + + if len(closedOrders) == 0 { + break + } + + // for each closed order, if it's newer than the open order's update time, we will update it. + for _, closedOrder := range closedOrders { + creationTime := closedOrder.CreationTime.Time() + if creationTime.After(startTime) { + startTime = creationTime + } + + // skip non-grid order prices + if _, ok := gridPriceMap[closedOrder.Price.String()]; !ok { + continue + } - // types.SortOrdersAscending() - // for each closed order, if it's newer than the open order's update time, we will update it. - for _, closedOrder := range closedOrders { - // skip non-grid order prices - if _, ok := gridPriceMap[closedOrder.Price.String()]; !ok { - continue + existingOrder := orderBook.Lookup(func(o types.Order) bool { + return o.Price.Compare(closedOrder.Price) == 0 + }) + + if existingOrder != nil { + // update order + if existingOrder.CreationTime.Time().Before(closedOrder.CreationTime.Time()) { + orderBook.Remove(*existingOrder) + orderBook.Add(closedOrder) + } + } else { + orderBook.Add(closedOrder) + } + } + + missingPrices := s.scanMissingGridOrders(orderBook, s.grid) + if len(missingPrices) == 0 { + break } + } + + return nil +} + +func (s *Strategy) scanMissingGridOrders(orderBook *bbgo.ActiveOrderBook, grid *Grid) PriceMap { + // Add all open orders to the local order book + gridPrices := make(PriceMap) + missingPrices := make(PriceMap) + for _, pin := range grid.Pins { + price := fixedpoint.Value(pin) + gridPrices[price.String()] = price existingOrder := orderBook.Lookup(func(o types.Order) bool { - return o.Price.Compare(closedOrder.Price) == 0 + return o.Price.Compare(price) == 0 }) - if existingOrder == nil { - orderBook.Add(closedOrder) - } else { - // TODO: Compare update time and create time - orderBook.Update(closedOrder) + missingPrices[price.String()] = price } } - return nil + return missingPrices } func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { From 8a45fe522e69e13a4c1706a03b88632882f652c4 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 16:48:40 +0800 Subject: [PATCH 0318/1392] grid2: pull out session dependency from the recoverGrid method --- pkg/strategy/grid2/strategy.go | 56 ++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 841ec370d9..7169847b77 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -989,35 +989,22 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { return nil } -func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { - historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) - if !implemented { - return nil - } - - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) - if err != nil { - return err - } - - // no open orders, the grid is not placed yet - if len(openOrders) == 0 { - return nil - } - +func (s *Strategy) recoverGrid(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { lastOrderID := uint64(0) - firstOrderTime := openOrders[0].CreationTime.Time() - lastOrderTime := firstOrderTime - for _, o := range openOrders { - if o.OrderID > lastOrderID { - lastOrderID = o.OrderID - } + if len(openOrders) > 0 { + firstOrderTime := openOrders[0].CreationTime.Time() + lastOrderTime := firstOrderTime + for _, o := range openOrders { + if o.OrderID > lastOrderID { + lastOrderID = o.OrderID + } - createTime := o.CreationTime.Time() - if createTime.Before(firstOrderTime) { - firstOrderTime = createTime - } else if createTime.After(lastOrderTime) { - lastOrderTime = createTime + createTime := o.CreationTime.Time() + if createTime.Before(firstOrderTime) { + firstOrderTime = createTime + } else if createTime.After(lastOrderTime) { + lastOrderTime = createTime + } } } @@ -1092,6 +1079,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio return nil } +// scanMissingGridOrders finds the missing grid order prices func (s *Strategy) scanMissingGridOrders(orderBook *bbgo.ActiveOrderBook, grid *Grid) PriceMap { // Add all open orders to the local order book gridPrices := make(PriceMap) @@ -1179,6 +1167,20 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } } + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } + + if len(openOrders) > 0 { + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) + if implemented { + if err := s.recoverGrid(ctx, historyService, openOrders); err != nil { + return err + } + } + } + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() From 882c56a820c9a28a4e664cfbed7a8dd9b83d1efb Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 17:54:30 +0800 Subject: [PATCH 0319/1392] grid2: add RecoverWhenStart option --- pkg/strategy/grid2/strategy.go | 40 +++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7169847b77..cd432585ce 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -96,6 +96,9 @@ type Strategy struct { // KeepOrdersWhenShutdown option is used for keeping the grid orders when shutting down bbgo KeepOrdersWhenShutdown bool `json:"keepOrdersWhenShutdown"` + // RecoverWhenStart option is used for recovering grid orders + RecoverWhenStart bool `json:"recoverWhenStart"` + // ClearOpenOrdersWhenStart // If this is set, when bbgo started, it will clear the open orders in the same market (by symbol) ClearOpenOrdersWhenStart bool `json:"clearOpenOrdersWhenStart"` @@ -990,10 +993,20 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { } func (s *Strategy) recoverGrid(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { + // Add all open orders to the local order book + gridPriceMap := make(PriceMap) + for _, pin := range s.grid.Pins { + price := fixedpoint.Value(pin) + gridPriceMap[price.String()] = price + } + lastOrderID := uint64(0) + now := time.Now() + firstOrderTime := now.AddDate(0, -1, 0) + lastOrderTime := firstOrderTime if len(openOrders) > 0 { - firstOrderTime := openOrders[0].CreationTime.Time() - lastOrderTime := firstOrderTime + firstOrderTime = openOrders[0].CreationTime.Time() + lastOrderTime = firstOrderTime for _, o := range openOrders { if o.OrderID > lastOrderID { lastOrderID = o.OrderID @@ -1011,13 +1024,6 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang // Allocate a local order book orderBook := bbgo.NewActiveOrderBook(s.Symbol) - // Add all open orders to the local order book - gridPriceMap := make(PriceMap) - for _, pin := range s.grid.Pins { - price := fixedpoint.Value(pin) - gridPriceMap[price.String()] = price - } - // Ensure that orders are grid orders // The price must be at the grid pin for _, openOrder := range openOrders { @@ -1029,7 +1035,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang // Note that for MAX Exchange, the order history API only uses fromID parameter to query history order. // The time range does not matter. startTime := firstOrderTime - endTime := time.Now() + endTime := now // a simple guard, in reality, this startTime is not possible to exceed the endTime // because the queries closed orders might still in the range. @@ -1060,7 +1066,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang }) if existingOrder != nil { - // update order + // To update order, we need to remove the old order, because it's using order ID as the key of the map. if existingOrder.CreationTime.Time().Before(closedOrder.CreationTime.Time()) { orderBook.Remove(*existingOrder) orderBook.Add(closedOrder) @@ -1172,11 +1178,15 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. return err } - if len(openOrders) > 0 { + if s.RecoverWhenStart && len(openOrders) > 0 { + s.logger.Infof("recoverWhenStart is set, found %d open orders, trying to recover grid orders...", len(openOrders)) + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) - if implemented { + if !implemented { + s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") + } else { if err := s.recoverGrid(ctx, historyService, openOrders); err != nil { - return err + return errors.Wrap(err, "recover grid error") } } } @@ -1185,7 +1195,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. defer wg.Done() if s.KeepOrdersWhenShutdown { - s.logger.Infof("KeepOrdersWhenShutdown is set, will keep the orders on the exchange") + s.logger.Infof("keepOrdersWhenShutdown is set, will keep the orders on the exchange") return } From fedb67171a203bc2bf8b5dfbff5b20d29c38106e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 18:18:45 +0800 Subject: [PATCH 0320/1392] config: update grid2 config example --- config/grid2-max.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 3895bf2ac1..596d0069f5 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -36,7 +36,7 @@ exchangeStrategies: symbol: BTCUSDT upperPrice: 18_000.0 lowerPrice: 16_000.0 - gridNumber: 100 + gridNumber: 20 ## compound is used for buying more inventory when the profit is made by the filled SELL order. ## when compound is disabled, fixed quantity is used for each grid order. @@ -87,6 +87,7 @@ exchangeStrategies: resetPositionWhenStart: true clearOpenOrdersWhenStart: false keepOrdersWhenShutdown: false + recoverOrdersWhenStart: false ## skipSpreadCheck skips the minimal spread check for the grid profit skipSpreadCheck: true From 6bcf5f8f82ecd47e429366311d152520caa1c609 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 18:19:00 +0800 Subject: [PATCH 0321/1392] bbgo: improve active order book printing --- pkg/bbgo/activeorderbook.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 7edab47050..78ed67fb29 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -3,6 +3,7 @@ package bbgo import ( "context" "encoding/json" + "sort" "time" "github.com/pkg/errors" @@ -257,7 +258,16 @@ func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) { } func (b *ActiveOrderBook) Print() { - for _, o := range b.orders.Orders() { + orders := b.orders.Orders() + + // sort orders by price + sort.Slice(orders, func(i, j int) bool { + o1 := orders[i] + o2 := orders[j] + return o1.Price.Compare(o2.Price) > 0 + }) + + for _, o := range orders { log.Infof("%s", o) } } From 606b4650b355bcd35f445fa249fdf1437e5fe694 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 19:15:46 +0800 Subject: [PATCH 0322/1392] grid2: add RecoverOrdersWhenStart and fix grid recover logic --- pkg/strategy/grid2/strategy.go | 73 ++++++++++++++++++++++++++++------ pkg/types/order.go | 2 +- pkg/types/sort.go | 9 +++++ 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index cd432585ce..85f94793ef 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -96,8 +96,8 @@ type Strategy struct { // KeepOrdersWhenShutdown option is used for keeping the grid orders when shutting down bbgo KeepOrdersWhenShutdown bool `json:"keepOrdersWhenShutdown"` - // RecoverWhenStart option is used for recovering grid orders - RecoverWhenStart bool `json:"recoverWhenStart"` + // RecoverOrdersWhenStart option is used for recovering grid orders + RecoverOrdersWhenStart bool `json:"recoverOrdersWhenStart"` // ClearOpenOrdersWhenStart // If this is set, when bbgo started, it will clear the open orders in the same market (by symbol) @@ -993,22 +993,25 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { } func (s *Strategy) recoverGrid(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { + grid := s.newGrid() + // Add all open orders to the local order book gridPriceMap := make(PriceMap) - for _, pin := range s.grid.Pins { + for _, pin := range grid.Pins { price := fixedpoint.Value(pin) gridPriceMap[price.String()] = price } - lastOrderID := uint64(0) + lastOrderID := uint64(1) now := time.Now() firstOrderTime := now.AddDate(0, -1, 0) lastOrderTime := firstOrderTime if len(openOrders) > 0 { + lastOrderID = openOrders[0].OrderID firstOrderTime = openOrders[0].CreationTime.Time() lastOrderTime = firstOrderTime for _, o := range openOrders { - if o.OrderID > lastOrderID { + if o.OrderID < lastOrderID { lastOrderID = o.OrderID } @@ -1051,6 +1054,11 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang // for each closed order, if it's newer than the open order's update time, we will update it. for _, closedOrder := range closedOrders { + // skip orders that are not limit order + if closedOrder.Type != types.OrderTypeLimit { + continue + } + creationTime := closedOrder.CreationTime.Time() if creationTime.After(startTime) { startTime = creationTime @@ -1062,31 +1070,70 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang } existingOrder := orderBook.Lookup(func(o types.Order) bool { - return o.Price.Compare(closedOrder.Price) == 0 + return o.Price.Eq(closedOrder.Price) }) - if existingOrder != nil { + if existingOrder == nil { + orderBook.Add(closedOrder) + } else { // To update order, we need to remove the old order, because it's using order ID as the key of the map. - if existingOrder.CreationTime.Time().Before(closedOrder.CreationTime.Time()) { + if creationTime.After(existingOrder.CreationTime.Time()) { orderBook.Remove(*existingOrder) orderBook.Add(closedOrder) } - } else { - orderBook.Add(closedOrder) } } - missingPrices := s.scanMissingGridOrders(orderBook, s.grid) + orderBook.Print() + + missingPrices := scanMissingGridOrders(orderBook, grid) if len(missingPrices) == 0 { + s.logger.Infof("no missing grid prices, stop re-playing order history") break } } + s.logger.Infof("orderbook:") + orderBook.Print() + + tmpOrders := orderBook.Orders() + types.SortOrdersUpdateTimeAscending(tmpOrders) + + // TODO: make sure that the number of orders matches the grid number - 1 + + filledOrders := ordersFilled(tmpOrders) + + s.logger.Infof("found %d filled orders", len(filledOrders)) + + if len(filledOrders) > 1 { + // remove the oldest updated order (for empty slot) + filledOrders = filledOrders[1:] + } + + s.grid = grid + for _, o := range filledOrders { + s.processFilledOrder(o) + } + + s.logger.Infof("recover complete") + + // recover complete return nil } +func ordersFilled(in []types.Order) (out []types.Order) { + for _, o := range in { + switch o.Status { + case types.OrderStatusFilled: + o2 := o + out = append(out, o2) + } + } + return out +} + // scanMissingGridOrders finds the missing grid order prices -func (s *Strategy) scanMissingGridOrders(orderBook *bbgo.ActiveOrderBook, grid *Grid) PriceMap { +func scanMissingGridOrders(orderBook *bbgo.ActiveOrderBook, grid *Grid) PriceMap { // Add all open orders to the local order book gridPrices := make(PriceMap) missingPrices := make(PriceMap) @@ -1178,7 +1225,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. return err } - if s.RecoverWhenStart && len(openOrders) > 0 { + if s.RecoverOrdersWhenStart && len(openOrders) > 0 { s.logger.Infof("recoverWhenStart is set, found %d open orders, trying to recover grid orders...", len(openOrders)) historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) diff --git a/pkg/types/order.go b/pkg/types/order.go index 1b33ea64f5..4444bac872 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -316,7 +316,7 @@ func (o Order) String() string { desc := fmt.Sprintf("ORDER %s | %s | %s | %s | %s %-4s | %s/%s @ %s", o.Exchange.String(), - o.CreationTime.Time().Local().Format(time.RFC1123), + o.CreationTime.Time().Local().Format(time.StampMilli), orderID, o.Symbol, o.Type, diff --git a/pkg/types/sort.go b/pkg/types/sort.go index 6893d52e39..9c0c606130 100644 --- a/pkg/types/sort.go +++ b/pkg/types/sort.go @@ -12,6 +12,7 @@ func SortTradesAscending(trades []Trade) []Trade { return trades } +// SortOrdersAscending sorts by creation time ascending-ly func SortOrdersAscending(orders []Order) []Order { sort.Slice(orders, func(i, j int) bool { return orders[i].CreationTime.Time().Before(orders[j].CreationTime.Time()) @@ -19,6 +20,14 @@ func SortOrdersAscending(orders []Order) []Order { return orders } +// SortOrdersAscending sorts by update time ascending-ly +func SortOrdersUpdateTimeAscending(orders []Order) []Order { + sort.Slice(orders, func(i, j int) bool { + return orders[i].UpdateTime.Time().Before(orders[j].UpdateTime.Time()) + }) + return orders +} + func SortKLinesAscending(klines []KLine) []KLine { sort.Slice(klines, func(i, j int) bool { return klines[i].StartTime.Unix() < klines[j].StartTime.Unix() From ae20cef8f46a1a68b541ef40904be99eb38e493e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 23:41:36 +0800 Subject: [PATCH 0323/1392] grid2: add scanOrderCreationTimeRange func --- pkg/strategy/grid2/strategy.go | 40 ++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 85f94793ef..7b903dc863 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1006,23 +1006,11 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang now := time.Now() firstOrderTime := now.AddDate(0, -1, 0) lastOrderTime := firstOrderTime - if len(openOrders) > 0 { - lastOrderID = openOrders[0].OrderID - firstOrderTime = openOrders[0].CreationTime.Time() - lastOrderTime = firstOrderTime - for _, o := range openOrders { - if o.OrderID < lastOrderID { - lastOrderID = o.OrderID - } - - createTime := o.CreationTime.Time() - if createTime.Before(firstOrderTime) { - firstOrderTime = createTime - } else if createTime.After(lastOrderTime) { - lastOrderTime = createTime - } - } + if since, until, ok := scanOrderCreationTimeRange(openOrders); ok { + firstOrderTime = since + lastOrderTime = until } + _ = lastOrderTime // Allocate a local order book orderBook := bbgo.NewActiveOrderBook(s.Symbol) @@ -1132,6 +1120,26 @@ func ordersFilled(in []types.Order) (out []types.Order) { return out } +// scanOrderCreationTimeRange finds the earliest creation time and the latest creation time from the given orders +func scanOrderCreationTimeRange(orders []types.Order) (time.Time, time.Time, bool) { + if len(orders) == 0 { + return time.Time{}, time.Time{}, false + } + + firstOrderTime := orders[0].CreationTime.Time() + lastOrderTime := firstOrderTime + for _, o := range orders { + createTime := o.CreationTime.Time() + if createTime.Before(firstOrderTime) { + firstOrderTime = createTime + } else if createTime.After(lastOrderTime) { + lastOrderTime = createTime + } + } + + return firstOrderTime, lastOrderTime, true +} + // scanMissingGridOrders finds the missing grid order prices func scanMissingGridOrders(orderBook *bbgo.ActiveOrderBook, grid *Grid) PriceMap { // Add all open orders to the local order book From 8715d0aca9ab43102ddb912cdb437c588dcbe611 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Dec 2022 23:50:30 +0800 Subject: [PATCH 0324/1392] grid2: prevent infinite loop --- pkg/strategy/grid2/strategy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7b903dc863..9afa423eb5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1036,7 +1036,8 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang return err } - if len(closedOrders) == 0 { + // need to prevent infinite loop for: len(closedOrders) == 1 and it's creationTime = startTime + if len(closedOrders) == 0 || len(closedOrders) == 1 && closedOrders[0].CreationTime.Time().Equal(startTime) { break } From 3d9b919e24dde563456da5ddea9dde4031dc7678 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 00:54:40 +0800 Subject: [PATCH 0325/1392] grid2: fix grid recovering --- pkg/strategy/grid2/mocks/order_executor.go | 15 +++ pkg/strategy/grid2/strategy.go | 142 ++++++++++++++++++--- 2 files changed, 142 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/grid2/mocks/order_executor.go b/pkg/strategy/grid2/mocks/order_executor.go index 0d25bc1d68..32aa101209 100644 --- a/pkg/strategy/grid2/mocks/order_executor.go +++ b/pkg/strategy/grid2/mocks/order_executor.go @@ -8,6 +8,7 @@ import ( context "context" reflect "reflect" + bbgo "github.com/c9s/bbgo/pkg/bbgo" fixedpoint "github.com/c9s/bbgo/pkg/fixedpoint" types "github.com/c9s/bbgo/pkg/types" gomock "github.com/golang/mock/gomock" @@ -36,6 +37,20 @@ func (m *MockOrderExecutor) EXPECT() *MockOrderExecutorMockRecorder { return m.recorder } +// ActiveMakerOrders mocks base method. +func (m *MockOrderExecutor) ActiveMakerOrders() *bbgo.ActiveOrderBook { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ActiveMakerOrders") + ret0, _ := ret[0].(*bbgo.ActiveOrderBook) + return ret0 +} + +// ActiveMakerOrders indicates an expected call of ActiveMakerOrders. +func (mr *MockOrderExecutorMockRecorder) ActiveMakerOrders() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActiveMakerOrders", reflect.TypeOf((*MockOrderExecutor)(nil).ActiveMakerOrders)) +} + // ClosePosition mocks base method. func (m *MockOrderExecutor) ClosePosition(arg0 context.Context, arg1 fixedpoint.Value, arg2 ...string) error { m.ctrl.T.Helper() diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 9afa423eb5..7edd135d87 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -38,6 +38,7 @@ type OrderExecutor interface { SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error GracefulCancel(ctx context.Context, orders ...types.Order) error + ActiveMakerOrders() *bbgo.ActiveOrderBook } type Strategy struct { @@ -1004,14 +1005,22 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang lastOrderID := uint64(1) now := time.Now() - firstOrderTime := now.AddDate(0, -1, 0) + firstOrderTime := now.AddDate(0, 0, -7) lastOrderTime := firstOrderTime if since, until, ok := scanOrderCreationTimeRange(openOrders); ok { firstOrderTime = since lastOrderTime = until } + + // for MAX exchange we need the order ID to query the closed order history + if oid, ok := findEarliestOrderID(openOrders); ok { + lastOrderID = oid + } + _ = lastOrderTime + activeOrderBook := s.orderExecutor.ActiveMakerOrders() + // Allocate a local order book orderBook := bbgo.NewActiveOrderBook(s.Symbol) @@ -1020,6 +1029,9 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang for _, openOrder := range openOrders { if _, exists := gridPriceMap[openOrder.Price.String()]; exists { orderBook.Add(openOrder) + + // put the order back to the active order book so that we can receive order update + activeOrderBook.Add(openOrder) } } @@ -1073,43 +1085,62 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang } } - orderBook.Print() - missingPrices := scanMissingGridOrders(orderBook, grid) if len(missingPrices) == 0 { - s.logger.Infof("no missing grid prices, stop re-playing order history") + s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") break } } - s.logger.Infof("orderbook:") - orderBook.Print() + debugOrderBook(orderBook, grid.Pins) tmpOrders := orderBook.Orders() + + // if all orders on the order book are active orders, we don't need to recover. + if isCompleteGridOrderBook(orderBook, s.GridNum) { + s.logger.Infof("GRID RECOVER: all orders are active orders, do not need recover") + return nil + } + + // for reverse order recovering, we need the orders to be sort by update time ascending-ly types.SortOrdersUpdateTimeAscending(tmpOrders) - // TODO: make sure that the number of orders matches the grid number - 1 + if len(tmpOrders) > 1 && len(tmpOrders) == int(s.GridNum)+1 { + // remove the latest updated order because it's near the empty slot + tmpOrders = tmpOrders[:len(tmpOrders)-1] + } + // we will only submit reverse orders for filled orders filledOrders := ordersFilled(tmpOrders) - s.logger.Infof("found %d filled orders", len(filledOrders)) - - if len(filledOrders) > 1 { - // remove the oldest updated order (for empty slot) - filledOrders = filledOrders[1:] - } + s.logger.Infof("GRID RECOVER: found %d filled grid orders", len(filledOrders)) s.grid = grid for _, o := range filledOrders { s.processFilledOrder(o) } - s.logger.Infof("recover complete") + s.logger.Infof("GRID RECOVER COMPLETE") + + debugOrderBook(s.orderExecutor.ActiveMakerOrders(), grid.Pins) - // recover complete return nil } +func isActiveOrder(o types.Order) bool { + return o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled +} + +func isCompleteGridOrderBook(orderBook *bbgo.ActiveOrderBook, gridNum int64) bool { + tmpOrders := orderBook.Orders() + + if len(tmpOrders) == int(gridNum) && ordersAll(tmpOrders, isActiveOrder) { + return true + } + + return false +} + func ordersFilled(in []types.Order) (out []types.Order) { for _, o := range in { switch o.Status { @@ -1121,6 +1152,87 @@ func ordersFilled(in []types.Order) (out []types.Order) { return out } +func ordersAll(orders []types.Order, f func(o types.Order) bool) bool { + for _, o := range orders { + if !f(o) { + return false + } + } + return true +} + +func ordersAny(orders []types.Order, f func(o types.Order) bool) bool { + for _, o := range orders { + if f(o) { + return true + } + } + return false +} + +func debugOrderBook(b *bbgo.ActiveOrderBook, pins []Pin) { + fmt.Println("================== GRID ORDERS ==================") + + // scan missing orders + missing := 0 + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + existingOrder := b.Lookup(func(o types.Order) bool { + return o.Price.Eq(price) + }) + if existingOrder == nil { + missing++ + } + } + + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + + fmt.Printf("%s -> ", price.String()) + + existingOrder := b.Lookup(func(o types.Order) bool { + return o.Price.Eq(price) + }) + + if existingOrder != nil { + fmt.Printf("%s", existingOrder.String()) + + switch existingOrder.Status { + case types.OrderStatusFilled: + fmt.Printf(" | 🔧") + case types.OrderStatusCanceled: + fmt.Printf(" | 🔄") + default: + fmt.Printf(" | ✅") + } + } else { + fmt.Printf("ORDER MISSING ⚠️ ") + if missing == 1 { + fmt.Printf(" COULD BE EMPTY SLOT") + } + } + fmt.Printf("\n") + } + fmt.Println("================== END OF GRID ORDERS ===================") +} + +func findEarliestOrderID(orders []types.Order) (uint64, bool) { + if len(orders) == 0 { + return 0, false + } + + earliestOrderID := orders[0].OrderID + for _, o := range orders { + if o.OrderID < earliestOrderID { + earliestOrderID = o.OrderID + } + } + + return earliestOrderID, true +} + // scanOrderCreationTimeRange finds the earliest creation time and the latest creation time from the given orders func scanOrderCreationTimeRange(orders []types.Order) (time.Time, time.Time, bool) { if len(orders) == 0 { From 216bdb891fa4fb2f13cdd45b67179f348e51469a Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 01:08:28 +0800 Subject: [PATCH 0326/1392] grid2: skip canceled orders --- pkg/exchange/max/exchange.go | 3 +-- pkg/strategy/grid2/strategy.go | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 6debfdc7bd..0b5c0f8b45 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -244,8 +244,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ // lastOrderID is not supported on MAX func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) ([]types.Order, error) { - log.Warn("!!!MAX EXCHANGE API NOTICE!!!") - log.Warn("the since/until conditions will not be effected on closed orders query, max exchange does not support time-range-based query") + log.Warn("!!!MAX EXCHANGE API NOTICE!!! the since/until conditions will not be effected on closed orders query, max exchange does not support time-range-based query") return e.queryClosedOrdersByLastOrderID(ctx, symbol, lastOrderID) } diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7edd135d87..2c397ee452 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1060,6 +1060,11 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang continue } + // skip canceled orders (?) + if closedOrder.Status == types.OrderStatusCanceled { + continue + } + creationTime := closedOrder.CreationTime.Time() if creationTime.After(startTime) { startTime = creationTime From d18535bfb8a854b37c0597010a73b995a36092a5 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 01:29:05 +0800 Subject: [PATCH 0327/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index 3c1870faaf..7162e69342 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -60,4 +60,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 1b09b89060..cfec28f9d4 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -43,4 +43,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 35a7b3f31a..4211995532 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -52,4 +52,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index ca8b2e13e9..d5819a29e3 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -42,4 +42,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index eb08e098a3..efd3c28d9c 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -41,4 +41,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index 88b03d2868..1cb49a406b 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -51,4 +51,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index c2646bb6b1..79407138a4 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -43,4 +43,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 2ebf65916d..27ec167b5b 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -50,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 5055a522ab..4ea39bbbcf 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -44,4 +44,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index ce543ab4f9..9426fc9313 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -47,4 +47,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 70a1c1d03d..1a71a372a6 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -44,4 +44,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index 0baf138910..4b510f8ecf 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -43,4 +43,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index b98cdc801e..7e540c5850 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -40,4 +40,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index e29b7cbe0b..ea4ecf6d50 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -43,4 +43,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 94b44e8371..260cf70867 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -43,4 +43,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index b45e35593b..d4aa174cc6 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -43,4 +43,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index cf37ce1847..4eba889e9e 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -42,4 +42,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index c399d60508..8bb818dfa9 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -46,4 +46,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index 461eb307a8..4733cd0f36 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -44,4 +44,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index 8e60f072d1..bee12f5038 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -42,4 +42,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index 126952bd31..f7ee450567 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -51,4 +51,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 2195bd12e8..90db070e5d 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -53,4 +53,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 10515e48da..c53957181b 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -48,4 +48,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index 3182868067..ef8947eea5 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -44,4 +44,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index f0681016b7..e040da5144 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -44,4 +44,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 592b4251ab..1ee6f977aa 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -42,4 +42,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 2145c23ba1..33ad27aa4d 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -44,4 +44,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index d08b5ef7cf..c0c567216d 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -42,4 +42,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index 22cf1b4da0..62e44424b7 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -41,4 +41,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 23-Dec-2022 +###### Auto generated by spf13/cobra on 24-Dec-2022 From 53b2a0d7ab58de7ffd047c5d98d4b17eb77132c8 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 01:29:05 +0800 Subject: [PATCH 0328/1392] bump version to v1.43.1 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index e76f3da2c5..eb15e9ef31 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.43.0-8bcfb78b-dev" +const Version = "v1.43.1-b7f3d4f1-dev" -const VersionGitRef = "8bcfb78b" +const VersionGitRef = "b7f3d4f1" diff --git a/pkg/version/version.go b/pkg/version/version.go index 8a8217347d..699cdef19b 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.43.0-8bcfb78b" +const Version = "v1.43.1-b7f3d4f1" -const VersionGitRef = "8bcfb78b" +const VersionGitRef = "b7f3d4f1" From 1be5c11577c3c2309c03c61d467822eebf69ed7c Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 01:29:05 +0800 Subject: [PATCH 0329/1392] add v1.43.1 release note --- doc/release/v1.43.1.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/release/v1.43.1.md diff --git a/doc/release/v1.43.1.md b/doc/release/v1.43.1.md new file mode 100644 index 0000000000..dc2523e985 --- /dev/null +++ b/doc/release/v1.43.1.md @@ -0,0 +1,7 @@ +## Fixes + +- Fixed grid2 order recovering + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.43.0...main) + + - [#1032](https://github.com/c9s/bbgo/pull/1032): strategy: grid2: recover grid orders [part 2] From 8e20a5506068aab8c445ff75f660d43add08dc01 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 14:48:30 +0800 Subject: [PATCH 0330/1392] grid2: refactor recover functions to replayOrderHistory and reuse scanMissingPinPrices --- pkg/strategy/grid2/strategy.go | 111 +++++++++++++++++---------------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2c397ee452..babfcd6834 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -993,9 +993,7 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { return nil } -func (s *Strategy) recoverGrid(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { - grid := s.newGrid() - +func buildGridPriceMap(grid *Grid) PriceMap { // Add all open orders to the local order book gridPriceMap := make(PriceMap) for _, pin := range grid.Pins { @@ -1003,6 +1001,15 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang gridPriceMap[price.String()] = price } + return gridPriceMap +} + +func (s *Strategy) recoverGrid(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { + grid := s.newGrid() + + // Add all open orders to the local order book + gridPriceMap := buildGridPriceMap(grid) + lastOrderID := uint64(1) now := time.Now() firstOrderTime := now.AddDate(0, 0, -7) @@ -1037,8 +1044,47 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang // Note that for MAX Exchange, the order history API only uses fromID parameter to query history order. // The time range does not matter. - startTime := firstOrderTime - endTime := now + // TODO: handle context correctly + if err := s.replayOrderHistory(ctx, grid, orderBook, historyService, firstOrderTime, now, lastOrderID); err != nil { + return err + } + + debugOrderBook(orderBook, grid.Pins) + + tmpOrders := orderBook.Orders() + + // if all orders on the order book are active orders, we don't need to recover. + if isCompleteGridOrderBook(orderBook, s.GridNum) { + s.logger.Infof("GRID RECOVER: all orders are active orders, do not need recover") + return nil + } + + // for reverse order recovering, we need the orders to be sort by update time ascending-ly + types.SortOrdersUpdateTimeAscending(tmpOrders) + + if len(tmpOrders) > 1 && len(tmpOrders) == int(s.GridNum)+1 { + // remove the latest updated order because it's near the empty slot + tmpOrders = tmpOrders[:len(tmpOrders)-1] + } + + // we will only submit reverse orders for filled orders + filledOrders := ordersFilled(tmpOrders) + + s.logger.Infof("GRID RECOVER: found %d filled grid orders", len(filledOrders)) + + s.grid = grid + for _, o := range filledOrders { + s.processFilledOrder(o) + } + + s.logger.Infof("GRID RECOVER COMPLETE") + + debugOrderBook(s.orderExecutor.ActiveMakerOrders(), grid.Pins) + return nil +} + +func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook *bbgo.ActiveOrderBook, historyService types.ExchangeTradeHistoryService, startTime, endTime time.Time, lastOrderID uint64) error { + gridPriceMap := buildGridPriceMap(grid) // a simple guard, in reality, this startTime is not possible to exceed the endTime // because the queries closed orders might still in the range. @@ -1090,45 +1136,13 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang } } - missingPrices := scanMissingGridOrders(orderBook, grid) + missingPrices := scanMissingPinPrices(orderBook, grid.Pins) if len(missingPrices) == 0 { s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") break } } - debugOrderBook(orderBook, grid.Pins) - - tmpOrders := orderBook.Orders() - - // if all orders on the order book are active orders, we don't need to recover. - if isCompleteGridOrderBook(orderBook, s.GridNum) { - s.logger.Infof("GRID RECOVER: all orders are active orders, do not need recover") - return nil - } - - // for reverse order recovering, we need the orders to be sort by update time ascending-ly - types.SortOrdersUpdateTimeAscending(tmpOrders) - - if len(tmpOrders) > 1 && len(tmpOrders) == int(s.GridNum)+1 { - // remove the latest updated order because it's near the empty slot - tmpOrders = tmpOrders[:len(tmpOrders)-1] - } - - // we will only submit reverse orders for filled orders - filledOrders := ordersFilled(tmpOrders) - - s.logger.Infof("GRID RECOVER: found %d filled grid orders", len(filledOrders)) - - s.grid = grid - for _, o := range filledOrders { - s.processFilledOrder(o) - } - - s.logger.Infof("GRID RECOVER COMPLETE") - - debugOrderBook(s.orderExecutor.ActiveMakerOrders(), grid.Pins) - return nil } @@ -1178,18 +1192,8 @@ func ordersAny(orders []types.Order, f func(o types.Order) bool) bool { func debugOrderBook(b *bbgo.ActiveOrderBook, pins []Pin) { fmt.Println("================== GRID ORDERS ==================") - // scan missing orders - missing := 0 - for i := len(pins) - 1; i >= 0; i-- { - pin := pins[i] - price := fixedpoint.Value(pin) - existingOrder := b.Lookup(func(o types.Order) bool { - return o.Price.Eq(price) - }) - if existingOrder == nil { - missing++ - } - } + missingPins := scanMissingPinPrices(b, pins) + missing := len(missingPins) for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] @@ -1258,15 +1262,14 @@ func scanOrderCreationTimeRange(orders []types.Order) (time.Time, time.Time, boo return firstOrderTime, lastOrderTime, true } -// scanMissingGridOrders finds the missing grid order prices -func scanMissingGridOrders(orderBook *bbgo.ActiveOrderBook, grid *Grid) PriceMap { +// scanMissingPinPrices finds the missing grid order prices +func scanMissingPinPrices(orderBook *bbgo.ActiveOrderBook, pins []Pin) PriceMap { // Add all open orders to the local order book gridPrices := make(PriceMap) missingPrices := make(PriceMap) - for _, pin := range grid.Pins { + for _, pin := range pins { price := fixedpoint.Value(pin) gridPrices[price.String()] = price - existingOrder := orderBook.Lookup(func(o types.Order) bool { return o.Price.Compare(price) == 0 }) From a46b3fe90840bc99d45132d77061cc1bdc4f726b Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 14:52:01 +0800 Subject: [PATCH 0331/1392] grid2: improve debugGrid func --- pkg/strategy/grid2/strategy.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index babfcd6834..948249d3db 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1049,7 +1049,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang return err } - debugOrderBook(orderBook, grid.Pins) + debugGrid(grid, orderBook) tmpOrders := orderBook.Orders() @@ -1079,7 +1079,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang s.logger.Infof("GRID RECOVER COMPLETE") - debugOrderBook(s.orderExecutor.ActiveMakerOrders(), grid.Pins) + debugGrid(grid, s.orderExecutor.ActiveMakerOrders()) return nil } @@ -1189,10 +1189,11 @@ func ordersAny(orders []types.Order, f func(o types.Order) bool) bool { return false } -func debugOrderBook(b *bbgo.ActiveOrderBook, pins []Pin) { +func debugGrid(grid *Grid, book *bbgo.ActiveOrderBook) { fmt.Println("================== GRID ORDERS ==================") - missingPins := scanMissingPinPrices(b, pins) + pins := grid.Pins + missingPins := scanMissingPinPrices(book, pins) missing := len(missingPins) for i := len(pins) - 1; i >= 0; i-- { @@ -1201,7 +1202,7 @@ func debugOrderBook(b *bbgo.ActiveOrderBook, pins []Pin) { fmt.Printf("%s -> ", price.String()) - existingOrder := b.Lookup(func(o types.Order) bool { + existingOrder := book.Lookup(func(o types.Order) bool { return o.Price.Eq(price) }) From 6b7515098365f813d945f4b4c0d2dfedf8e14f83 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 15:58:02 +0800 Subject: [PATCH 0332/1392] refactor order related functions into core api --- pkg/strategy/grid2/strategy.go | 37 ++-------------------------------- pkg/types/order.go | 33 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 948249d3db..f2e59f639b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1068,7 +1068,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang } // we will only submit reverse orders for filled orders - filledOrders := ordersFilled(tmpOrders) + filledOrders := types.OrdersFilled(tmpOrders) s.logger.Infof("GRID RECOVER: found %d filled grid orders", len(filledOrders)) @@ -1146,49 +1146,16 @@ func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook return nil } -func isActiveOrder(o types.Order) bool { - return o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled -} - func isCompleteGridOrderBook(orderBook *bbgo.ActiveOrderBook, gridNum int64) bool { tmpOrders := orderBook.Orders() - if len(tmpOrders) == int(gridNum) && ordersAll(tmpOrders, isActiveOrder) { + if len(tmpOrders) == int(gridNum) && types.OrdersAll(tmpOrders, types.IsActiveOrder) { return true } return false } -func ordersFilled(in []types.Order) (out []types.Order) { - for _, o := range in { - switch o.Status { - case types.OrderStatusFilled: - o2 := o - out = append(out, o2) - } - } - return out -} - -func ordersAll(orders []types.Order, f func(o types.Order) bool) bool { - for _, o := range orders { - if !f(o) { - return false - } - } - return true -} - -func ordersAny(orders []types.Order, f func(o types.Order) bool) bool { - for _, o := range orders { - if f(o) { - return true - } - } - return false -} - func debugGrid(grid *Grid, book *bbgo.ActiveOrderBook) { fmt.Println("================== GRID ORDERS ==================") diff --git a/pkg/types/order.go b/pkg/types/order.go index 4444bac872..82456caf94 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -394,3 +394,36 @@ func (o Order) SlackAttachment() slack.Attachment { Footer: strings.ToLower(o.Exchange.String()) + templateutil.Render(" creation time {{ . }}", o.CreationTime.Time().Format(time.StampMilli)), } } + +func OrdersFilled(in []Order) (out []Order) { + for _, o := range in { + switch o.Status { + case OrderStatusFilled: + o2 := o + out = append(out, o2) + } + } + return out +} + +func OrdersAll(orders []Order, f func(o Order) bool) bool { + for _, o := range orders { + if !f(o) { + return false + } + } + return true +} + +func OrdersAny(orders []Order, f func(o Order) bool) bool { + for _, o := range orders { + if f(o) { + return true + } + } + return false +} + +func IsActiveOrder(o Order) bool { + return o.Status == OrderStatusNew || o.Status == OrderStatusPartiallyFilled +} From cb2d9d7eb2432e8ae96105767c3c2a5a887fada5 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 16:14:39 +0800 Subject: [PATCH 0333/1392] grid2: fix replayOrderHistory logic --- pkg/strategy/grid2/strategy.go | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f2e59f639b..6ac1eab86f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1042,6 +1042,12 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang } } + missingPrices := scanMissingPinPrices(orderBook, grid.Pins) + if len(missingPrices) == 0 { + s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") + return nil + } + // Note that for MAX Exchange, the order history API only uses fromID parameter to query history order. // The time range does not matter. // TODO: handle context correctly @@ -1083,24 +1089,34 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang return nil } +// replayOrderHistory queries the closed order history from the API and rebuild the orderbook from the order history. +// startTime, endTime is the time range of the order history. func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook *bbgo.ActiveOrderBook, historyService types.ExchangeTradeHistoryService, startTime, endTime time.Time, lastOrderID uint64) error { gridPriceMap := buildGridPriceMap(grid) // a simple guard, in reality, this startTime is not possible to exceed the endTime // because the queries closed orders might still in the range. - for startTime.Before(endTime) { + orderIdChanged := true + for startTime.Before(endTime) && orderIdChanged { closedOrders, err := historyService.QueryClosedOrders(ctx, s.Symbol, startTime, endTime, lastOrderID) if err != nil { return err } - // need to prevent infinite loop for: len(closedOrders) == 1 and it's creationTime = startTime - if len(closedOrders) == 0 || len(closedOrders) == 1 && closedOrders[0].CreationTime.Time().Equal(startTime) { + // need to prevent infinite loop for: + // if there is only one order and the order creation time matches our startTime + if len(closedOrders) == 0 || len(closedOrders) == 1 && closedOrders[0].OrderID == lastOrderID { break } // for each closed order, if it's newer than the open order's update time, we will update it. + orderIdChanged = false for _, closedOrder := range closedOrders { + if closedOrder.OrderID > lastOrderID { + lastOrderID = closedOrder.OrderID + orderIdChanged = true + } + // skip orders that are not limit order if closedOrder.Type != types.OrderTypeLimit { continue @@ -1135,12 +1151,6 @@ func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook } } } - - missingPrices := scanMissingPinPrices(orderBook, grid.Pins) - if len(missingPrices) == 0 { - s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") - break - } } return nil From e0daf9904e83068e8cd9dda51e09741b0e58c497 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 17:08:50 +0800 Subject: [PATCH 0334/1392] grid2: add recover time range rollback --- pkg/strategy/grid2/strategy.go | 42 ++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6ac1eab86f..c322d89637 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -26,6 +26,9 @@ var log = logrus.WithField("strategy", ID) var maxNumberOfOrderTradesQueryTries = 10 +const historyRollbackDuration = 3 * 24 * time.Hour +const historyRollbackOrderIdRange = 1000 + func init() { // Register the pointer of the strategy struct, // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) @@ -1018,14 +1021,13 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang firstOrderTime = since lastOrderTime = until } + _ = lastOrderTime // for MAX exchange we need the order ID to query the closed order history if oid, ok := findEarliestOrderID(openOrders); ok { lastOrderID = oid } - _ = lastOrderTime - activeOrderBook := s.orderExecutor.ActiveMakerOrders() // Allocate a local order book @@ -1042,17 +1044,39 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang } } + // if all open orders are the grid orders, then we don't have to recover missingPrices := scanMissingPinPrices(orderBook, grid.Pins) - if len(missingPrices) == 0 { + if numMissing := len(missingPrices); numMissing <= 1 { s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") return nil - } + } else { + // Note that for MAX Exchange, the order history API only uses fromID parameter to query history order. + // The time range does not matter. + // TODO: handle context correctly + startTime := firstOrderTime + endTime := now + maxTries := 3 + for maxTries > 0 { + maxTries-- + if err := s.replayOrderHistory(ctx, grid, orderBook, historyService, startTime, endTime, lastOrderID); err != nil { + return err + } - // Note that for MAX Exchange, the order history API only uses fromID parameter to query history order. - // The time range does not matter. - // TODO: handle context correctly - if err := s.replayOrderHistory(ctx, grid, orderBook, historyService, firstOrderTime, now, lastOrderID); err != nil { - return err + // Verify if there are still missing prices + missingPrices = scanMissingPinPrices(orderBook, grid.Pins) + if len(missingPrices) <= 1 { + // skip this order history loop and start recovering + break + } + + // history rollback range + startTime = startTime.Add(-historyRollbackDuration) + if newFromOrderID := lastOrderID - historyRollbackOrderIdRange; newFromOrderID > 1 { + lastOrderID = newFromOrderID + } + + s.logger.Infof("GRID RECOVER: there are still more than two missing orders, rolling back query start time to earlier time point %s, fromID %d", startTime.String(), lastOrderID) + } } debugGrid(grid, orderBook) From 4388bc209bdfe5bff2925b1456677cffaecfdead Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 20:37:53 +0800 Subject: [PATCH 0335/1392] types: add simple duration type for parsing [0-9]+[wd] --- pkg/types/duration.go | 47 +++++++++++++++++++++++++++++++++++++++++++ pkg/types/market.go | 6 ++++++ 2 files changed, 53 insertions(+) create mode 100644 pkg/types/duration.go diff --git a/pkg/types/duration.go b/pkg/types/duration.go new file mode 100644 index 0000000000..9ec93d9bb9 --- /dev/null +++ b/pkg/types/duration.go @@ -0,0 +1,47 @@ +package types + +import ( + "regexp" + "strconv" + "time" + + "github.com/pkg/errors" +) + +var simpleDurationRegExp = regexp.MustCompile("^(\\d+)[hdw]$") + +var ErrNotSimpleDuration = errors.New("the given input is not simple duration format") + +type SimpleDuration struct { + Num int64 + Unit string + Duration Duration +} + +func ParseSimpleDuration(s string) (*SimpleDuration, error) { + if !simpleDurationRegExp.MatchString(s) { + return nil, errors.Wrapf(ErrNotSimpleDuration, "input %q is not a simple duration", s) + } + + matches := simpleDurationRegExp.FindStringSubmatch(s) + numStr := matches[1] + unit := matches[2] + num, err := strconv.ParseInt(numStr, 10, 64) + if err != nil { + return nil, err + } + + switch unit { + case "d": + d := Duration(time.Duration(num) * 24 * time.Hour) + return &SimpleDuration{num, unit, d}, nil + case "w": + d := Duration(time.Duration(num) * 7 * 24 * time.Hour) + return &SimpleDuration{num, unit, d}, nil + case "h": + d := Duration(time.Duration(num) * time.Hour) + return &SimpleDuration{num, unit, d}, nil + } + + return nil, errors.Wrapf(ErrNotSimpleDuration, "input %q is not a simple duration", s) +} diff --git a/pkg/types/market.go b/pkg/types/market.go index e2b08cd211..e9a9bd6b56 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -26,6 +26,12 @@ func (d *Duration) UnmarshalJSON(data []byte) error { switch t := o.(type) { case string: + sd, err := ParseSimpleDuration(t) + if err == nil { + *d = sd.Duration + return nil + } + dd, err := time.ParseDuration(t) if err != nil { return err From d27786d5aed7aa1e1efd9bdbe06e262c971db301 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 20:39:01 +0800 Subject: [PATCH 0336/1392] types: always use pointer on duration --- pkg/types/market.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/types/market.go b/pkg/types/market.go index e9a9bd6b56..23f3610ca5 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -13,8 +13,8 @@ import ( type Duration time.Duration -func (d Duration) Duration() time.Duration { - return time.Duration(d) +func (d *Duration) Duration() time.Duration { + return time.Duration(*d) } func (d *Duration) UnmarshalJSON(data []byte) error { From f60b4630c5b0e1d262a2ed1bca6317f15d8edefe Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Dec 2022 20:39:11 +0800 Subject: [PATCH 0337/1392] grid2: add AutoRange parameter --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c322d89637..9726502417 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -65,6 +65,8 @@ type Strategy struct { // GridNum is the grid number, how many orders you want to post on the orderbook. GridNum int64 `json:"gridNumber"` + AutoRange types.Duration `json:"autoRange"` + UpperPrice fixedpoint.Value `json:"upperPrice"` LowerPrice fixedpoint.Value `json:"lowerPrice"` From 579df0cec9ca6f1b79094ed08fc5d628cd047f59 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 25 Dec 2022 16:08:34 +0800 Subject: [PATCH 0338/1392] types: add simple duration tests --- pkg/strategy/grid2/strategy.go | 2 +- pkg/types/duration.go | 71 +++++++++++++++++++++++++++++++++- pkg/types/duration_test.go | 55 ++++++++++++++++++++++++++ pkg/types/market.go | 47 ---------------------- 4 files changed, 125 insertions(+), 50 deletions(-) create mode 100644 pkg/types/duration_test.go diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 9726502417..6dcca83e2c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -65,7 +65,7 @@ type Strategy struct { // GridNum is the grid number, how many orders you want to post on the orderbook. GridNum int64 `json:"gridNumber"` - AutoRange types.Duration `json:"autoRange"` + AutoRange types.SimpleDuration `json:"autoRange"` UpperPrice fixedpoint.Value `json:"upperPrice"` diff --git a/pkg/types/duration.go b/pkg/types/duration.go index 9ec93d9bb9..f465f24bd9 100644 --- a/pkg/types/duration.go +++ b/pkg/types/duration.go @@ -1,6 +1,8 @@ package types import ( + "encoding/json" + "fmt" "regexp" "strconv" "time" @@ -8,9 +10,9 @@ import ( "github.com/pkg/errors" ) -var simpleDurationRegExp = regexp.MustCompile("^(\\d+)[hdw]$") +var simpleDurationRegExp = regexp.MustCompile("^(\\d+)([hdw])$") -var ErrNotSimpleDuration = errors.New("the given input is not simple duration format") +var ErrNotSimpleDuration = errors.New("the given input is not simple duration format, valid format: [1-9][0-9]*[hdw]") type SimpleDuration struct { Num int64 @@ -18,7 +20,28 @@ type SimpleDuration struct { Duration Duration } +func (d *SimpleDuration) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + sd, err := ParseSimpleDuration(s) + if err != nil { + return err + } + + if sd != nil { + *d = *sd + } + return nil +} + func ParseSimpleDuration(s string) (*SimpleDuration, error) { + if s == "" { + return nil, nil + } + if !simpleDurationRegExp.MatchString(s) { return nil, errors.Wrapf(ErrNotSimpleDuration, "input %q is not a simple duration", s) } @@ -45,3 +68,47 @@ func ParseSimpleDuration(s string) (*SimpleDuration, error) { return nil, errors.Wrapf(ErrNotSimpleDuration, "input %q is not a simple duration", s) } + +type Duration time.Duration + +func (d *Duration) Duration() time.Duration { + return time.Duration(*d) +} + +func (d *Duration) UnmarshalJSON(data []byte) error { + var o interface{} + + if err := json.Unmarshal(data, &o); err != nil { + return err + } + + switch t := o.(type) { + case string: + sd, err := ParseSimpleDuration(t) + if err == nil { + *d = sd.Duration + return nil + } + + dd, err := time.ParseDuration(t) + if err != nil { + return err + } + + *d = Duration(dd) + + case float64: + *d = Duration(int64(t * float64(time.Second))) + + case int64: + *d = Duration(t * int64(time.Second)) + case int: + *d = Duration(t * int(time.Second)) + + default: + return fmt.Errorf("unsupported type %T value: %v", t, t) + + } + + return nil +} diff --git a/pkg/types/duration_test.go b/pkg/types/duration_test.go new file mode 100644 index 0000000000..44a56c80d7 --- /dev/null +++ b/pkg/types/duration_test.go @@ -0,0 +1,55 @@ +package types + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestParseSimpleDuration(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want *SimpleDuration + wantErr assert.ErrorAssertionFunc + }{ + { + name: "3h", + args: args{ + s: "3h", + }, + want: &SimpleDuration{Num: 3, Unit: "h", Duration: Duration(3 * time.Hour)}, + wantErr: assert.NoError, + }, + { + name: "3d", + args: args{ + s: "3d", + }, + want: &SimpleDuration{Num: 3, Unit: "d", Duration: Duration(3 * 24 * time.Hour)}, + wantErr: assert.NoError, + }, + { + name: "3w", + args: args{ + s: "3w", + }, + want: &SimpleDuration{Num: 3, Unit: "w", Duration: Duration(3 * 7 * 24 * time.Hour)}, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseSimpleDuration(tt.args.s) + if !tt.wantErr(t, err, fmt.Sprintf("ParseSimpleDuration(%v)", tt.args.s)) { + return + } + assert.Equalf(t, tt.want, got, "ParseSimpleDuration(%v)", tt.args.s) + }) + } +} diff --git a/pkg/types/market.go b/pkg/types/market.go index 23f3610ca5..6cc9466ff8 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -1,60 +1,13 @@ package types import ( - "encoding/json" - "fmt" "math" - "time" "github.com/leekchan/accounting" "github.com/c9s/bbgo/pkg/fixedpoint" ) -type Duration time.Duration - -func (d *Duration) Duration() time.Duration { - return time.Duration(*d) -} - -func (d *Duration) UnmarshalJSON(data []byte) error { - var o interface{} - - if err := json.Unmarshal(data, &o); err != nil { - return err - } - - switch t := o.(type) { - case string: - sd, err := ParseSimpleDuration(t) - if err == nil { - *d = sd.Duration - return nil - } - - dd, err := time.ParseDuration(t) - if err != nil { - return err - } - - *d = Duration(dd) - - case float64: - *d = Duration(int64(t * float64(time.Second))) - - case int64: - *d = Duration(t * int64(time.Second)) - case int: - *d = Duration(t * int(time.Second)) - - default: - return fmt.Errorf("unsupported type %T value: %v", t, t) - - } - - return nil -} - type Market struct { Symbol string `json:"symbol"` From 54b4f593ecb5e67a83b4d8865209956432f65718 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 00:29:31 +0800 Subject: [PATCH 0339/1392] grid2: validate upper price and lower price only when autoRange is not given --- pkg/strategy/grid2/strategy.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6dcca83e2c..549a937dd5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -65,7 +65,7 @@ type Strategy struct { // GridNum is the grid number, how many orders you want to post on the orderbook. GridNum int64 `json:"gridNumber"` - AutoRange types.SimpleDuration `json:"autoRange"` + AutoRange *types.SimpleDuration `json:"autoRange"` UpperPrice fixedpoint.Value `json:"upperPrice"` @@ -139,16 +139,18 @@ func (s *Strategy) ID() string { } func (s *Strategy) Validate() error { - if s.UpperPrice.IsZero() { - return errors.New("upperPrice can not be zero, you forgot to set?") - } + if s.AutoRange == nil { + if s.UpperPrice.IsZero() { + return errors.New("upperPrice can not be zero, you forgot to set?") + } - if s.LowerPrice.IsZero() { - return errors.New("lowerPrice can not be zero, you forgot to set?") - } + if s.LowerPrice.IsZero() { + return errors.New("lowerPrice can not be zero, you forgot to set?") + } - if s.UpperPrice.Compare(s.LowerPrice) <= 0 { - return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) + if s.UpperPrice.Compare(s.LowerPrice) <= 0 { + return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) + } } if s.GridNum == 0 { From 961725f03c8ee03bf35a38137b5a106acb083c78 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 00:56:03 +0800 Subject: [PATCH 0340/1392] grid2: support autoRange --- pkg/strategy/grid2/strategy.go | 16 +++++++++++++++- pkg/types/duration.go | 20 ++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 549a937dd5..bff7447d3c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -172,6 +172,11 @@ func (s *Strategy) Validate() error { func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) + + if s.AutoRange != nil { + interval := s.AutoRange.Interval() + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: interval}) + } } // InstanceID returns the instance identifier from the current grid configuration parameters @@ -1301,7 +1306,16 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. }) s.groupID = util.FNV32(instanceID) - s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + + if s.AutoRange != nil { + indicatorSet := session.StandardIndicatorSet(s.Symbol) + interval := s.AutoRange.Interval() + pivotLow := indicatorSet.PivotLow(types.IntervalWindow{Interval: interval, Window: s.AutoRange.Num}) + pivotHigh := indicatorSet.PivotHigh(types.IntervalWindow{Interval: interval, Window: s.AutoRange.Num}) + s.UpperPrice = fixedpoint.NewFromFloat(pivotHigh.Last()) + s.LowerPrice = fixedpoint.NewFromFloat(pivotLow.Last()) + s.logger.Infof("autoRange is enabled, using pivot high %f and pivot low %f", s.UpperPrice.Float64(), s.LowerPrice.Float64()) + } if s.ProfitSpread.Sign() > 0 { s.ProfitSpread = s.Market.TruncatePrice(s.ProfitSpread) diff --git a/pkg/types/duration.go b/pkg/types/duration.go index f465f24bd9..881cbe4d60 100644 --- a/pkg/types/duration.go +++ b/pkg/types/duration.go @@ -15,11 +15,27 @@ var simpleDurationRegExp = regexp.MustCompile("^(\\d+)([hdw])$") var ErrNotSimpleDuration = errors.New("the given input is not simple duration format, valid format: [1-9][0-9]*[hdw]") type SimpleDuration struct { - Num int64 + Num int Unit string Duration Duration } +func (d *SimpleDuration) Interval() Interval { + switch d.Unit { + + case "d": + return Interval1d + case "h": + return Interval1h + + case "w": + return Interval1w + + } + + return "" +} + func (d *SimpleDuration) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { @@ -49,7 +65,7 @@ func ParseSimpleDuration(s string) (*SimpleDuration, error) { matches := simpleDurationRegExp.FindStringSubmatch(s) numStr := matches[1] unit := matches[2] - num, err := strconv.ParseInt(numStr, 10, 64) + num, err := strconv.Atoi(numStr) if err != nil { return nil, err } From 24f0f40fad2e4cb45dbbdf3edb9b16d366086fdd Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 01:00:15 +0800 Subject: [PATCH 0341/1392] config: add autoRange config doc --- config/grid2.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/grid2.yaml b/config/grid2.yaml index aed8bc28da..18725fe9d5 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -55,6 +55,15 @@ exchangeStrategies: - on: binance grid2: symbol: BTCUSDT + + ## autoRange can be used to detect a price range from a specific time frame + ## the pivot low / pivot high of the given range will be used for lowerPrice and upperPrice. + ## when autoRange is set, it will override the upperPrice/lowerPrice settings. + ## + ## the valid format is [1-9][hdw] + ## example: "14d" means it will find the highest/lowest price that is higher/lower than left 14d and right 14d. + # autoRange: 14d + lowerPrice: 28_000.0 upperPrice: 50_000.0 From 0a6261b6b99be501afcca27f0f4a861eb0d1d1a0 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 01:04:17 +0800 Subject: [PATCH 0342/1392] grid2: split more files --- pkg/strategy/grid2/debug.go | 48 ++++++++++++++++++++++ pkg/strategy/grid2/pricemap.go | 16 ++++++++ pkg/strategy/grid2/strategy.go | 74 ---------------------------------- pkg/strategy/grid2/trade.go | 27 +++++++++++++ 4 files changed, 91 insertions(+), 74 deletions(-) create mode 100644 pkg/strategy/grid2/debug.go create mode 100644 pkg/strategy/grid2/pricemap.go create mode 100644 pkg/strategy/grid2/trade.go diff --git a/pkg/strategy/grid2/debug.go b/pkg/strategy/grid2/debug.go new file mode 100644 index 0000000000..56fbf92cb4 --- /dev/null +++ b/pkg/strategy/grid2/debug.go @@ -0,0 +1,48 @@ +package grid2 + +import ( + "fmt" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func debugGrid(grid *Grid, book *bbgo.ActiveOrderBook) { + fmt.Println("================== GRID ORDERS ==================") + + pins := grid.Pins + missingPins := scanMissingPinPrices(book, pins) + missing := len(missingPins) + + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + + fmt.Printf("%s -> ", price.String()) + + existingOrder := book.Lookup(func(o types.Order) bool { + return o.Price.Eq(price) + }) + + if existingOrder != nil { + fmt.Printf("%s", existingOrder.String()) + + switch existingOrder.Status { + case types.OrderStatusFilled: + fmt.Printf(" | 🔧") + case types.OrderStatusCanceled: + fmt.Printf(" | 🔄") + default: + fmt.Printf(" | ✅") + } + } else { + fmt.Printf("ORDER MISSING ⚠️ ") + if missing == 1 { + fmt.Printf(" COULD BE EMPTY SLOT") + } + } + fmt.Printf("\n") + } + fmt.Println("================== END OF GRID ORDERS ===================") +} diff --git a/pkg/strategy/grid2/pricemap.go b/pkg/strategy/grid2/pricemap.go new file mode 100644 index 0000000000..04ae59711c --- /dev/null +++ b/pkg/strategy/grid2/pricemap.go @@ -0,0 +1,16 @@ +package grid2 + +import "github.com/c9s/bbgo/pkg/fixedpoint" + +type PriceMap map[string]fixedpoint.Value + +func buildGridPriceMap(grid *Grid) PriceMap { + // Add all open orders to the local order book + gridPriceMap := make(PriceMap) + for _, pin := range grid.Pins { + price := fixedpoint.Value(pin) + gridPriceMap[price.String()] = price + } + + return gridPriceMap +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index bff7447d3c..5403b453ca 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -20,8 +20,6 @@ const ID = "grid2" const orderTag = "grid2" -type PriceMap map[string]fixedpoint.Value - var log = logrus.WithField("strategy", ID) var maxNumberOfOrderTradesQueryTries = 10 @@ -247,27 +245,6 @@ func (s *Strategy) calculateProfit(o types.Order, buyPrice, buyQuantity fixedpoi return profit } -// collectTradeFee collects the fee from the given trade slice -func collectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { - fees := make(map[string]fixedpoint.Value) - for _, t := range trades { - if fee, ok := fees[t.FeeCurrency]; ok { - fees[t.FeeCurrency] = fee.Add(t.Fee) - } else { - fees[t.FeeCurrency] = t.Fee - } - } - return fees -} - -func aggregateTradesQuantity(trades []types.Trade) fixedpoint.Value { - tq := fixedpoint.Zero - for _, t := range trades { - tq = tq.Add(t.Quantity) - } - return tq -} - func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { tq := aggregateTradesQuantity(trades) @@ -757,7 +734,6 @@ func (s *Strategy) newGrid() *Grid { // openGrid // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. -// TODO: fix sell order placement for profitSpread func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) error { // grid object guard if s.grid != nil { @@ -1005,17 +981,6 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { return nil } -func buildGridPriceMap(grid *Grid) PriceMap { - // Add all open orders to the local order book - gridPriceMap := make(PriceMap) - for _, pin := range grid.Pins { - price := fixedpoint.Value(pin) - gridPriceMap[price.String()] = price - } - - return gridPriceMap -} - func (s *Strategy) recoverGrid(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { grid := s.newGrid() @@ -1199,45 +1164,6 @@ func isCompleteGridOrderBook(orderBook *bbgo.ActiveOrderBook, gridNum int64) boo return false } -func debugGrid(grid *Grid, book *bbgo.ActiveOrderBook) { - fmt.Println("================== GRID ORDERS ==================") - - pins := grid.Pins - missingPins := scanMissingPinPrices(book, pins) - missing := len(missingPins) - - for i := len(pins) - 1; i >= 0; i-- { - pin := pins[i] - price := fixedpoint.Value(pin) - - fmt.Printf("%s -> ", price.String()) - - existingOrder := book.Lookup(func(o types.Order) bool { - return o.Price.Eq(price) - }) - - if existingOrder != nil { - fmt.Printf("%s", existingOrder.String()) - - switch existingOrder.Status { - case types.OrderStatusFilled: - fmt.Printf(" | 🔧") - case types.OrderStatusCanceled: - fmt.Printf(" | 🔄") - default: - fmt.Printf(" | ✅") - } - } else { - fmt.Printf("ORDER MISSING ⚠️ ") - if missing == 1 { - fmt.Printf(" COULD BE EMPTY SLOT") - } - } - fmt.Printf("\n") - } - fmt.Println("================== END OF GRID ORDERS ===================") -} - func findEarliestOrderID(orders []types.Order) (uint64, bool) { if len(orders) == 0 { return 0, false diff --git a/pkg/strategy/grid2/trade.go b/pkg/strategy/grid2/trade.go new file mode 100644 index 0000000000..381744ccd6 --- /dev/null +++ b/pkg/strategy/grid2/trade.go @@ -0,0 +1,27 @@ +package grid2 + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// collectTradeFee collects the fee from the given trade slice +func collectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { + fees := make(map[string]fixedpoint.Value) + for _, t := range trades { + if fee, ok := fees[t.FeeCurrency]; ok { + fees[t.FeeCurrency] = fee.Add(t.Fee) + } else { + fees[t.FeeCurrency] = t.Fee + } + } + return fees +} + +func aggregateTradesQuantity(trades []types.Trade) fixedpoint.Value { + tq := fixedpoint.Zero + for _, t := range trades { + tq = tq.Add(t.Quantity) + } + return tq +} From 6444fd5e03998e02b203c82361d68398456979c2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 01:24:56 +0800 Subject: [PATCH 0343/1392] grid2: remove default profit stats --- pkg/strategy/grid2/strategy.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5403b453ca..c601370761 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -115,9 +115,8 @@ type Strategy struct { SkipSpreadCheck bool `json:"skipSpreadCheck"` - GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"` - ProfitStats *types.ProfitStats `persistence:"profit_stats"` - Position *types.Position `persistence:"position"` + GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"` + Position *types.Position `persistence:"position"` grid *Grid session *bbgo.ExchangeSession @@ -1251,10 +1250,6 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.GridProfitStats = newGridProfitStats(s.Market) } - if s.ProfitStats == nil { - s.ProfitStats = types.NewProfitStats(s.Market) - } - if s.Position == nil { s.Position = types.NewPositionFromMarket(s.Market) } @@ -1276,7 +1271,6 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) orderExecutor.BindEnvironment(s.Environment) - orderExecutor.BindProfitStats(s.ProfitStats) orderExecutor.Bind() orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { s.GridProfitStats.AddTrade(trade) From 8af7d6f4571a256d1580e5f66f387e0561d4d982 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 01:35:37 +0800 Subject: [PATCH 0344/1392] grid2: use initial grid order id to query closed order history --- pkg/strategy/grid2/profit_stats.go | 1 + pkg/strategy/grid2/strategy.go | 32 +++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/grid2/profit_stats.go b/pkg/strategy/grid2/profit_stats.go index 40001369ed..df88af1a72 100644 --- a/pkg/strategy/grid2/profit_stats.go +++ b/pkg/strategy/grid2/profit_stats.go @@ -24,6 +24,7 @@ type GridProfitStats struct { Market types.Market `json:"market,omitempty"` ProfitEntries []*GridProfit `json:"profitEntries,omitempty"` Since *time.Time `json:"since,omitempty"` + InitialOrderID uint64 `json:"initialOrderID"` } func newGridProfitStats(market types.Market) *GridProfitStats { diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c601370761..a1a276397a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -3,6 +3,7 @@ package grid2 import ( "context" "fmt" + "sort" "strconv" "sync" "time" @@ -819,10 +820,23 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return err } + var orderIds []uint64 + for _, order := range createdOrders { + orderIds = append(orderIds, order.OrderID) + s.logger.Info(order.String()) } + sort.Slice(orderIds, func(i, j int) bool { + return orderIds[i] < orderIds[j] + }) + + if len(orderIds) > 0 { + s.GridProfitStats.InitialOrderID = orderIds[0] + bbgo.Sync(ctx, s) + } + s.logger.Infof("ALL GRID ORDERS SUBMITTED") return nil } @@ -997,8 +1011,12 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang _ = lastOrderTime // for MAX exchange we need the order ID to query the closed order history - if oid, ok := findEarliestOrderID(openOrders); ok { - lastOrderID = oid + if s.GridProfitStats != nil && s.GridProfitStats.InitialOrderID > 0 { + lastOrderID = s.GridProfitStats.InitialOrderID + } else { + if oid, ok := findEarliestOrderID(openOrders); ok { + lastOrderID = oid + } } activeOrderBook := s.orderExecutor.ActiveMakerOrders() @@ -1332,14 +1350,14 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. session.MarketDataStream.OnKLineClosed(s.newTakeProfitHandler(ctx, session)) } - session.UserDataStream.OnStart(func() { - if s.TriggerPrice.IsZero() { + // if TriggerPrice is zero, that means we need to open the grid when start up + if s.TriggerPrice.IsZero() { + session.UserDataStream.OnStart(func() { if err := s.openGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("failed to setup grid orders") } - return - } - }) + }) + } return nil } From 2d2d194bda65b6eb32c4a8fa17f9829eb893a889 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 01:40:59 +0800 Subject: [PATCH 0345/1392] grid2: fix InstanceID for autoRange --- pkg/strategy/grid2/strategy.go | 10 +++++++++- pkg/types/duration.go | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index a1a276397a..2182eb506a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -179,7 +179,15 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // InstanceID returns the instance identifier from the current grid configuration parameters func (s *Strategy) InstanceID() string { - return fmt.Sprintf("%s-%s-%d-%d-%d", ID, s.Symbol, s.GridNum, s.UpperPrice.Int(), s.LowerPrice.Int()) + id := fmt.Sprintf("%s-%s-size-%d", ID, s.Symbol, s.GridNum) + + if s.AutoRange != nil { + id += "-autoRange-" + s.AutoRange.String() + } else { + id += "-" + s.UpperPrice.String() + "-" + s.LowerPrice.String() + } + + return id } func (s *Strategy) checkSpread() error { diff --git a/pkg/types/duration.go b/pkg/types/duration.go index 881cbe4d60..c4ff6632f5 100644 --- a/pkg/types/duration.go +++ b/pkg/types/duration.go @@ -20,6 +20,10 @@ type SimpleDuration struct { Duration Duration } +func (d *SimpleDuration) String() string { + return fmt.Sprintf("%d%s", d.Num, d.Unit) +} + func (d *SimpleDuration) Interval() Interval { switch d.Unit { From e9ff0dcc66181cd32f997418d144411a449aa233 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 01:49:46 +0800 Subject: [PATCH 0346/1392] types: fix lint issue --- pkg/types/duration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/types/duration.go b/pkg/types/duration.go index c4ff6632f5..be3a35a965 100644 --- a/pkg/types/duration.go +++ b/pkg/types/duration.go @@ -10,7 +10,7 @@ import ( "github.com/pkg/errors" ) -var simpleDurationRegExp = regexp.MustCompile("^(\\d+)([hdw])$") +var simpleDurationRegExp = regexp.MustCompile(`^(\d+)([hdw])$`) var ErrNotSimpleDuration = errors.New("the given input is not simple duration format, valid format: [1-9][0-9]*[hdw]") From ecf5ed3c853f3923d1ae24662c0cd7838c2add02 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 01:50:25 +0800 Subject: [PATCH 0347/1392] remove empty render.go --- pkg/util/render.go | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 pkg/util/render.go diff --git a/pkg/util/render.go b/pkg/util/render.go deleted file mode 100644 index a5e4c54faa..0000000000 --- a/pkg/util/render.go +++ /dev/null @@ -1,2 +0,0 @@ -package util - From c07c3c62a99ea0d855abd40306530b385926f785 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 01:51:32 +0800 Subject: [PATCH 0348/1392] util: remove unused NotZero funcs --- pkg/util/math.go | 32 -------------------------------- pkg/util/math_test.go | 35 ----------------------------------- 2 files changed, 67 deletions(-) diff --git a/pkg/util/math.go b/pkg/util/math.go index bf73885e70..1159ff59d6 100644 --- a/pkg/util/math.go +++ b/pkg/util/math.go @@ -1,7 +1,6 @@ package util import ( - "math" "strconv" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -27,34 +26,3 @@ func FormatValue(val fixedpoint.Value, prec int) string { func FormatFloat(val float64, prec int) string { return strconv.FormatFloat(val, 'f', prec, 64) } - -func ParseFloat(s string) (float64, error) { - if len(s) == 0 { - return 0.0, nil - } - - return strconv.ParseFloat(s, 64) -} - -func MustParseFloat(s string) float64 { - if len(s) == 0 { - return 0.0 - } - - v, err := strconv.ParseFloat(s, 64) - if err != nil { - panic(err) - } - return v -} - -const epsilon = 0.0000001 - -func Zero(v float64) bool { - return math.Abs(v) < epsilon -} - -func NotZero(v float64) bool { - return math.Abs(v) > epsilon -} - diff --git a/pkg/util/math_test.go b/pkg/util/math_test.go index 96d96a3015..c7d868219f 100644 --- a/pkg/util/math_test.go +++ b/pkg/util/math_test.go @@ -1,36 +1 @@ package util - -import "testing" - -func TestNotZero(t *testing.T) { - type args struct { - v float64 - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "0", - args: args{ - v: 0, - }, - want: false, - }, - { - name: "0.00001", - args: args{ - v: 0.00001, - }, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := NotZero(tt.args.v); got != tt.want { - t.Errorf("NotZero() = %v, want %v", got, tt.want) - } - }) - } -} From a66cee91309985f06f2a360f5b43ec3012807d78 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 16:05:21 +0800 Subject: [PATCH 0349/1392] util: remove unused func --- pkg/strategy/xbalance/strategy.go | 2 +- pkg/util/math.go | 28 ---------------------------- 2 files changed, 1 insertion(+), 29 deletions(-) delete mode 100644 pkg/util/math.go diff --git a/pkg/strategy/xbalance/strategy.go b/pkg/strategy/xbalance/strategy.go index 7175382467..8499dd4287 100644 --- a/pkg/strategy/xbalance/strategy.go +++ b/pkg/strategy/xbalance/strategy.go @@ -52,7 +52,7 @@ func (s *State) SlackAttachment() slack.Attachment { Title: s.Asset + " Transfer States", Fields: []slack.AttachmentField{ {Title: "Total Number of Transfers", Value: fmt.Sprintf("%d", s.DailyNumberOfTransfers), Short: true}, - {Title: "Total Amount of Transfers", Value: util.FormatFloat(s.DailyAmountOfTransfers.Float64(), 4), Short: true}, + {Title: "Total Amount of Transfers", Value: s.DailyAmountOfTransfers.String(), Short: true}, }, Footer: templateutil.Render("Since {{ . }}", time.Unix(s.Since, 0).Format(time.RFC822)), } diff --git a/pkg/util/math.go b/pkg/util/math.go deleted file mode 100644 index 1159ff59d6..0000000000 --- a/pkg/util/math.go +++ /dev/null @@ -1,28 +0,0 @@ -package util - -import ( - "strconv" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -const MaxDigits = 18 // MAX_INT64 ~ 9 * 10^18 - -var Pow10Table = [MaxDigits + 1]int64{ - 1, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, -} - -func Pow10(n int64) int64 { - if n < 0 || n > MaxDigits { - return 0 - } - return Pow10Table[n] -} - -func FormatValue(val fixedpoint.Value, prec int) string { - return val.FormatString(prec) -} - -func FormatFloat(val float64, prec int) string { - return strconv.FormatFloat(val, 'f', prec, 64) -} From a4f5d1533465804c5f921712574eb43ac13e9e11 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 18:05:35 +0800 Subject: [PATCH 0350/1392] grid2: adjust rollback duration to twice --- pkg/strategy/grid2/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2182eb506a..e29a1b75b6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1055,6 +1055,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang startTime := firstOrderTime endTime := now maxTries := 3 + localHistoryRollbackDuration := historyRollbackDuration for maxTries > 0 { maxTries-- if err := s.replayOrderHistory(ctx, grid, orderBook, historyService, startTime, endTime, lastOrderID); err != nil { @@ -1069,12 +1070,13 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang } // history rollback range - startTime = startTime.Add(-historyRollbackDuration) + startTime = startTime.Add(-localHistoryRollbackDuration) if newFromOrderID := lastOrderID - historyRollbackOrderIdRange; newFromOrderID > 1 { lastOrderID = newFromOrderID } s.logger.Infof("GRID RECOVER: there are still more than two missing orders, rolling back query start time to earlier time point %s, fromID %d", startTime.String(), lastOrderID) + localHistoryRollbackDuration = localHistoryRollbackDuration * 2 } } From d9312abba2095736302730b2e7e64e9899dbf420 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 18:08:36 +0800 Subject: [PATCH 0351/1392] grid2: adjust maxTries to 5 --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e29a1b75b6..cac6d4d54f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1054,7 +1054,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang // TODO: handle context correctly startTime := firstOrderTime endTime := now - maxTries := 3 + maxTries := 5 localHistoryRollbackDuration := historyRollbackDuration for maxTries > 0 { maxTries-- From c9a70d98978622002745d973f1ff04893acc627d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Dec 2022 18:15:39 +0800 Subject: [PATCH 0352/1392] grid2: add grid callbacks --- pkg/strategy/grid2/strategy.go | 8 ++++++ pkg/strategy/grid2/strategy_callbacks.go | 35 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 pkg/strategy/grid2/strategy_callbacks.go diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index cac6d4d54f..f5ab0ee3a6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -43,6 +43,7 @@ type OrderExecutor interface { ActiveMakerOrders() *bbgo.ActiveOrderBook } +//go:generate callbackgen -type Strategy type Strategy struct { Environment *bbgo.Environment @@ -130,6 +131,10 @@ type Strategy struct { groupID uint32 logger *logrus.Entry + + gridReadyCallbacks []func() + gridProfitCallbacks []func(stats *GridProfitStats, profit *GridProfit) + gridClosedCallbacks []func() } func (s *Strategy) ID() string { @@ -351,6 +356,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) s.GridProfitStats.AddProfit(profit) + s.EmitGridProfit(s.GridProfitStats, profit) bbgo.Notify(profit) bbgo.Notify(s.GridProfitStats) @@ -730,6 +736,7 @@ func (s *Strategy) closeGrid(ctx context.Context) error { // free the grid object s.grid = nil + s.EmitGridClosed() return nil } @@ -846,6 +853,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) } s.logger.Infof("ALL GRID ORDERS SUBMITTED") + s.EmitGridReady() return nil } diff --git a/pkg/strategy/grid2/strategy_callbacks.go b/pkg/strategy/grid2/strategy_callbacks.go new file mode 100644 index 0000000000..72e6597792 --- /dev/null +++ b/pkg/strategy/grid2/strategy_callbacks.go @@ -0,0 +1,35 @@ +// Code generated by "callbackgen -type Strategy"; DO NOT EDIT. + +package grid2 + +import () + +func (s *Strategy) OnGridReady(cb func()) { + s.gridReadyCallbacks = append(s.gridReadyCallbacks, cb) +} + +func (s *Strategy) EmitGridReady() { + for _, cb := range s.gridReadyCallbacks { + cb() + } +} + +func (s *Strategy) OnGridProfit(cb func(stats *GridProfitStats, profit *GridProfit)) { + s.gridProfitCallbacks = append(s.gridProfitCallbacks, cb) +} + +func (s *Strategy) EmitGridProfit(stats *GridProfitStats, profit *GridProfit) { + for _, cb := range s.gridProfitCallbacks { + cb(stats, profit) + } +} + +func (s *Strategy) OnGridClosed(cb func()) { + s.gridClosedCallbacks = append(s.gridClosedCallbacks, cb) +} + +func (s *Strategy) EmitGridClosed() { + for _, cb := range s.gridClosedCallbacks { + cb() + } +} From a238da3dc408d4699c6ad7205a43d9ef7d91552c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Wed, 28 Dec 2022 17:15:30 +0800 Subject: [PATCH 0353/1392] create log dir to avoid error --- pkg/cmd/root.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index b156842627..3253bea76e 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -10,14 +10,14 @@ import ( "github.com/heroku/rollrus" "github.com/joho/godotenv" - "github.com/lestrrat-go/file-rotatelogs" + rotatelogs "github.com/lestrrat-go/file-rotatelogs" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rifflock/lfshook" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/x-cray/logrus-prefixed-formatter" + prefixed "github.com/x-cray/logrus-prefixed-formatter" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/util" @@ -219,11 +219,14 @@ func init() { func Execute() { environment := os.Getenv("BBGO_ENV") + logDir := "log" switch environment { case "production", "prod": - + if err := os.MkdirAll(logDir, 0777); err != nil { + log.Panic(err) + } writer, err := rotatelogs.New( - path.Join("log", "access_log.%Y%m%d"), + path.Join(logDir, "access_log.%Y%m%d"), rotatelogs.WithLinkName("access_log"), // rotatelogs.WithMaxAge(24 * time.Hour), rotatelogs.WithRotationTime(time.Duration(24)*time.Hour), From f03cc3c5dee9c50da155618cb8dcf703f0694b10 Mon Sep 17 00:00:00 2001 From: Yo-An Lin Date: Fri, 30 Dec 2022 07:47:09 +0800 Subject: [PATCH 0354/1392] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d04b7ccd7..25806e1b0a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # BBGO -A crypto trading bot framework written in Go. The name bbgo comes from the BB8 bot in the Star Wars movie. +A modern crypto trading bot framework written in Go. ## Current Status From 5ccdab34be05b39f270894e873f1538246c2ef5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= Date: Thu, 5 Jan 2023 18:36:09 +0800 Subject: [PATCH 0355/1392] add RSI to StandardIndicatorSet --- pkg/bbgo/standard_indicator_set.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/bbgo/standard_indicator_set.go b/pkg/bbgo/standard_indicator_set.go index e291cccc6f..3f74591d0e 100644 --- a/pkg/bbgo/standard_indicator_set.go +++ b/pkg/bbgo/standard_indicator_set.go @@ -167,6 +167,11 @@ func (s *StandardIndicatorSet) MACD(iw types.IntervalWindow, shortPeriod, longPe return inc } +func (s *StandardIndicatorSet) RSI(iw types.IntervalWindow) *indicator.RSI { + inc := s.allocateSimpleIndicator(&indicator.RSI{IntervalWindow: iw}, iw, "rsi") + return inc.(*indicator.RSI) +} + // GHFilter is a helper function that returns the G-H (alpha beta) digital filter of the given interval and the window size. func (s *StandardIndicatorSet) GHFilter(iw types.IntervalWindow) *indicator.GHFilter { inc := s.allocateSimpleIndicator(&indicator.GHFilter{IntervalWindow: iw}, iw, "ghfilter") From 5765969573a21bab72789b6e1679fcac12dc7d42 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 5 Jan 2023 19:07:15 +0800 Subject: [PATCH 0356/1392] service: add redis namespace support --- pkg/service/persistence.go | 9 +++++---- pkg/service/persistence_redis.go | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pkg/service/persistence.go b/pkg/service/persistence.go index e8cb47c359..a4a08356cd 100644 --- a/pkg/service/persistence.go +++ b/pkg/service/persistence.go @@ -11,10 +11,11 @@ type Store interface { } type RedisPersistenceConfig struct { - Host string `yaml:"host" json:"host" env:"REDIS_HOST"` - Port string `yaml:"port" json:"port" env:"REDIS_PORT"` - Password string `yaml:"password,omitempty" json:"password,omitempty" env:"REDIS_PASSWORD"` - DB int `yaml:"db" json:"db" env:"REDIS_DB"` + Host string `yaml:"host" json:"host" env:"REDIS_HOST"` + Port string `yaml:"port" json:"port" env:"REDIS_PORT"` + Password string `yaml:"password,omitempty" json:"password,omitempty" env:"REDIS_PASSWORD"` + DB int `yaml:"db" json:"db" env:"REDIS_DB"` + Namespace string `yaml:"namespace" json:"namespace" env:"REDIS_NAMESPACE"` } type JsonPersistenceConfig struct { diff --git a/pkg/service/persistence_redis.go b/pkg/service/persistence_redis.go index 6b91d05832..b7171e4e47 100644 --- a/pkg/service/persistence_redis.go +++ b/pkg/service/persistence_redis.go @@ -11,8 +11,13 @@ import ( log "github.com/sirupsen/logrus" ) +var redisLogger = log.WithFields(log.Fields{ + "persistence": "redis", +}) + type RedisPersistenceService struct { - redis *redis.Client + redis *redis.Client + config *RedisPersistenceConfig } func NewRedisPersistenceService(config *RedisPersistenceConfig) *RedisPersistenceService { @@ -25,7 +30,8 @@ func NewRedisPersistenceService(config *RedisPersistenceConfig) *RedisPersistenc }) return &RedisPersistenceService{ - redis: client, + redis: client, + config: config, } } @@ -34,6 +40,10 @@ func (s *RedisPersistenceService) NewStore(id string, subIDs ...string) Store { id += ":" + strings.Join(subIDs, ":") } + if s.config != nil && s.config.Namespace != "" { + id = s.config.Namespace + ":" + id + } + return &RedisStore{ redis: s.redis, ID: id, @@ -54,7 +64,7 @@ func (store *RedisStore) Load(val interface{}) error { cmd := store.redis.Get(context.Background(), store.ID) data, err := cmd.Result() - log.Debugf("[redis] get key %q, data = %s", store.ID, string(data)) + redisLogger.Debugf("[redis] get key %q, data = %s", store.ID, string(data)) if err != nil { if err == redis.Nil { @@ -85,7 +95,7 @@ func (store *RedisStore) Save(val interface{}) error { cmd := store.redis.Set(context.Background(), store.ID, data, 0) _, err = cmd.Result() - log.Debugf("[redis] set key %q, data = %s", store.ID, string(data)) + redisLogger.Debugf("[redis] set key %q, data = %s", store.ID, string(data)) return err } From 857b5d0f3074f595bdca300ae7b569c6ef962edf Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 10 Jan 2023 20:15:51 +0800 Subject: [PATCH 0357/1392] grid2: integrate prometheus metrics --- pkg/bbgo/activeorderbook.go | 6 +- pkg/bbgo/activeorderbook_callbacks.go | 10 +++ pkg/strategy/grid2/metrics.go | 77 +++++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 78 +++++++++++++++++++++++- pkg/strategy/grid2/strategy_callbacks.go | 10 +++ 5 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 pkg/strategy/grid2/metrics.go diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 78ed67fb29..3558a54737 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -18,8 +18,10 @@ const CancelOrderWaitTime = 20 * time.Millisecond // ActiveOrderBook manages the local active order books. //go:generate callbackgen -type ActiveOrderBook type ActiveOrderBook struct { - Symbol string - orders *types.SyncOrderMap + Symbol string + orders *types.SyncOrderMap + + newCallbacks []func(o types.Order) filledCallbacks []func(o types.Order) canceledCallbacks []func(o types.Order) diff --git a/pkg/bbgo/activeorderbook_callbacks.go b/pkg/bbgo/activeorderbook_callbacks.go index 014fd6a59b..c75be96ef4 100644 --- a/pkg/bbgo/activeorderbook_callbacks.go +++ b/pkg/bbgo/activeorderbook_callbacks.go @@ -6,6 +6,16 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +func (b *ActiveOrderBook) OnNew(cb func(o types.Order)) { + b.newCallbacks = append(b.newCallbacks, cb) +} + +func (b *ActiveOrderBook) EmitNew(o types.Order) { + for _, cb := range b.newCallbacks { + cb(o) + } +} + func (b *ActiveOrderBook) OnFilled(cb func(o types.Order)) { b.filledCallbacks = append(b.filledCallbacks, cb) } diff --git a/pkg/strategy/grid2/metrics.go b/pkg/strategy/grid2/metrics.go new file mode 100644 index 0000000000..82d56200af --- /dev/null +++ b/pkg/strategy/grid2/metrics.go @@ -0,0 +1,77 @@ +package grid2 + +import "github.com/prometheus/client_golang/prometheus" + +var ( + metricsGridNumOfOrders *prometheus.GaugeVec + metricsGridOrderPrices *prometheus.GaugeVec + metricsGridProfit *prometheus.GaugeVec +) + +func labelKeys(labels prometheus.Labels) []string { + var keys []string + for k := range labels { + keys = append(keys, k) + } + + return keys +} + +func mergeLabels(a, b prometheus.Labels) prometheus.Labels { + labels := prometheus.Labels{} + for k, v := range a { + labels[k] = v + } + + for k, v := range b { + labels[k] = v + } + return labels +} + +func initMetrics(extendedLabels []string) { + metricsGridNumOfOrders = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_num_of_orders", + Help: "number of orders", + }, + append([]string{ + "strategy_instance", + "exchange", // exchange name + "symbol", // symbol of the market + }, extendedLabels...), + ) + + metricsGridOrderPrices = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_order_prices", + Help: "order prices", + }, + append([]string{ + "strategy_instance", + "exchange", // exchange name + "symbol", // symbol of the market + "ith", + }, extendedLabels...), + ) + + metricsGridProfit = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_grid_profit", + Help: "realized grid profit", + }, + append([]string{ + "strategy_instance", + "exchange", // exchange name + "symbol", // symbol of the market + }, extendedLabels...), + ) +} + +func registerMetrics() { + prometheus.MustRegister( + metricsGridNumOfOrders, + metricsGridProfit, + metricsGridOrderPrices, + ) +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f5ab0ee3a6..96d54a0c5e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -9,6 +9,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" @@ -111,6 +112,13 @@ type Strategy struct { ResetPositionWhenStart bool `json:"resetPositionWhenStart"` + // StrategyInstance + // an optional field for prometheus metrics + StrategyInstance string `json:"strategyInstance"` + + // PrometheusLabels will be used as the base prometheus labels + PrometheusLabels prometheus.Labels `json:"prometheusLabels"` + // FeeRate is used for calculating the minimal profit spread. // it makes sure that your grid configuration is profitable. FeeRate fixedpoint.Value `json:"feeRate"` @@ -135,6 +143,7 @@ type Strategy struct { gridReadyCallbacks []func() gridProfitCallbacks []func(stats *GridProfitStats, profit *GridProfit) gridClosedCallbacks []func() + gridErrorCallbacks []func(err error) } func (s *Strategy) ID() string { @@ -173,6 +182,13 @@ func (s *Strategy) Validate() error { return nil } +func (s *Strategy) Defaults() error { + if s.StrategyInstance == "" { + s.StrategyInstance = "main" + } + return nil +} + func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) @@ -355,10 +371,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { profit := s.calculateProfit(o, newPrice, newQuantity) s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) s.GridProfitStats.AddProfit(profit) - s.EmitGridProfit(s.GridProfitStats, profit) - bbgo.Notify(profit) - bbgo.Notify(s.GridProfitStats) case types.SideTypeBuy: newSide = types.SideTypeSell @@ -668,6 +681,8 @@ func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.Exch return func(o types.Order) { s.handleOrderFilled(o) bbgo.Sync(ctx, s) + + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) } } @@ -835,6 +850,10 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return err } + // update the number of orders to metrics + baseLabels := s.newPrometheusLabels() + metricsGridNumOfOrders.With(baseLabels).Set(float64(len(createdOrders))) + var orderIds []uint64 for _, order := range createdOrders { @@ -854,9 +873,30 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) s.logger.Infof("ALL GRID ORDERS SUBMITTED") s.EmitGridReady() + + s.updateOpenOrderPricesMetrics(createdOrders) return nil } +func (s *Strategy) updateOpenOrderPricesMetrics(orders []types.Order) { + orders = sortOrdersByPriceAscending(orders) + num := len(orders) + for idx, order := range orders { + labels := s.newPrometheusLabels() + labels["ith"] = strconv.Itoa(num - idx) + metricsGridOrderPrices.With(labels).Set(order.Price.Float64()) + } +} + +func sortOrdersByPriceAscending(orders []types.Order) []types.Order { + sort.Slice(orders, func(i, j int) bool { + a := orders[i] + b := orders[j] + return a.Price.Compare(b.Price) < 0 + }) + return orders +} + func (s *Strategy) debugGridOrders(submitOrders []types.SubmitOrder, lastPrice fixedpoint.Value) { s.logger.Infof("GRID ORDERS: [") for i, order := range submitOrders { @@ -1253,6 +1293,20 @@ func scanMissingPinPrices(orderBook *bbgo.ActiveOrderBook, pins []Pin) PriceMap return missingPrices } +func (s *Strategy) newPrometheusLabels() prometheus.Labels { + labels := prometheus.Labels{ + "strategy_instance": s.StrategyInstance, + "exchange": s.session.Name, + "symbol": s.Symbol, + } + + if s.PrometheusLabels == nil { + return labels + } + + return mergeLabels(s.PrometheusLabels, labels) +} + func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() @@ -1290,6 +1344,14 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.Position = types.NewPositionFromMarket(s.Market) } + // initialize and register prometheus metrics + if s.PrometheusLabels != nil { + initMetrics(labelKeys(s.PrometheusLabels)) + } else { + initMetrics(nil) + } + registerMetrics() + if s.ResetPositionWhenStart { s.Position.Reset() } @@ -1318,6 +1380,16 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.orderExecutor = orderExecutor + s.OnGridProfit(func(stats *GridProfitStats, profit *GridProfit) { + bbgo.Notify(profit) + bbgo.Notify(stats) + }) + + s.OnGridProfit(func(stats *GridProfitStats, profit *GridProfit) { + labels := s.newPrometheusLabels() + metricsGridProfit.With(labels).Set(stats.TotalQuoteProfit.Float64()) + }) + // TODO: detect if there are previous grid orders on the order book if s.ClearOpenOrdersWhenStart { if err := s.clearOpenOrders(ctx, session); err != nil { diff --git a/pkg/strategy/grid2/strategy_callbacks.go b/pkg/strategy/grid2/strategy_callbacks.go index 72e6597792..18caeed095 100644 --- a/pkg/strategy/grid2/strategy_callbacks.go +++ b/pkg/strategy/grid2/strategy_callbacks.go @@ -33,3 +33,13 @@ func (s *Strategy) EmitGridClosed() { cb() } } + +func (s *Strategy) OnGridError(cb func(err error)) { + s.gridErrorCallbacks = append(s.gridErrorCallbacks, cb) +} + +func (s *Strategy) EmitGridError(err error) { + for _, cb := range s.gridErrorCallbacks { + cb(err) + } +} From 75919a0bf1cda8e4fcd12d2587a4e38c4e7c234d Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 10 Jan 2023 21:21:35 +0800 Subject: [PATCH 0358/1392] grid2: reset metricsGridOrderPrices --- pkg/strategy/grid2/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 96d54a0c5e..40375536ae 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -881,6 +881,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) func (s *Strategy) updateOpenOrderPricesMetrics(orders []types.Order) { orders = sortOrdersByPriceAscending(orders) num := len(orders) + metricsGridOrderPrices.Reset() for idx, order := range orders { labels := s.newPrometheusLabels() labels["ith"] = strconv.Itoa(num - idx) From 0d47afd5fd87e882728fce1e8747f861577cfe5f Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 10 Jan 2023 21:41:10 +0800 Subject: [PATCH 0359/1392] grid2: add order side to the metrics label --- pkg/strategy/grid2/metrics.go | 1 + pkg/strategy/grid2/strategy.go | 1 + 2 files changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/metrics.go b/pkg/strategy/grid2/metrics.go index 82d56200af..adf37bad36 100644 --- a/pkg/strategy/grid2/metrics.go +++ b/pkg/strategy/grid2/metrics.go @@ -52,6 +52,7 @@ func initMetrics(extendedLabels []string) { "exchange", // exchange name "symbol", // symbol of the market "ith", + "side", }, extendedLabels...), ) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 40375536ae..0e0662d886 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -884,6 +884,7 @@ func (s *Strategy) updateOpenOrderPricesMetrics(orders []types.Order) { metricsGridOrderPrices.Reset() for idx, order := range orders { labels := s.newPrometheusLabels() + labels["side"] = order.Side.String() labels["ith"] = strconv.Itoa(num - idx) metricsGridOrderPrices.With(labels).Set(order.Price.Float64()) } From 668bf2d8479bb1ac080fab9cf675eb3e644e9594 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 11 Jan 2023 00:47:36 +0800 Subject: [PATCH 0360/1392] grid2: remove strategyInstance since we have custom labels --- pkg/strategy/grid2/metrics.go | 3 --- pkg/strategy/grid2/strategy.go | 12 ++---------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/pkg/strategy/grid2/metrics.go b/pkg/strategy/grid2/metrics.go index adf37bad36..1fdeb23b44 100644 --- a/pkg/strategy/grid2/metrics.go +++ b/pkg/strategy/grid2/metrics.go @@ -36,7 +36,6 @@ func initMetrics(extendedLabels []string) { Help: "number of orders", }, append([]string{ - "strategy_instance", "exchange", // exchange name "symbol", // symbol of the market }, extendedLabels...), @@ -48,7 +47,6 @@ func initMetrics(extendedLabels []string) { Help: "order prices", }, append([]string{ - "strategy_instance", "exchange", // exchange name "symbol", // symbol of the market "ith", @@ -62,7 +60,6 @@ func initMetrics(extendedLabels []string) { Help: "realized grid profit", }, append([]string{ - "strategy_instance", "exchange", // exchange name "symbol", // symbol of the market }, extendedLabels...), diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 0e0662d886..041bcabfc5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -112,10 +112,6 @@ type Strategy struct { ResetPositionWhenStart bool `json:"resetPositionWhenStart"` - // StrategyInstance - // an optional field for prometheus metrics - StrategyInstance string `json:"strategyInstance"` - // PrometheusLabels will be used as the base prometheus labels PrometheusLabels prometheus.Labels `json:"prometheusLabels"` @@ -183,9 +179,6 @@ func (s *Strategy) Validate() error { } func (s *Strategy) Defaults() error { - if s.StrategyInstance == "" { - s.StrategyInstance = "main" - } return nil } @@ -1297,9 +1290,8 @@ func scanMissingPinPrices(orderBook *bbgo.ActiveOrderBook, pins []Pin) PriceMap func (s *Strategy) newPrometheusLabels() prometheus.Labels { labels := prometheus.Labels{ - "strategy_instance": s.StrategyInstance, - "exchange": s.session.Name, - "symbol": s.Symbol, + "exchange": s.session.Name, + "symbol": s.Symbol, } if s.PrometheusLabels == nil { From 46eb590a9f5ad3cc44e0ee469b7d65abd559ac3a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 12 Jan 2023 14:33:09 +0800 Subject: [PATCH 0361/1392] grid2: OpenGrid, CloseGrid --- pkg/strategy/grid2/strategy.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 041bcabfc5..6746fca0f1 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -236,7 +236,7 @@ func (s *Strategy) handleOrderCanceled(o types.Order) { ctx := context.Background() if s.CloseWhenCancelOrder { s.logger.Infof("one of the grid orders is canceled, now closing grid...") - if err := s.closeGrid(ctx); err != nil { + if err := s.CloseGrid(ctx); err != nil { s.logger.WithError(err).Errorf("graceful order cancel error") } } @@ -687,7 +687,7 @@ func (s *Strategy) newStopLossPriceHandler(ctx context.Context, session *bbgo.Ex s.logger.Infof("last low price %f hits stopLossPrice %f, closing grid", k.Low.Float64(), s.StopLossPrice.Float64()) - if err := s.closeGrid(ctx); err != nil { + if err := s.CloseGrid(ctx); err != nil { s.logger.WithError(err).Errorf("can not close grid") return } @@ -713,7 +713,7 @@ func (s *Strategy) newTakeProfitHandler(ctx context.Context, session *bbgo.Excha s.logger.Infof("last high price %f hits takeProfitPrice %f, closing grid", k.High.Float64(), s.TakeProfitPrice.Float64()) - if err := s.closeGrid(ctx); err != nil { + if err := s.CloseGrid(ctx); err != nil { s.logger.WithError(err).Errorf("can not close grid") return } @@ -731,8 +731,12 @@ func (s *Strategy) newTakeProfitHandler(ctx context.Context, session *bbgo.Excha }) } -// closeGrid closes the grid orders -func (s *Strategy) closeGrid(ctx context.Context) error { +func (s *Strategy) OpenGrid(ctx context.Context) error { + return s.openGrid(ctx, s.session) +} + +// CloseGrid closes the grid orders +func (s *Strategy) CloseGrid(ctx context.Context) error { bbgo.Sync(ctx, s) // now we can cancel the open orders @@ -1417,7 +1421,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. return } - if err := s.closeGrid(ctx); err != nil { + if err := s.CloseGrid(ctx); err != nil { s.logger.WithError(err).Errorf("grid graceful order cancel error") } }) From 746279d0a77245d88b3c9d9047541635a15d31d1 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 12 Jan 2023 19:37:36 +0900 Subject: [PATCH 0362/1392] Fix klingerOscillator, add test for it --- pkg/indicator/klingeroscillator.go | 50 +++++++++-------------- pkg/indicator/klingeroscillator_test.go | 53 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 32 deletions(-) create mode 100644 pkg/indicator/klingeroscillator_test.go diff --git a/pkg/indicator/klingeroscillator.go b/pkg/indicator/klingeroscillator.go index 69376f5916..198a5d29d7 100644 --- a/pkg/indicator/klingeroscillator.go +++ b/pkg/indicator/klingeroscillator.go @@ -1,6 +1,8 @@ package indicator -import "github.com/c9s/bbgo/pkg/types" +import ( + "github.com/c9s/bbgo/pkg/types" +) // Refer: Klinger Oscillator // Refer URL: https://www.investopedia.com/terms/k/klingeroscillator.asp @@ -14,8 +16,8 @@ import "github.com/c9s/bbgo/pkg/types" type KlingerOscillator struct { types.SeriesBase types.IntervalWindow - Fast *EWMA - Slow *EWMA + Fast types.UpdatableSeries + Slow types.UpdatableSeries VF VolumeForce updateCallbacks []func(value float64) @@ -47,40 +49,24 @@ func (inc *KlingerOscillator) Update(high, low, cloze, volume float64) { inc.Fast = &EWMA{IntervalWindow: types.IntervalWindow{Window: 34, Interval: inc.Interval}} inc.Slow = &EWMA{IntervalWindow: types.IntervalWindow{Window: 55, Interval: inc.Interval}} } - inc.VF.Update(high, low, cloze, volume) - inc.Fast.Update(inc.VF.Value) - inc.Slow.Update(inc.VF.Value) -} - -var _ types.SeriesExtend = &KlingerOscillator{} - -func (inc *KlingerOscillator) PushK(k types.KLine) { - inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64(), k.Volume.Float64()) -} -func (inc *KlingerOscillator) CalculateAndUpdate(allKLines []types.KLine) { - if inc.Fast == nil { - for _, k := range allKLines { - inc.PushK(k) - inc.EmitUpdate(inc.Last()) - } + if inc.VF.lastSum > 0 { + inc.VF.Update(high, low, cloze, volume) + inc.Fast.Update(inc.VF.Value) + inc.Slow.Update(inc.VF.Value) } else { - k := allKLines[len(allKLines)-1] - inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.VF.Update(high, low, cloze, volume) } } -func (inc *KlingerOscillator) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } +var _ types.SeriesExtend = &KlingerOscillator{} - inc.CalculateAndUpdate(window) +func (inc *KlingerOscillator) PushK(k types.KLine) { + inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64(), k.Volume.Float64()) } -func (inc *KlingerOscillator) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +func (inc *KlingerOscillator) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) } // Utility to hold the state of calculation @@ -93,12 +79,12 @@ type VolumeForce struct { } func (inc *VolumeForce) Update(high, low, cloze, volume float64) { - if inc.Value == 0 { + if inc.lastSum == 0 { inc.dm = high - low inc.cm = inc.dm inc.trend = 1. inc.lastSum = high + low + cloze - inc.Value = volume * 100. + inc.Value = volume // first volume is not calculated return } trend := 1. @@ -114,5 +100,5 @@ func (inc *VolumeForce) Update(high, low, cloze, volume float64) { inc.trend = trend inc.lastSum = high + low + cloze inc.dm = dm - inc.Value = volume * (2.*(inc.dm/inc.cm) - 1.) * trend * 100. + inc.Value = volume * (2.*(inc.dm/inc.cm) - 1.) * trend } diff --git a/pkg/indicator/klingeroscillator_test.go b/pkg/indicator/klingeroscillator_test.go new file mode 100644 index 0000000000..9a0c3d3d2b --- /dev/null +++ b/pkg/indicator/klingeroscillator_test.go @@ -0,0 +1,53 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +/* +import pandas as pd +import pandas_ta as ta +high = pd.Series([1.1, 1.3, 1.5, 1.7, 1.9, 2.2, 2.4, 2.1, 1.8, 1.7]) +low = pd.Series([0.9, 1.1, 1.2, 1.5, 1.7, 2.0, 2.2, 1.9, 1.6, 1.5]) +close = pd.Series([1.0, 1.2, 1.4, 1.6, 1.8, 2.1, 2.3, 2.0, 1.7, 1.6]) +vol = pd.Series([300., 200., 200., 150., 150., 200., 200., 150., 300., 350.]) +# kvo = ta.kvo(high, low, close, vol, fast=3, slow=5, signal=1) +# print(kvo) +# # The implementation of kvo in pandas_ta is different from the one defined in investopedia +# # VF is not simply multipying trend +# # Also the value is not multiplied by 100 in pandas_ta +*/ + +func Test_KlingerOscillator(t *testing.T) { + var high, low, cloze, vResult, vol []fixedpoint.Value + if err := json.Unmarshal([]byte(`[1.1, 1.3, 1.5, 1.7, 1.9, 2.2, 2.4, 2.1, 1.8, 1.7]`), &high); err != nil { + panic(err) + } + if err := json.Unmarshal([]byte(`[0.9, 1.1, 1.2, 1.5, 1.7, 2.0, 2.2, 1.9, 1.6, 1.5]`), &low); err != nil { + panic(err) + } + if err := json.Unmarshal([]byte(`[1.0, 1.2, 1.4, 1.6, 1.8, 2.1, 2.3, 2.0, 1.7, 1.6]`), &cloze); err != nil { + panic(err) + } + if err := json.Unmarshal([]byte(`[300.0, 200.0, 200.0, 150.0, 150.0, 200.0, 200.0, 150.0, 300.0, 350.0]`), &vol); err != nil { + panic(err) + } + if err := json.Unmarshal([]byte(`[300.0, 0.0, -28.5, -83, -95, -138, -146.7, 0, 100, 175]`), &vResult); err != nil { + panic(err) + } + + k := KlingerOscillator{ + Fast: &EWMA{IntervalWindow: types.IntervalWindow{Window: 3}}, + Slow: &EWMA{IntervalWindow: types.IntervalWindow{Window: 5}}, + } + var Delta = 0.5 + for i := 0; i < len(high); i++ { + k.Update(high[i].Float64(), low[i].Float64(), cloze[i].Float64(), vol[i].Float64()) + assert.InDelta(t, k.VF.Value, vResult[i].Float64(), Delta) + } +} From 96405658c9723fe416cbacf0895c99f3f1e10df4 Mon Sep 17 00:00:00 2001 From: Fredrik <35973823+frin1@users.noreply.github.com> Date: Sat, 14 Jan 2023 16:12:53 +0100 Subject: [PATCH 0363/1392] Added indicators --- pkg/datatype/bools/slice.go | 54 +++++ pkg/indicator/supertrendPivot.go | 217 +++++++++++++++++++++ pkg/indicator/supertrendPivot_callbacks.go | 15 ++ pkg/indicator/utBotAlert.go | 154 +++++++++++++++ pkg/indicator/utBotAlert_callbacks.go | 17 ++ 5 files changed, 457 insertions(+) create mode 100644 pkg/datatype/bools/slice.go create mode 100644 pkg/indicator/supertrendPivot.go create mode 100644 pkg/indicator/supertrendPivot_callbacks.go create mode 100644 pkg/indicator/utBotAlert.go create mode 100644 pkg/indicator/utBotAlert_callbacks.go diff --git a/pkg/datatype/bools/slice.go b/pkg/datatype/bools/slice.go new file mode 100644 index 0000000000..d84388f403 --- /dev/null +++ b/pkg/datatype/bools/slice.go @@ -0,0 +1,54 @@ +package bools + +type BoolSlice []bool + +func New(a ...bool) BoolSlice { + return BoolSlice(a) +} + +func (s *BoolSlice) Push(v bool) { + *s = append(*s, v) +} + +func (s *BoolSlice) Update(v bool) { + *s = append(*s, v) +} + +func (s *BoolSlice) Pop(i int64) (v bool) { + v = (*s)[i] + *s = append((*s)[:i], (*s)[i+1:]...) + return v +} + +func (s BoolSlice) Tail(size int) BoolSlice { + length := len(s) + if length <= size { + win := make(BoolSlice, length) + copy(win, s) + return win + } + + win := make(BoolSlice, size) + copy(win, s[length-size:]) + return win +} + +func (s *BoolSlice) Length() int { + return len(*s) +} + +func (s *BoolSlice) Index(i int) bool { + length := len(*s) + if length-i < 0 || i < 0 { + return false + } + return (*s)[length-i-1] +} + +func (s *BoolSlice) Last() bool { + length := len(*s) + if length > 0 { + return (*s)[length-1] + } + return false +} diff --git a/pkg/indicator/supertrendPivot.go b/pkg/indicator/supertrendPivot.go new file mode 100644 index 0000000000..9af2ef797a --- /dev/null +++ b/pkg/indicator/supertrendPivot.go @@ -0,0 +1,217 @@ +package indicator + +import ( + "math" + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// based on "Pivot Point Supertrend by LonesomeTheBlue" from tradingview + +var logpst = logrus.WithField("indicator", "pivotSupertrend") + +//go:generate callbackgen -type PivotSupertrend +type PivotSupertrend struct { + types.SeriesBase + types.IntervalWindow + ATRMultiplier float64 `json:"atrMultiplier"` + PivotWindow int `json:"pivotWindow"` + + AverageTrueRange *ATR // Value must be set when initialized in strategy + + PivotLow *PivotLow // Value must be set when initialized in strategy + PivotHigh *PivotHigh // Value must be set when initialized in strategy + + trendPrices floats.Slice // Tsl: value of the trend line (buy or sell) + supportLine floats.Slice // The support line in an uptrend (green) + resistanceLine floats.Slice // The resistance line in a downtrend (red) + + closePrice float64 + previousClosePrice float64 + uptrendPrice float64 + previousUptrendPrice float64 + downtrendPrice float64 + previousDowntrendPrice float64 + + lastPp float64 + src float64 // center + previousPivotHigh float64 // temp variable to save the last value + previousPivotLow float64 // temp variable to save the last value + + trend types.Direction + previousTrend types.Direction + tradeSignal types.Direction + + EndTime time.Time + UpdateCallbacks []func(value float64) +} + +func (inc *PivotSupertrend) Last() float64 { + return inc.trendPrices.Last() +} + +func (inc *PivotSupertrend) Index(i int) float64 { + length := inc.Length() + if length == 0 || length-i-1 < 0 { + return 0 + } + return inc.trendPrices[length-i-1] +} + +func (inc *PivotSupertrend) Length() int { + return len(inc.trendPrices) +} + +func (inc *PivotSupertrend) Update(highPrice, lowPrice, closePrice float64) { + if inc.Window <= 0 { + panic("window must be greater than 0") + } + + if inc.AverageTrueRange == nil { + inc.SeriesBase.Series = inc + } + + // Start with DirectionUp + if inc.trend != types.DirectionUp && inc.trend != types.DirectionDown { + inc.trend = types.DirectionUp + } + + inc.previousPivotLow = inc.PivotLow.Last() + inc.previousPivotHigh = inc.PivotHigh.Last() + + // Update High / Low pivots + inc.PivotLow.Update(lowPrice) + inc.PivotHigh.Update(highPrice) + + // Update ATR + inc.AverageTrueRange.Update(highPrice, lowPrice, closePrice) + + // Update last prices + inc.previousUptrendPrice = inc.uptrendPrice + inc.previousDowntrendPrice = inc.downtrendPrice + inc.previousClosePrice = inc.closePrice + inc.previousTrend = inc.trend + + inc.closePrice = closePrice + + // Initialize lastPp as soon as pivots are made + if inc.lastPp == 0 || math.IsNaN(inc.lastPp) { + if inc.PivotHigh.Length() > 0 { + inc.lastPp = inc.PivotHigh.Last() + } else if inc.PivotLow.Length() > 0 { + inc.lastPp = inc.PivotLow.Last() + } else { + inc.lastPp = math.NaN() + return + } + } + + // Set lastPp to the latest pivotPoint (only changed when new pivot is found) + if inc.PivotHigh.Last() != inc.previousPivotHigh { + inc.lastPp = inc.PivotHigh.Last() + } else if inc.PivotLow.Last() != inc.previousPivotLow { + inc.lastPp = inc.PivotLow.Last() + } + + // calculate the Center line using pivot points + if inc.src == 0 || math.IsNaN(inc.src) { + inc.src = inc.lastPp + } else { + //weighted calculation + inc.src = (inc.src*2 + inc.lastPp) / 3 + } + + // Update uptrend + inc.uptrendPrice = inc.src - inc.AverageTrueRange.Last()*inc.ATRMultiplier + if inc.previousClosePrice > inc.previousUptrendPrice { + inc.uptrendPrice = math.Max(inc.uptrendPrice, inc.previousUptrendPrice) + } + + // Update downtrend + inc.downtrendPrice = inc.src + inc.AverageTrueRange.Last()*inc.ATRMultiplier + if inc.previousClosePrice < inc.previousDowntrendPrice { + inc.downtrendPrice = math.Min(inc.downtrendPrice, inc.previousDowntrendPrice) + } + + // Update trend + if inc.previousTrend == types.DirectionUp && inc.closePrice < inc.previousUptrendPrice { + inc.trend = types.DirectionDown + } else if inc.previousTrend == types.DirectionDown && inc.closePrice > inc.previousDowntrendPrice { + inc.trend = types.DirectionUp + } else { + inc.trend = inc.previousTrend + } + + // Update signal + if inc.AverageTrueRange.Last() <= 0 { + inc.tradeSignal = types.DirectionNone + } else if inc.trend == types.DirectionUp && inc.previousTrend == types.DirectionDown { + inc.tradeSignal = types.DirectionUp + } else if inc.trend == types.DirectionDown && inc.previousTrend == types.DirectionUp { + inc.tradeSignal = types.DirectionDown + } else { + inc.tradeSignal = types.DirectionNone + } + + // Update trend price + if inc.trend == types.DirectionDown { + inc.trendPrices.Push(inc.downtrendPrice) + } else { + inc.trendPrices.Push(inc.uptrendPrice) + } + + // Save the trend lines + inc.supportLine.Push(inc.uptrendPrice) + inc.resistanceLine.Push(inc.downtrendPrice) + + logpst.Debugf("Update pivot point supertrend result: closePrice: %v, uptrendPrice: %v, downtrendPrice: %v, trend: %v,"+ + " tradeSignal: %v, AverageTrueRange.Last(): %v", inc.closePrice, inc.uptrendPrice, inc.downtrendPrice, + inc.trend, inc.tradeSignal, inc.AverageTrueRange.Last()) +} + +// GetSignal returns signal (Down, None or Up) +func (inc *PivotSupertrend) GetSignal() types.Direction { + return inc.tradeSignal +} + +// GetDirection returns current trend +func (inc *PivotSupertrend) GetDirection() types.Direction { + return inc.trend +} + +// GetCurrentSupertrendSupport returns last supertrend support value +func (inc *PivotSupertrend) GetCurrentSupertrendSupport() float64 { + return inc.supportLine.Last() +} + +// GetCurrentSupertrendResistance returns last supertrend resistance value +func (inc *PivotSupertrend) GetCurrentSupertrendResistance() float64 { + return inc.resistanceLine.Last() +} + +var _ types.SeriesExtend = &PivotSupertrend{} + +func (inc *PivotSupertrend) PushK(k types.KLine) { + if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { + return + } + + inc.Update(k.GetHigh().Float64(), k.GetLow().Float64(), k.GetClose().Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} + +func (inc *PivotSupertrend) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) +} + +func (inc *PivotSupertrend) LoadK(allKLines []types.KLine) { + inc.SeriesBase.Series = inc + for _, k := range allKLines { + inc.PushK(k) + } +} diff --git a/pkg/indicator/supertrendPivot_callbacks.go b/pkg/indicator/supertrendPivot_callbacks.go new file mode 100644 index 0000000000..8827909073 --- /dev/null +++ b/pkg/indicator/supertrendPivot_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type Supertrend"; DO NOT EDIT. + +package indicator + +import () + +func (inc *PivotSupertrend) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *PivotSupertrend) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/utBotAlert.go b/pkg/indicator/utBotAlert.go new file mode 100644 index 0000000000..9d974ca421 --- /dev/null +++ b/pkg/indicator/utBotAlert.go @@ -0,0 +1,154 @@ +package indicator + +import ( + "math" + "time" + + "github.com/c9s/bbgo/pkg/datatype/bools" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// based on "UT Bot Alerts by QuantNomad" from tradingview + +//go:generate callbackgen -type UtBotAlert +type UtBotAlert struct { + types.IntervalWindow + KeyValue float64 `json:"keyValue"` // Should be ATRMultiplier + + Values []types.Direction + buyValue bools.BoolSlice + sellValue bools.BoolSlice + + AverageTrueRange *ATR // Value must be set when initialized in strategy + + xATRTrailingStop floats.Slice + pos types.Direction // NB: This is currently not in use (kept in case of expanding as it is in the tradingview version) + previousPos types.Direction // NB: This is currently not in use (kept in case of expanding as it is in the tradingview version) + + previousClosePrice float64 + + EndTime time.Time + UpdateCallbacks []func(value types.Direction) +} + +func NewUtBotAlert(iw types.IntervalWindow, keyValue float64) *UtBotAlert { + return &UtBotAlert{ + IntervalWindow: iw, + KeyValue: keyValue, + AverageTrueRange: &ATR{ + IntervalWindow: iw, + }, + } +} + +func (inc *UtBotAlert) Last() types.Direction { + length := len(inc.Values) + if length > 0 { + return inc.Values[length-1] + } + return types.DirectionNone +} + +func (inc *UtBotAlert) Index(i int) types.Direction { + length := inc.Length() + if length == 0 || length-i-1 < 0 { + return 0 + } + return inc.Values[length-i-1] +} + +func (inc *UtBotAlert) Length() int { + return len(inc.Values) +} + +func (inc *UtBotAlert) Update(highPrice, lowPrice, closePrice float64) { + if inc.Window <= 0 { + panic("window must be greater than 0") + } + + // Update ATR + inc.AverageTrueRange.Update(highPrice, lowPrice, closePrice) + + nLoss := inc.AverageTrueRange.Last() * inc.KeyValue + + // xATRTrailingStop + if inc.xATRTrailingStop.Length() == 0 { + // For first run + inc.xATRTrailingStop.Update(0) + + } else if closePrice > inc.xATRTrailingStop.Index(1) && inc.previousClosePrice > inc.xATRTrailingStop.Index(1) { + inc.xATRTrailingStop.Update(math.Max(inc.xATRTrailingStop.Index(1), closePrice-nLoss)) + + } else if closePrice < inc.xATRTrailingStop.Index(1) && inc.previousClosePrice < inc.xATRTrailingStop.Index(1) { + inc.xATRTrailingStop.Update(math.Min(inc.xATRTrailingStop.Index(1), closePrice+nLoss)) + + } else if closePrice > inc.xATRTrailingStop.Index(1) { + inc.xATRTrailingStop.Update(closePrice - nLoss) + + } else { + inc.xATRTrailingStop.Update(closePrice + nLoss) + } + + // pos + if inc.previousClosePrice < inc.xATRTrailingStop.Index(1) && closePrice > inc.xATRTrailingStop.Index(1) { + inc.pos = types.DirectionUp + } else if inc.previousClosePrice > inc.xATRTrailingStop.Index(1) && closePrice < inc.xATRTrailingStop.Index(1) { + inc.pos = types.DirectionDown + } else { + inc.pos = inc.previousPos + } + + above := closePrice > inc.xATRTrailingStop.Last() && inc.previousClosePrice < inc.xATRTrailingStop.Index(1) + below := closePrice < inc.xATRTrailingStop.Last() && inc.previousClosePrice > inc.xATRTrailingStop.Index(1) + + buy := closePrice > inc.xATRTrailingStop.Last() && above // buy + sell := closePrice < inc.xATRTrailingStop.Last() && below // sell + + inc.buyValue.Push(buy) + inc.sellValue.Push(sell) + + if buy { + inc.Values = append(inc.Values, types.DirectionUp) + } else if sell { + inc.Values = append(inc.Values, types.DirectionDown) + } else { + inc.Values = append(inc.Values, types.DirectionNone) + } + + // Update last prices + inc.previousClosePrice = closePrice + inc.previousPos = inc.pos + +} + +// GetSignal returns signal (down, none or up) +func (inc *UtBotAlert) GetSignal() types.Direction { + length := len(inc.Values) + if length > 0 { + return inc.Values[length-1] + } + return types.DirectionNone +} + +func (inc *UtBotAlert) PushK(k types.KLine) { + if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { + return + } + + inc.Update(k.GetHigh().Float64(), k.GetLow().Float64(), k.GetClose().Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) + +} + +func (inc *UtBotAlert) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) +} + +// LoadK calculates the initial values +func (inc *UtBotAlert) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + inc.PushK(k) + } +} diff --git a/pkg/indicator/utBotAlert_callbacks.go b/pkg/indicator/utBotAlert_callbacks.go new file mode 100644 index 0000000000..964188f264 --- /dev/null +++ b/pkg/indicator/utBotAlert_callbacks.go @@ -0,0 +1,17 @@ +// Code generated by "callbackgen -type Supertrend"; DO NOT EDIT. + +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +func (inc *UtBotAlert) OnUpdate(cb func(value types.Direction)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *UtBotAlert) EmitUpdate(value types.Direction) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} From f1fbf537c434b23cb806d6d4d20b668011b74a59 Mon Sep 17 00:00:00 2001 From: Fredrik <35973823+frin1@users.noreply.github.com> Date: Sat, 14 Jan 2023 16:13:10 +0100 Subject: [PATCH 0364/1392] Added functions to supertrend --- pkg/indicator/supertrend.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/pkg/indicator/supertrend.go b/pkg/indicator/supertrend.go index 6d15e19a2c..1713974da2 100644 --- a/pkg/indicator/supertrend.go +++ b/pkg/indicator/supertrend.go @@ -20,7 +20,9 @@ type Supertrend struct { AverageTrueRange *ATR - trendPrices floats.Slice + trendPrices floats.Slice // Value of the trend line (buy or sell) + supportLine floats.Slice // The support line in an uptrend (green) + resistanceLine floats.Slice // The resistance line in a downtrend (red) closePrice float64 previousClosePrice float64 @@ -119,6 +121,10 @@ func (inc *Supertrend) Update(highPrice, lowPrice, closePrice float64) { inc.trendPrices.Push(inc.uptrendPrice) } + // Save the trend lines + inc.supportLine.Push(inc.uptrendPrice) + inc.resistanceLine.Push(inc.downtrendPrice) + logst.Debugf("Update supertrend result: closePrice: %v, uptrendPrice: %v, downtrendPrice: %v, trend: %v,"+ " tradeSignal: %v, AverageTrueRange.Last(): %v", inc.closePrice, inc.uptrendPrice, inc.downtrendPrice, inc.trend, inc.tradeSignal, inc.AverageTrueRange.Last()) @@ -128,6 +134,21 @@ func (inc *Supertrend) GetSignal() types.Direction { return inc.tradeSignal } +// GetDirection returns current trend +func (inc *Supertrend) GetDirection() types.Direction { + return inc.trend +} + +// GetCurrentSupertrendSupport returns last supertrend support +func (inc *Supertrend) GetCurrentSupertrendSupport() float64 { + return inc.supportLine.Last() +} + +// GetCurrentSupertrendResistance returns last supertrend resistance +func (inc *Supertrend) GetCurrentSupertrendResistance() float64 { + return inc.resistanceLine.Last() +} + var _ types.SeriesExtend = &Supertrend{} func (inc *Supertrend) PushK(k types.KLine) { From c8f934cafb2a2ecd66e6381b8c5cccea28145a86 Mon Sep 17 00:00:00 2001 From: Fredrik <35973823+frin1@users.noreply.github.com> Date: Sun, 15 Jan 2023 10:25:22 +0100 Subject: [PATCH 0365/1392] Rename variables --- pkg/indicator/supertrend.go | 12 ++++++------ pkg/indicator/supertrendPivot.go | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/indicator/supertrend.go b/pkg/indicator/supertrend.go index 1713974da2..38fdd52e63 100644 --- a/pkg/indicator/supertrend.go +++ b/pkg/indicator/supertrend.go @@ -134,18 +134,18 @@ func (inc *Supertrend) GetSignal() types.Direction { return inc.tradeSignal } -// GetDirection returns current trend -func (inc *Supertrend) GetDirection() types.Direction { +// GetDirection return the current trend +func (inc *Supertrend) Direction() types.Direction { return inc.trend } -// GetCurrentSupertrendSupport returns last supertrend support -func (inc *Supertrend) GetCurrentSupertrendSupport() float64 { +// LastSupertrendSupport return the current supertrend support +func (inc *Supertrend) LastSupertrendSupport() float64 { return inc.supportLine.Last() } -// GetCurrentSupertrendResistance returns last supertrend resistance -func (inc *Supertrend) GetCurrentSupertrendResistance() float64 { +// LastSupertrendResistance return the current supertrend resistance +func (inc *Supertrend) LastSupertrendResistance() float64 { return inc.resistanceLine.Last() } diff --git a/pkg/indicator/supertrendPivot.go b/pkg/indicator/supertrendPivot.go index 9af2ef797a..14b62925d9 100644 --- a/pkg/indicator/supertrendPivot.go +++ b/pkg/indicator/supertrendPivot.go @@ -178,18 +178,18 @@ func (inc *PivotSupertrend) GetSignal() types.Direction { return inc.tradeSignal } -// GetDirection returns current trend -func (inc *PivotSupertrend) GetDirection() types.Direction { +// GetDirection return the current trend +func (inc *PivotSupertrend) Direction() types.Direction { return inc.trend } -// GetCurrentSupertrendSupport returns last supertrend support value -func (inc *PivotSupertrend) GetCurrentSupertrendSupport() float64 { +// LastSupertrendSupport return the current supertrend support value +func (inc *PivotSupertrend) LastSupertrendSupport() float64 { return inc.supportLine.Last() } -// GetCurrentSupertrendResistance returns last supertrend resistance value -func (inc *PivotSupertrend) GetCurrentSupertrendResistance() float64 { +// LastSupertrendResistance return the current supertrend resistance value +func (inc *PivotSupertrend) LastSupertrendResistance() float64 { return inc.resistanceLine.Last() } From 0b71f2f1d2b20c35a142939dac3f7696c0a174f6 Mon Sep 17 00:00:00 2001 From: zenix Date: Mon, 16 Jan 2023 12:37:51 +0900 Subject: [PATCH 0366/1392] fix: query price range from volume profile trades on every updates. will make it slower on updates --- pkg/indicator/volumeprofile.go | 27 +++++++++++++++++++-------- pkg/indicator/volumeprofile_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 pkg/indicator/volumeprofile_test.go diff --git a/pkg/indicator/volumeprofile.go b/pkg/indicator/volumeprofile.go index df8903fe03..c02535c031 100644 --- a/pkg/indicator/volumeprofile.go +++ b/pkg/indicator/volumeprofile.go @@ -27,12 +27,8 @@ type VolumeProfile struct { } func (inc *VolumeProfile) Update(price, volume float64, timestamp types.Time) { - if inc.minPrice == 0 { - inc.minPrice = math.Inf(1) - } - if inc.maxPrice == 0 { - inc.maxPrice = math.Inf(-1) - } + inc.minPrice = math.Inf(1) + inc.maxPrice = math.Inf(-1) if inc.profile == nil { inc.profile = make(map[float64]float64) } @@ -47,13 +43,14 @@ func (inc *VolumeProfile) Update(price, volume float64, timestamp types.Time) { for i = 0; i < len(inc.trades); i++ { td := inc.trades[i] if td.timestamp.After(filter) { - inc.trades = inc.trades[i : len(inc.trades)-1] + inc.trades = inc.trades[i:len(inc.trades)] break } inc.profile[math.Round(td.price/inc.Delta)] -= td.volume } - for k, _ := range inc.profile { + for i = 0; i < len(inc.trades); i++ { + k := math.Round(inc.trades[i].price / inc.Delta) if k < inc.minPrice { inc.minPrice = k } @@ -74,8 +71,14 @@ func (inc *VolumeProfile) PointOfControlAboveEqual(price float64, limit ...float if len(limit) > 0 { filter = limit[0] } + if inc.Delta == 0 { + panic("Delta for volumeprofile shouldn't be zero") + } start := math.Round(price / inc.Delta) vol = math.Inf(-1) + if start > filter { + return 0, 0 + } for ; start <= filter; start += inc.Delta { abs := math.Abs(inc.profile[start]) if vol < abs { @@ -93,8 +96,16 @@ func (inc *VolumeProfile) PointOfControlBelowEqual(price float64, limit ...float if len(limit) > 0 { filter = limit[0] } + if inc.Delta == 0 { + panic("Delta for volumeprofile shouldn't be zero") + } start := math.Round(price / inc.Delta) vol = math.Inf(-1) + + if start < filter { + return 0, 0 + } + for ; start >= filter; start -= inc.Delta { abs := math.Abs(inc.profile[start]) if vol < abs { diff --git a/pkg/indicator/volumeprofile_test.go b/pkg/indicator/volumeprofile_test.go new file mode 100644 index 0000000000..825307e69e --- /dev/null +++ b/pkg/indicator/volumeprofile_test.go @@ -0,0 +1,29 @@ +package indicator + +import ( + "testing" + "time" + + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +func Test_DelVolumeProfile(t *testing.T) { + + vp := VolumeProfile{IntervalWindow: types.IntervalWindow{Window: 1, Interval: types.Interval1s}, Delta: 1.0} + vp.Update(1., 100., types.Time(time.Now())) + r, v := vp.PointOfControlAboveEqual(1.) + assert.Equal(t, r, 1.) + assert.Equal(t, v, 100.) + vp.Update(2., 100., types.Time(time.Now().Add(time.Second*10))) + r, v = vp.PointOfControlAboveEqual(1.) + assert.Equal(t, r, 2.) + assert.Equal(t, v, 100.) + r, v = vp.PointOfControlBelowEqual(1.) + assert.Equal(t, r, 0.) + assert.Equal(t, v, 0.) + r, v = vp.PointOfControlBelowEqual(2.) + assert.Equal(t, r, 2.) + assert.Equal(t, v, 100.) + +} From 74daa76e7524d94f7e0038b7dfc5239ff110a071 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Jan 2023 18:34:08 +0800 Subject: [PATCH 0367/1392] grid2: fix grid num calculation --- pkg/strategy/grid2/grid.go | 8 ++++++-- pkg/strategy/grid2/grid_test.go | 14 +++++++------- pkg/strategy/grid2/strategy_test.go | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index ccdf34427c..5a83f700ff 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -58,7 +58,8 @@ func buildPinCache(pins []Pin) map[Pin]struct{} { func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { height := upper.Sub(lower) - spread := height.Div(size) + one := fixedpoint.NewFromInt(1) + spread := height.Div(size.Sub(one)) grid := &Grid{ UpperPrice: upper, @@ -83,7 +84,10 @@ func (g *Grid) CalculateGeometricPins() { func (g *Grid) CalculateArithmeticPins() { g.calculator = func() []Pin { - return calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) + one := fixedpoint.NewFromInt(1) + height := g.UpperPrice.Sub(g.LowerPrice) + spread := height.Div(g.Size.Sub(one)) + return calculateArithmeticPins(g.LowerPrice, g.UpperPrice, spread, g.TickSize) } g.addPins(g.calculator()) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 8edfe59cd2..a5d72281f4 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -28,7 +28,7 @@ func number(a interface{}) fixedpoint.Value { func TestNewGrid(t *testing.T) { upper := fixedpoint.NewFromFloat(500.0) lower := fixedpoint.NewFromFloat(100.0) - size := fixedpoint.NewFromFloat(100.0) + size := fixedpoint.NewFromFloat(101.0) grid := NewGrid(lower, upper, size, number(0.01)) grid.CalculateArithmeticPins() @@ -44,7 +44,7 @@ func TestNewGrid(t *testing.T) { func TestGrid_HasPin(t *testing.T) { upper := fixedpoint.NewFromFloat(500.0) lower := fixedpoint.NewFromFloat(100.0) - size := fixedpoint.NewFromFloat(100.0) + size := fixedpoint.NewFromFloat(101.0) grid := NewGrid(lower, upper, size, number(0.01)) grid.CalculateArithmeticPins() @@ -56,7 +56,7 @@ func TestGrid_HasPin(t *testing.T) { func TestGrid_ExtendUpperPrice(t *testing.T) { upper := number(500.0) lower := number(100.0) - size := number(4.0) + size := number(5.0) grid := NewGrid(lower, upper, size, number(0.01)) grid.CalculateArithmeticPins() @@ -64,7 +64,7 @@ func TestGrid_ExtendUpperPrice(t *testing.T) { t.Logf("pins: %+v", grid.Pins) assert.Equal(t, number(100.0), originalSpread) - assert.Len(t, grid.Pins, 5) // (1000-500) / 4 + assert.Len(t, grid.Pins, 5) newPins := grid.ExtendUpperPrice(number(1000.0)) assert.Len(t, grid.Pins, 10) @@ -76,7 +76,7 @@ func TestGrid_ExtendUpperPrice(t *testing.T) { func TestGrid_ExtendLowerPrice(t *testing.T) { upper := fixedpoint.NewFromFloat(3000.0) lower := fixedpoint.NewFromFloat(2000.0) - size := fixedpoint.NewFromFloat(10.0) + size := fixedpoint.NewFromFloat(11.0) grid := NewGrid(lower, upper, size, number(0.01)) grid.CalculateArithmeticPins() @@ -111,7 +111,7 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { func TestGrid_NextLowerPin(t *testing.T) { upper := number(500.0) lower := number(100.0) - size := number(4.0) + size := number(5.0) grid := NewGrid(lower, upper, size, number(0.01)) grid.CalculateArithmeticPins() @@ -129,7 +129,7 @@ func TestGrid_NextLowerPin(t *testing.T) { func TestGrid_NextHigherPin(t *testing.T) { upper := number(500.0) lower := number(100.0) - size := number(4.0) + size := number(5.0) grid := NewGrid(lower, upper, size, number(0.01)) grid.CalculateArithmeticPins() t.Logf("pins: %+v", grid.Pins) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 3225859b84..99a7a84631 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -277,7 +277,7 @@ func newTestStrategy() *Strategy { GridProfitStats: newGridProfitStats(market), UpperPrice: number(20_000), LowerPrice: number(10_000), - GridNum: 10, + GridNum: 11, historicalTrades: bbgo.NewTradeStore(), // QuoteInvestment: number(9000.0), From 4c2c647160d48331a60afed96e290deb814405b2 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 18 Jan 2023 19:11:23 +0900 Subject: [PATCH 0368/1392] fix: remove bind and handler for newly added indicators --- pkg/indicator/psar.go | 24 ++++-------------------- pkg/indicator/psar_test.go | 7 +++---- pkg/indicator/tsi.go | 24 ++---------------------- pkg/indicator/tsi_test.go | 4 +++- 4 files changed, 12 insertions(+), 47 deletions(-) diff --git a/pkg/indicator/psar.go b/pkg/indicator/psar.go index 95e4822c9d..e39e3fb2ce 100644 --- a/pkg/indicator/psar.go +++ b/pkg/indicator/psar.go @@ -115,26 +115,10 @@ func (inc *PSAR) Update(high, low float64) { var _ types.SeriesExtend = &PSAR{} -func (inc *PSAR) CalculateAndUpdate(kLines []types.KLine) { - for _, k := range kLines { - if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { - continue - } - inc.Update(k.High.Float64(), k.Low.Float64()) - } - - inc.EmitUpdate(inc.Last()) - inc.EndTime = kLines[len(kLines)-1].EndTime.Time() -} - -func (inc *PSAR) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - - inc.CalculateAndUpdate(window) +func (inc *PSAR) PushK(k types.KLine) { + inc.Update(k.High.Float64(), k.Low.Float64()) } -func (inc *PSAR) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +func (inc *PSAR) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) } diff --git a/pkg/indicator/psar_test.go b/pkg/indicator/psar_test.go index eae9d513fb..6914f7e547 100644 --- a/pkg/indicator/psar_test.go +++ b/pkg/indicator/psar_test.go @@ -27,14 +27,13 @@ func Test_PSAR(t *testing.T) { psar := PSAR{ IntervalWindow: types.IntervalWindow{Window: 2}, } - var klines []types.KLine for _, v := range input { - klines = append(klines, types.KLine{ + kline := types.KLine{ High: v, Low: v, - }) + } + psar.PushK(kline) } - psar.CalculateAndUpdate(klines) assert.Equal(t, psar.Length(), 29) assert.Equal(t, psar.AF, 0.04) assert.Equal(t, psar.Last(), 0.16) diff --git a/pkg/indicator/tsi.go b/pkg/indicator/tsi.go index 57f4e5456d..4cdb6c7508 100644 --- a/pkg/indicator/tsi.go +++ b/pkg/indicator/tsi.go @@ -94,26 +94,6 @@ func (inc *TSI) PushK(k types.KLine) { var _ types.SeriesExtend = &TSI{} -func (inc *TSI) CalculateAndUpdate(allKLines []types.KLine) { - if inc.PrevValue == 0 { - for _, k := range allKLines { - inc.PushK(k) - inc.EmitUpdate(inc.Last()) - } - } else { - k := allKLines[len(allKLines)-1] - inc.PushK(k) - inc.EmitUpdate(inc.Last()) - } -} - -func (inc *TSI) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - inc.CalculateAndUpdate(window) -} - -func (inc *TSI) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +func (inc *TSI) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) } diff --git a/pkg/indicator/tsi_test.go b/pkg/indicator/tsi_test.go index 8ab639e567..dc7c9c3cb1 100644 --- a/pkg/indicator/tsi_test.go +++ b/pkg/indicator/tsi_test.go @@ -27,7 +27,9 @@ func Test_TSI(t *testing.T) { } tsi := TSI{} klines := buildKLines(input) - tsi.CalculateAndUpdate(klines) + for _, kline := range klines { + tsi.PushK(kline) + } assert.Equal(t, tsi.Length(), 29) Delta := 1.5e-2 assert.InDelta(t, tsi.Last(), 22.89, Delta) From bfe5eace1a3c6169fbdc512a1b02885f604d5c0a Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 19 Jan 2023 13:07:01 +0900 Subject: [PATCH 0369/1392] feature: get historical public trades from binance --- .../get_historical_trades_request.go | 20 ++ ...et_historical_trades_request_requestgen.go | 175 ++++++++++++++++++ pkg/exchange/binance/historical_trades.go | 29 +++ 3 files changed, 224 insertions(+) create mode 100644 pkg/exchange/binance/binanceapi/get_historical_trades_request.go create mode 100644 pkg/exchange/binance/binanceapi/get_historical_trades_request_requestgen.go create mode 100644 pkg/exchange/binance/historical_trades.go diff --git a/pkg/exchange/binance/binanceapi/get_historical_trades_request.go b/pkg/exchange/binance/binanceapi/get_historical_trades_request.go new file mode 100644 index 0000000000..11b0518438 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_historical_trades_request.go @@ -0,0 +1,20 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" +) + +//type Trade = binance.TradeV3 + +//go:generate requestgen -method GET -url "/api/v3/historicalTrades" -type GetHistoricalTradesRequest -responseType []Trade +type GetHistoricalTradesRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` + limit *uint64 `param:"limit"` + fromID *uint64 `param:"fromId"` +} + +func (c *RestClient) NewGetHistoricalTradesRequest() *GetHistoricalTradesRequest { + return &GetHistoricalTradesRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_historical_trades_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_historical_trades_request_requestgen.go new file mode 100644 index 0000000000..a786cfcb41 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_historical_trades_request_requestgen.go @@ -0,0 +1,175 @@ +// Code generated by "requestgen -method GET -url /api/v3/historicalTrades -type GetHistoricalTradesRequest -responseType []Trade"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/adshao/go-binance/v2" + "net/url" + "reflect" + "regexp" +) + +func (g *GetHistoricalTradesRequest) Symbol(symbol string) *GetHistoricalTradesRequest { + g.symbol = symbol + return g +} + +func (g *GetHistoricalTradesRequest) Limit(limit uint64) *GetHistoricalTradesRequest { + g.limit = &limit + return g +} + +func (g *GetHistoricalTradesRequest) FromID(fromID uint64) *GetHistoricalTradesRequest { + g.fromID = &fromID + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetHistoricalTradesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetHistoricalTradesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check fromID field -> json key fromId + if g.fromID != nil { + fromID := *g.fromID + + // assign parameter of fromID + params["fromId"] = fromID + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetHistoricalTradesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetHistoricalTradesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetHistoricalTradesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetHistoricalTradesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetHistoricalTradesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetHistoricalTradesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetHistoricalTradesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetHistoricalTradesRequest) Do(ctx context.Context) ([]binance.TradeV3, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/historicalTrades" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []binance.TradeV3 + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/historical_trades.go b/pkg/exchange/binance/historical_trades.go new file mode 100644 index 0000000000..40c636bf2d --- /dev/null +++ b/pkg/exchange/binance/historical_trades.go @@ -0,0 +1,29 @@ +package binance + +import ( + "context" + + "github.com/c9s/bbgo/pkg/types" +) + +func (e *Exchange) QueryHistoricalTrades(ctx context.Context, symbol string, limit uint64) ([]types.Trade, error) { + req := e.client2.NewGetHistoricalTradesRequest() + req.Symbol(symbol) + req.Limit(limit) + trades, err := req.Do(ctx) + if err != nil { + return nil, err + } + + var result []types.Trade + for _, t := range trades { + localTrade, err := toGlobalTrade(t, e.IsMargin) + if err != nil { + log.WithError(err).Errorf("cannot convert binance trade: %+v", t) + continue + } + result = append(result, *localTrade) + } + result = types.SortTradesAscending(result) + return result, nil +} From d43acaa17c744f7dc600241b8ac984d4c604cbee Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 31 Jan 2023 21:30:58 +0800 Subject: [PATCH 0370/1392] grid2: add metrics registration guard --- pkg/strategy/grid2/metrics.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/strategy/grid2/metrics.go b/pkg/strategy/grid2/metrics.go index 1fdeb23b44..854a810ccb 100644 --- a/pkg/strategy/grid2/metrics.go +++ b/pkg/strategy/grid2/metrics.go @@ -66,10 +66,17 @@ func initMetrics(extendedLabels []string) { ) } +var metricsRegistered = false + func registerMetrics() { + if metricsRegistered { + return + } + prometheus.MustRegister( metricsGridNumOfOrders, metricsGridProfit, metricsGridOrderPrices, ) + metricsRegistered = true } From 4bf0cb6a0cd18b7c9f4447ccdfc4677742a55091 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Feb 2023 15:48:19 +0800 Subject: [PATCH 0371/1392] grid2: use Round instead of Trunc --- pkg/strategy/grid2/grid.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 5a83f700ff..792c1a334b 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -39,7 +39,7 @@ func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) [] for p := lower; p.Compare(upper) <= 0; p = p.Add(spread) { // tickSize here = 0.01 pp := p.Float64() / ts - pp = math.Trunc(pp) * ts + pp = math.Round(pp) * ts pin := Pin(fixedpoint.NewFromFloat(pp)) pins = append(pins, pin) } From 854ac4f8eaced83ee36dc5514bb303bad76e17c7 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Feb 2023 18:56:01 +0800 Subject: [PATCH 0372/1392] grid2: fix pin price algorithm --- pkg/strategy/grid2/grid.go | 9 ++++++--- pkg/strategy/grid2/grid_test.go | 18 +++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 792c1a334b..d5615626cd 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -35,11 +35,14 @@ type Pin fixedpoint.Value func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) []Pin { var pins []Pin + + // tickSize number is like 0.01, 0.1, 0.001 var ts = tickSize.Float64() + var prec = int(math.Round(math.Log10(ts) * -1.0)) + var pow10 = math.Pow10(prec) for p := lower; p.Compare(upper) <= 0; p = p.Add(spread) { - // tickSize here = 0.01 - pp := p.Float64() / ts - pp = math.Round(pp) * ts + pp := math.Round(p.Float64()*pow10*10.0) / 10.0 + pp = math.Trunc(pp) / pow10 pin := Pin(fixedpoint.NewFromFloat(pp)) pins = append(pins, pin) } diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index a5d72281f4..b5fdaff982 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -172,31 +172,31 @@ func Test_calculateArithmeticPins(t *testing.T) { Pin(number(1000.0)), Pin(number(1066.660)), Pin(number(1133.330)), - Pin(number("1199.99")), + Pin(number("1200.00")), Pin(number(1266.660)), Pin(number(1333.330)), - Pin(number(1399.990)), + Pin(number(1400.000)), Pin(number(1466.660)), Pin(number(1533.330)), - Pin(number(1599.990)), + Pin(number(1600.000)), Pin(number(1666.660)), Pin(number(1733.330)), - Pin(number(1799.990)), + Pin(number(1800.000)), Pin(number(1866.660)), Pin(number(1933.330)), - Pin(number(1999.990)), + Pin(number(2000.000)), Pin(number(2066.660)), Pin(number(2133.330)), - Pin(number("2199.99")), + Pin(number("2200.00")), Pin(number(2266.660)), Pin(number(2333.330)), - Pin(number("2399.99")), + Pin(number("2400.00")), Pin(number(2466.660)), Pin(number(2533.330)), - Pin(number("2599.99")), + Pin(number("2600.00")), Pin(number(2666.660)), Pin(number(2733.330)), - Pin(number(2799.990)), + Pin(number(2800.000)), Pin(number(2866.660)), Pin(number(2933.330)), }, From 3a7be0e2b2baeb9ca0cb83996d1c9cbed46b64c1 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 6 Feb 2023 16:31:57 +0800 Subject: [PATCH 0373/1392] grid2: add closing grid log --- pkg/strategy/grid2/strategy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6746fca0f1..31aa942a55 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -737,11 +737,12 @@ func (s *Strategy) OpenGrid(ctx context.Context) error { // CloseGrid closes the grid orders func (s *Strategy) CloseGrid(ctx context.Context) error { + s.logger.Infof("closing %s grid", s.Symbol) + bbgo.Sync(ctx, s) // now we can cancel the open orders s.logger.Infof("canceling grid orders...") - if err := s.orderExecutor.GracefulCancel(ctx); err != nil { return err } From 06c3f5f79c8483146ce9877691bcc3e36ad50d5c Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 6 Feb 2023 16:59:50 +0800 Subject: [PATCH 0374/1392] grid2: add PlainText method support to GridProfitStats --- pkg/strategy/grid2/profit_stats.go | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pkg/strategy/grid2/profit_stats.go b/pkg/strategy/grid2/profit_stats.go index df88af1a72..5f47effc6a 100644 --- a/pkg/strategy/grid2/profit_stats.go +++ b/pkg/strategy/grid2/profit_stats.go @@ -137,3 +137,43 @@ func (s *GridProfitStats) SlackAttachment() slack.Attachment { Footer: footer, } } + +func (s *GridProfitStats) String() string { + return s.PlainText() +} + +func (s *GridProfitStats) PlainText() string { + var o string + + o = fmt.Sprintf("%s Grid Profit Stats", s.Symbol) + + o += fmt.Sprintf(" Arbitrage count: %d", s.ArbitrageCount) + + if !s.FloatProfit.IsZero() { + o += " Float profit: " + style.PnLSignString(s.FloatProfit) + } + + if !s.GridProfit.IsZero() { + o += " Grid profit: " + style.PnLSignString(s.GridProfit) + } + + if !s.TotalQuoteProfit.IsZero() { + o += " Total quote profit: " + style.PnLSignString(s.TotalQuoteProfit) + " " + s.Market.QuoteCurrency + } + + if !s.TotalBaseProfit.IsZero() { + o += " Total base profit: " + style.PnLSignString(s.TotalBaseProfit) + " " + s.Market.BaseCurrency + } + + if len(s.TotalFee) > 0 { + for feeCurrency, fee := range s.TotalFee { + o += fmt.Sprintf(" Fee (%s)", feeCurrency) + fee.String() + " " + feeCurrency + } + } + + if s.Since != nil { + o += fmt.Sprintf(" Since %s", s.Since.String()) + } + + return o +} From e8c69dfaef52de217a55600d1148b5970ea3e185 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 7 Feb 2023 01:38:25 +0800 Subject: [PATCH 0375/1392] grid2: add mutex lock for the grid object field --- pkg/strategy/grid2/strategy.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 31aa942a55..52a4e15765 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -140,6 +140,9 @@ type Strategy struct { gridProfitCallbacks []func(stats *GridProfitStats, profit *GridProfit) gridClosedCallbacks []func() gridErrorCallbacks []func(err error) + + // mu is used for locking the grid object field, avoid double grid opening + mu sync.Mutex } func (s *Strategy) ID() string { @@ -764,6 +767,9 @@ func (s *Strategy) newGrid() *Grid { // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) error { // grid object guard + s.mu.Lock() + defer s.mu.Unlock() + if s.grid != nil { return nil } @@ -1151,7 +1157,10 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang s.logger.Infof("GRID RECOVER: found %d filled grid orders", len(filledOrders)) + s.mu.Lock() s.grid = grid + s.mu.Unlock() + for _, o := range filledOrders { s.processFilledOrder(o) } From 0b73520371d3f0d10fd07e4381e6b36d51e6125d Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 7 Feb 2023 14:15:17 +0800 Subject: [PATCH 0376/1392] config: update config example --- config/grid2.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/grid2.yaml b/config/grid2.yaml index 18725fe9d5..d74f66458c 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -100,7 +100,7 @@ exchangeStrategies: ## There are 3 kinds of setup ## NOTICE: you can only choose one, uncomment the config to enable it ## - ## 1) fixed amount: amount is the quote unit (e.g. USDT in BTCUSDT) + ## 1) fixed amount: amount is the quote unit (e.g. 10 USDT in BTCUSDT) # amount: 10.0 ## 2) fixed quantity: it will use your balance to place orders with the fixed quantity. e.g. 0.001 BTC From 760fc74187afed1cd3581c407369757b7c8a3d5e Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 8 Feb 2023 16:26:37 +0800 Subject: [PATCH 0377/1392] grid2: expose order group ID field --- pkg/strategy/grid2/strategy.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 52a4e15765..fadf73c01b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -115,6 +115,9 @@ type Strategy struct { // PrometheusLabels will be used as the base prometheus labels PrometheusLabels prometheus.Labels `json:"prometheusLabels"` + // OrderGroupID is the group ID used for the strategy instance for canceling orders + OrderGroupID uint32 `json:"orderGroupID"` + // FeeRate is used for calculating the minimal profit spread. // it makes sure that your grid configuration is profitable. FeeRate fixedpoint.Value `json:"feeRate"` @@ -131,9 +134,6 @@ type Strategy struct { orderExecutor OrderExecutor historicalTrades *bbgo.TradeStore - // groupID is the group ID used for the strategy instance for canceling orders - groupID uint32 - logger *logrus.Entry gridReadyCallbacks []func() @@ -1328,7 +1328,9 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. "symbol": s.Symbol, }) - s.groupID = util.FNV32(instanceID) + if s.OrderGroupID == 0 { + s.OrderGroupID = util.FNV32(instanceID) + } if s.AutoRange != nil { indicatorSet := session.StandardIndicatorSet(s.Symbol) From abdded812607911cf98d45a66d9c8d4f6b7ebfb9 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 8 Feb 2023 16:43:25 +0800 Subject: [PATCH 0378/1392] grid2: add ClearOpenOrdersIfMismatch --- pkg/strategy/grid2/grid.go | 5 +++ pkg/strategy/grid2/strategy.go | 59 ++++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index d5615626cd..d886816c5f 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -137,6 +137,11 @@ func (g *Grid) NextLowerPin(price fixedpoint.Value) (Pin, bool) { return Pin(fixedpoint.Zero), false } +func (g *Grid) HasPrice(price fixedpoint.Value) bool { + i := g.SearchPin(price) + return fixedpoint.Value(g.Pins[i]).Compare(price) == 0 +} + func (g *Grid) SearchPin(price fixedpoint.Value) int { i := sort.Search(len(g.Pins), func(i int) bool { a := fixedpoint.Value(g.Pins[i]) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index fadf73c01b..670df981b7 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -110,6 +110,8 @@ type Strategy struct { // If this is set, when bbgo started, it will clear the open orders in the same market (by symbol) ClearOpenOrdersWhenStart bool `json:"clearOpenOrdersWhenStart"` + ClearOpenOrdersIfMismatch bool `json:"clearOpenOrdersIfMismatch"` + ResetPositionWhenStart bool `json:"resetPositionWhenStart"` // PrometheusLabels will be used as the base prometheus labels @@ -1407,20 +1409,33 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } } - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) - if err != nil { - return err + if s.ClearOpenOrdersIfMismatch { + mismatch, err := s.openOrdersMismatches(ctx, session) + if err != nil { + s.logger.WithError(err).Errorf("clearOpenOrdersIfMismatch error") + } else if mismatch { + if err2 := s.clearOpenOrders(ctx, session); err2 != nil { + s.logger.WithError(err2).Errorf("clearOpenOrders error") + } + } } - if s.RecoverOrdersWhenStart && len(openOrders) > 0 { + if s.RecoverOrdersWhenStart { + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } + s.logger.Infof("recoverWhenStart is set, found %d open orders, trying to recover grid orders...", len(openOrders)) - historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) - if !implemented { - s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") - } else { - if err := s.recoverGrid(ctx, historyService, openOrders); err != nil { - return errors.Wrap(err, "recover grid error") + if len(openOrders) > 0 { + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) + if !implemented { + s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") + } else { + if err := s.recoverGrid(ctx, historyService, openOrders); err != nil { + return errors.Wrap(err, "recover grid error") + } } } } @@ -1461,3 +1476,27 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. return nil } + +// openOrdersMismatches verifies if the open orders are on the grid pins +// return true if mismatches +func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.ExchangeSession) (bool, error) { + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return false, err + } + + if len(openOrders) == 0 { + return false, nil + } + + grid := s.newGrid() + for _, o := range openOrders { + // if any of the open order is not on the grid, or out of the range + // we should cancel all of them + if !grid.HasPrice(o.Price) || grid.OutOfRange(o.Price) { + return true, nil + } + } + + return false, nil +} From 829704eda3b1c7f1d2120f133fa8acf8baeb92fd Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 8 Feb 2023 16:46:19 +0800 Subject: [PATCH 0379/1392] grid2: remove todo --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 670df981b7..a176d52956 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1402,7 +1402,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. metricsGridProfit.With(labels).Set(stats.TotalQuoteProfit.Float64()) }) - // TODO: detect if there are previous grid orders on the order book + // detect if there are previous grid orders on the order book if s.ClearOpenOrdersWhenStart { if err := s.clearOpenOrders(ctx, session); err != nil { return err From 3c69556424d3efdb067cbe56bbae9edd0f199b04 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 8 Feb 2023 17:30:33 +0800 Subject: [PATCH 0380/1392] bbgo: fix graceful shutdown call --- pkg/bbgo/graceful_shutdown.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/graceful_shutdown.go b/pkg/bbgo/graceful_shutdown.go index 5f6be55a01..4ea578f1f2 100644 --- a/pkg/bbgo/graceful_shutdown.go +++ b/pkg/bbgo/graceful_shutdown.go @@ -30,5 +30,6 @@ func OnShutdown(ctx context.Context, f ShutdownHandler) { func Shutdown(shutdownCtx context.Context) { logrus.Infof("shutting down...") - defaultIsolation.gracefulShutdown.Shutdown(shutdownCtx) + isolatedContext := GetIsolationFromContext(shutdownCtx) + isolatedContext.gracefulShutdown.Shutdown(shutdownCtx) } From 5bbe4ecd57b3f72b0b87d5f64f3927001e36c958 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 8 Feb 2023 17:39:02 +0800 Subject: [PATCH 0381/1392] bbgo: check isolation context for log message --- pkg/bbgo/graceful_shutdown.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/graceful_shutdown.go b/pkg/bbgo/graceful_shutdown.go index 4ea578f1f2..a3f61b49fe 100644 --- a/pkg/bbgo/graceful_shutdown.go +++ b/pkg/bbgo/graceful_shutdown.go @@ -29,7 +29,13 @@ func OnShutdown(ctx context.Context, f ShutdownHandler) { } func Shutdown(shutdownCtx context.Context) { - logrus.Infof("shutting down...") + isolatedContext := GetIsolationFromContext(shutdownCtx) + if isolatedContext == defaultIsolation { + logrus.Infof("bbgo shutting down...") + } else { + logrus.Infof("bbgo shutting down (custom isolation)...") + } + isolatedContext.gracefulShutdown.Shutdown(shutdownCtx) } From 2fed98ea559c3baf9f57df99c295d819dc6360f8 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 9 Feb 2023 17:11:26 +0800 Subject: [PATCH 0382/1392] batch: fix JumpIfEmpty algorithm --- pkg/exchange/batch/time_range_query.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/batch/time_range_query.go b/pkg/exchange/batch/time_range_query.go index f78afbcac5..852b23e8f6 100644 --- a/pkg/exchange/batch/time_range_query.go +++ b/pkg/exchange/batch/time_range_query.go @@ -72,7 +72,13 @@ func (q *AsyncTimeRangedBatchQuery) Query(ctx context.Context, ch interface{}, s if listLen == 0 { if q.JumpIfEmpty > 0 { - startTime = startTime.Add(q.JumpIfEmpty) + startTime2 := startTime.Add(q.JumpIfEmpty) + if startTime2.After(endTime) { + startTime = endTime + endTime = startTime2 + } else { + startTime = startTime.Add(q.JumpIfEmpty) + } log.Debugf("batch querying %T: empty records jump to %s", q.Type, startTime) continue From 34ab53303a8bd53ebfc311c5ce4c37d4700e6d75 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 10 Feb 2023 17:22:19 +0800 Subject: [PATCH 0383/1392] grid2: fix upper price error --- pkg/strategy/grid2/grid.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index d886816c5f..dc8865fd99 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -40,13 +40,16 @@ func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) [] var ts = tickSize.Float64() var prec = int(math.Round(math.Log10(ts) * -1.0)) var pow10 = math.Pow10(prec) - for p := lower; p.Compare(upper) <= 0; p = p.Add(spread) { + for p := lower; p.Compare(upper.Sub(spread)) <= 0; p = p.Add(spread) { pp := math.Round(p.Float64()*pow10*10.0) / 10.0 pp = math.Trunc(pp) / pow10 pin := Pin(fixedpoint.NewFromFloat(pp)) pins = append(pins, pin) } + // this makes sure there is no error at the upper price + pins = append(pins, Pin(upper)) + return pins } From 4eca007d3dd0d64924efdadbdd05dba430c262ee Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 10 Feb 2023 17:51:50 +0800 Subject: [PATCH 0384/1392] grid2: fix upper price buy order issue --- pkg/strategy/grid2/strategy.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index a176d52956..cf38e4e7c0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -956,7 +956,8 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin }) usedBase = usedBase.Add(quantity) } else if i > 0 { - // next price + // if we don't have enough base asset + // then we need to place a buy order at the next price. nextPin := pins[i-1] nextPrice := fixedpoint.Value(nextPin) submitOrders = append(submitOrders, types.SubmitOrder{ @@ -975,10 +976,17 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin // skip i == 0 } } else { + // if price spread is not enabled, and we have already placed a sell order index on the top of this price, + // then we should skip if s.ProfitSpread.IsZero() && i+1 == si { continue } + // should never place a buy order at the upper price + if i == len(pins)-1 { + continue + } + submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, Type: types.OrderTypeLimit, From 3df846d878c3379b5875e7660c7b086af5db5a59 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 13 Feb 2023 14:05:52 +0800 Subject: [PATCH 0385/1392] grid2: fix quote investment algorithm --- pkg/strategy/grid2/strategy.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index cf38e4e7c0..05d95ae537 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -567,6 +567,11 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f continue } + // should never place a buy order at the upper price + if i == len(pins)-1 { + continue + } + totalQuotePrice = totalQuotePrice.Add(price) } } @@ -645,6 +650,11 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv continue } + // should never place a buy order at the upper price + if i == len(pins)-1 { + continue + } + totalQuotePrice = totalQuotePrice.Add(price) } } From 353c74ef5e466fa8ff0309d2c805cef230ae5307 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 13 Feb 2023 16:11:42 +0800 Subject: [PATCH 0386/1392] grid2: gridNum can not be zero or one --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 05d95ae537..41f3a5f1ca 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -166,8 +166,8 @@ func (s *Strategy) Validate() error { } } - if s.GridNum == 0 { - return fmt.Errorf("gridNum can not be zero") + if s.GridNum == 0 || s.GridNum == 1 { + return fmt.Errorf("gridNum can not be zero or one") } if !s.SkipSpreadCheck { From 26c7e03dc15ddcf06e4c6932725c204f5bf5d8de Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 14 Feb 2023 16:44:59 +0800 Subject: [PATCH 0387/1392] grid2: fix balance check --- pkg/strategy/grid2/strategy.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 41f3a5f1ca..da842b7ce3 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -795,19 +795,19 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) } // check if base and quote are enough + var totalBase = fixedpoint.Zero + var totalQuote = fixedpoint.Zero + baseBalance, ok := session.Account.Balance(s.Market.BaseCurrency) - if !ok { - return fmt.Errorf("base %s balance not found", s.Market.BaseCurrency) + if ok { + totalBase = baseBalance.Available } quoteBalance, ok := session.Account.Balance(s.Market.QuoteCurrency) - if !ok { - return fmt.Errorf("quote %s balance not found", s.Market.QuoteCurrency) + if ok { + totalQuote = quoteBalance.Available } - totalBase := baseBalance.Available - totalQuote := quoteBalance.Available - // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. if s.QuantityOrAmount.IsSet() { From 3bf24d97f0874bd8a272b768de0c2e9f29fb573e Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 14:42:01 +0800 Subject: [PATCH 0388/1392] grid2: add HasPrice test --- pkg/strategy/grid2/grid_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index b5fdaff982..cc5b5662de 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -126,6 +126,19 @@ func TestGrid_NextLowerPin(t *testing.T) { assert.Equal(t, Pin(fixedpoint.Zero), next) } +func TestGrid_HasPrice(t *testing.T) { + upper := number(500.0) + lower := number(100.0) + size := number(5.0) + grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculateArithmeticPins() + + assert.True(t, grid.HasPrice(number(500.0)), "upper price") + assert.True(t, grid.HasPrice(number(100.0)), "lower price") + assert.True(t, grid.HasPrice(number(200.0)), "found 200 price ok") + assert.True(t, grid.HasPrice(number(300.0)), "found 300 price ok") +} + func TestGrid_NextHigherPin(t *testing.T) { upper := number(500.0) lower := number(100.0) From 2fecf0dc79453546975fdd2a77139a4f958d800e Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 14:57:21 +0800 Subject: [PATCH 0389/1392] grid2: fix HasPrice --- pkg/strategy/grid2/grid.go | 9 ++++++- pkg/strategy/grid2/grid_test.go | 45 +++++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index dc8865fd99..4cf611e5f6 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -141,8 +141,15 @@ func (g *Grid) NextLowerPin(price fixedpoint.Value) (Pin, bool) { } func (g *Grid) HasPrice(price fixedpoint.Value) bool { + if _, exists := g.pinsCache[Pin(price)]; exists { + return exists + } + i := g.SearchPin(price) - return fixedpoint.Value(g.Pins[i]).Compare(price) == 0 + if i >= 0 && i < len(g.Pins) { + return fixedpoint.Value(g.Pins[i]).Compare(price) == 0 + } + return false } func (g *Grid) SearchPin(price fixedpoint.Value) int { diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index cc5b5662de..500f1db78a 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -127,16 +127,41 @@ func TestGrid_NextLowerPin(t *testing.T) { } func TestGrid_HasPrice(t *testing.T) { - upper := number(500.0) - lower := number(100.0) - size := number(5.0) - grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculateArithmeticPins() - - assert.True(t, grid.HasPrice(number(500.0)), "upper price") - assert.True(t, grid.HasPrice(number(100.0)), "lower price") - assert.True(t, grid.HasPrice(number(200.0)), "found 200 price ok") - assert.True(t, grid.HasPrice(number(300.0)), "found 300 price ok") + t.Run("case1", func(t *testing.T) { + upper := number(500.0) + lower := number(100.0) + size := number(5.0) + grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculateArithmeticPins() + + assert.True(t, grid.HasPrice(number(500.0)), "upper price") + assert.True(t, grid.HasPrice(number(100.0)), "lower price") + assert.True(t, grid.HasPrice(number(200.0)), "found 200 price ok") + assert.True(t, grid.HasPrice(number(300.0)), "found 300 price ok") + }) + + t.Run("case2", func(t *testing.T) { + upper := number(0.9) + lower := number(0.1) + size := number(7.0) + grid := NewGrid(lower, upper, size, number(0.00000001)) + grid.CalculateArithmeticPins() + + assert.Equal(t, []Pin{ + Pin(number(0.1)), + Pin(number(0.23333333)), + Pin(number(0.36666666)), + Pin(number(0.49999999)), + Pin(number(0.63333332)), + Pin(number(0.76666665)), + Pin(number(0.9)), + }, grid.Pins) + + assert.False(t, grid.HasPrice(number(200.0)), "out of range") + assert.True(t, grid.HasPrice(number(0.9)), "upper price") + assert.True(t, grid.HasPrice(number(0.1)), "lower price") + assert.True(t, grid.HasPrice(number(0.49999999)), "found 0.49999999 price ok") + }) } func TestGrid_NextHigherPin(t *testing.T) { From d1cbc6a9cabc312a1f1b61798bf36f7398550a6a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 15:40:14 +0800 Subject: [PATCH 0390/1392] grid2: add one more quote investment test case --- pkg/strategy/grid2/strategy_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 99a7a84631..216d65931c 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -218,6 +218,7 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { } func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { + t.Run("quote quantity", func(t *testing.T) { // quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000) * q // q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000) @@ -237,6 +238,23 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { assert.Equal(t, number(0.2).String(), quantity.String()) }) + t.Run("quote quantity #2", func(t *testing.T) { + s := newTestStrategy() + lastPrice := number(160.0) + quoteInvestment := number(1_000.0) + quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, []Pin{ + Pin(number(100.0)), // buy + Pin(number(116.67)), // buy + Pin(number(133.33)), // buy + Pin(number(150.00)), // buy + Pin(number(166.67)), // buy + Pin(number(183.33)), + Pin(number(200.00)), + }) + assert.NoError(t, err) + assert.Equal(t, number(1.17647058).String(), quantity.String()) + }) + t.Run("profit spread", func(t *testing.T) { // quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000) * q // q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000) From 79f9f9c5bb111d67ad99e37673bfd15a238d1481 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 15:41:20 +0800 Subject: [PATCH 0391/1392] grid2: pull out quote investment variable --- pkg/strategy/grid2/strategy_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 216d65931c..29346932d7 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -226,7 +226,8 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { // q = 0.2 s := newTestStrategy() lastPrice := number(13_500.0) - quantity, err := s.calculateQuoteInvestmentQuantity(number(12_000.0), lastPrice, []Pin{ + quoteInvestment := number(12_000.0) + quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, []Pin{ Pin(number(10_000.0)), // buy Pin(number(11_000.0)), // buy Pin(number(12_000.0)), // buy @@ -263,7 +264,8 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { s := newTestStrategy() s.ProfitSpread = number(2000.0) lastPrice := number(13_500.0) - quantity, err := s.calculateQuoteInvestmentQuantity(number(7500.0), lastPrice, []Pin{ + quoteInvestment := number(7500.0) + quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, []Pin{ Pin(number(10_000.0)), // sell order @ 12_000 Pin(number(11_000.0)), // sell order @ 13_000 Pin(number(12_000.0)), // sell order @ 14_000 From 276149b378bf986bcd4f378f272979bbe8023610 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 15:49:40 +0800 Subject: [PATCH 0392/1392] grid2: improve base+quote logging --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index da842b7ce3..d5c424e363 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -614,7 +614,7 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv } s.logger.Infof("grid base investment sell orders: %d", maxNumberOfSellOrders) if maxNumberOfSellOrders > 0 { - s.logger.Infof("grid base investment quantity range: %f <=> %f", minBaseQuantity.Float64(), maxBaseQuantity.Float64()) + s.logger.Infof("grid base investment quantity: %f (base investment) / %d (number of sell orders) = %f (base quantity per order)", baseInvestment.Float64(), maxNumberOfSellOrders, maxBaseQuantity.Float64()) } // calculate quantity with quote investment From 8ef86858e22d8eea4980f06f8e6014212bd9c902 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 15:54:49 +0800 Subject: [PATCH 0393/1392] grid2: fix calculateBaseQuoteInvestmentQuantity logging --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index d5c424e363..194481ad38 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -579,8 +579,8 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f return quoteInvestment.Div(totalQuotePrice), nil } -func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { - s.logger.Infof("calculating quantity by quote/base investment: %f / %f", baseInvestment.Float64(), quoteInvestment.Float64()) +func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { + s.logger.Infof("calculating quantity by base/quote investment: %f / %f", baseInvestment.Float64(), quoteInvestment.Float64()) // q_p1 = q_p2 = q_p3 = q_p4 // baseInvestment = q_p1 + q_p2 + q_p3 + q_p4 + .... // baseInvestment = numberOfSellOrders * q @@ -824,7 +824,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) } else { // calculate the quantity from the investment configuration if !s.QuoteInvestment.IsZero() && !s.BaseInvestment.IsZero() { - quantity, err2 := s.calculateQuoteBaseInvestmentQuantity(s.QuoteInvestment, s.BaseInvestment, lastPrice, s.grid.Pins) + quantity, err2 := s.calculateBaseQuoteInvestmentQuantity(s.QuoteInvestment, s.BaseInvestment, lastPrice, s.grid.Pins) if err2 != nil { return err2 } From a7e100563a5fe8e7330ef7b64135bfae81bb746b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 16:03:24 +0800 Subject: [PATCH 0394/1392] grid2: add test case --- pkg/strategy/grid2/grid_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 500f1db78a..1c2cfd5fac 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -162,6 +162,31 @@ func TestGrid_HasPrice(t *testing.T) { assert.True(t, grid.HasPrice(number(0.1)), "lower price") assert.True(t, grid.HasPrice(number(0.49999999)), "found 0.49999999 price ok") }) + + t.Run("case3", func(t *testing.T) { + upper := number(0.9) + lower := number(0.1) + size := number(7.0) + grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculateArithmeticPins() + + assert.Equal(t, []Pin{ + Pin(number(0.1)), + Pin(number(0.23)), + Pin(number(0.36)), + Pin(number(0.50)), + Pin(number(0.63)), + Pin(number(0.76)), + Pin(number(0.9)), + }, grid.Pins) + + assert.False(t, grid.HasPrice(number(200.0)), "out of range") + assert.True(t, grid.HasPrice(number(0.9)), "upper price") + assert.True(t, grid.HasPrice(number(0.1)), "lower price") + assert.True(t, grid.HasPrice(number(0.5)), "found 0.5 price ok") + assert.True(t, grid.HasPrice(number(0.23)), "found 0.23 price ok") + }) + } func TestGrid_NextHigherPin(t *testing.T) { From 44210bf26a7cd0c135a38cb6293c7df47d8c3cd8 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 16:05:45 +0800 Subject: [PATCH 0395/1392] grid2: adjust test case tick size --- pkg/strategy/grid2/grid_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 1c2cfd5fac..9c653485e0 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -167,16 +167,16 @@ func TestGrid_HasPrice(t *testing.T) { upper := number(0.9) lower := number(0.1) size := number(7.0) - grid := NewGrid(lower, upper, size, number(0.01)) + grid := NewGrid(lower, upper, size, number(0.0001)) grid.CalculateArithmeticPins() assert.Equal(t, []Pin{ Pin(number(0.1)), - Pin(number(0.23)), - Pin(number(0.36)), - Pin(number(0.50)), - Pin(number(0.63)), - Pin(number(0.76)), + Pin(number(0.2333)), + Pin(number(0.3666)), + Pin(number(0.5000)), + Pin(number(0.6333)), + Pin(number(0.7666)), Pin(number(0.9)), }, grid.Pins) @@ -184,7 +184,7 @@ func TestGrid_HasPrice(t *testing.T) { assert.True(t, grid.HasPrice(number(0.9)), "upper price") assert.True(t, grid.HasPrice(number(0.1)), "lower price") assert.True(t, grid.HasPrice(number(0.5)), "found 0.5 price ok") - assert.True(t, grid.HasPrice(number(0.23)), "found 0.23 price ok") + assert.True(t, grid.HasPrice(number(0.2333)), "found 0.2333 price ok") }) } From 35bfdfab8dd62b4173f5d7d60e38fad9eb66fdc3 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 16:51:12 +0800 Subject: [PATCH 0396/1392] grid2: fix si index check --- pkg/strategy/grid2/strategy.go | 2 +- pkg/strategy/grid2/strategy_test.go | 50 ++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 194481ad38..c3d63745d9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -934,7 +934,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin var submitOrders []types.SubmitOrder // si is for sell order price index - var si = len(pins) - 1 + var si = len(pins) for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 29346932d7..7782de10db 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -77,7 +77,9 @@ func TestStrategy_generateGridOrders(t *testing.T) { s.QuantityOrAmount.Quantity = number(0.01) lastPrice := number(15300) - orders, err := s.generateGridOrders(number(10000.0), number(0), lastPrice) + quoteInvestment := number(10000.0) + baseInvestment := number(0) + orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice) assert.NoError(t, err) if !assert.Equal(t, 10, len(orders)) { for _, o := range orders { @@ -99,6 +101,52 @@ func TestStrategy_generateGridOrders(t *testing.T) { }, orders) }) + t.Run("quote only + buy only", func(t *testing.T) { + s := newTestStrategy() + s.UpperPrice = number(0.9) + s.LowerPrice = number(0.1) + s.GridNum = 7 + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + + assert.Equal(t, []Pin{ + Pin(number(0.1)), + Pin(number(0.23)), + Pin(number(0.36)), + Pin(number(0.50)), + Pin(number(0.63)), + Pin(number(0.76)), + Pin(number(0.9)), + }, s.grid.Pins, "pins are correct") + + lastPrice := number(22100) + quoteInvestment := number(100.0) + baseInvestment := number(0) + + quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, s.grid.Pins) + assert.NoError(t, err) + assert.Equal(t, number(38.75968992).String(), quantity.String()) + + s.QuantityOrAmount.Quantity = quantity + + orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice) + assert.NoError(t, err) + if !assert.Equal(t, 6, len(orders)) { + for _, o := range orders { + t.Logf("- %s %s", o.Price.String(), o.Side) + } + } + + assertPriceSide(t, []PriceSideAssert{ + {number(0.76), types.SideTypeBuy}, + {number(0.63), types.SideTypeBuy}, + {number(0.5), types.SideTypeBuy}, + {number(0.36), types.SideTypeBuy}, + {number(0.23), types.SideTypeBuy}, + {number(0.1), types.SideTypeBuy}, + }, orders) + }) + t.Run("base + quote", func(t *testing.T) { s := newTestStrategy() s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) From e73081d6ba8fbb2acd954fdbe3d16e9ec52812fc Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 17:33:07 +0800 Subject: [PATCH 0397/1392] grid2: add logFields config --- pkg/strategy/grid2/strategy.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c3d63745d9..bd4a3ef2ab 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -120,6 +120,8 @@ type Strategy struct { // OrderGroupID is the group ID used for the strategy instance for canceling orders OrderGroupID uint32 `json:"orderGroupID"` + LogFields logrus.Fields `json:"logFields"` + // FeeRate is used for calculating the minimal profit spread. // it makes sure that your grid configuration is profitable. FeeRate fixedpoint.Value `json:"feeRate"` @@ -1344,9 +1346,12 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.orderQueryService = service } - s.logger = log.WithFields(logrus.Fields{ - "symbol": s.Symbol, - }) + if s.LogFields == nil { + s.LogFields = logrus.Fields{} + } + + s.LogFields["symbol"] = s.Symbol + s.logger = log.WithFields(s.LogFields) if s.OrderGroupID == 0 { s.OrderGroupID = util.FNV32(instanceID) From 88116440ba356ef565d0c9d62f16a00bb22d0155 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 17:38:48 +0800 Subject: [PATCH 0398/1392] grid2: define strategy field in the logger entry --- pkg/strategy/grid2/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index bd4a3ef2ab..28052ef931 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1351,6 +1351,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } s.LogFields["symbol"] = s.Symbol + s.LogFields["strategy"] = ID s.logger = log.WithFields(s.LogFields) if s.OrderGroupID == 0 { From 6dfd18bd49e68a2bb4c13c8b750b8f63909b00ae Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 21:49:25 +0800 Subject: [PATCH 0399/1392] grid2: run recoverGrid only when user data stream is started --- pkg/strategy/grid2/strategy.go | 54 +++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 28052ef931..4ef5eb38b4 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1078,7 +1078,7 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { return nil } -func (s *Strategy) recoverGrid(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { +func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { grid := s.newGrid() // Add all open orders to the local order book @@ -1444,26 +1444,6 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } } - if s.RecoverOrdersWhenStart { - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) - if err != nil { - return err - } - - s.logger.Infof("recoverWhenStart is set, found %d open orders, trying to recover grid orders...", len(openOrders)) - - if len(openOrders) > 0 { - historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) - if !implemented { - s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") - } else { - if err := s.recoverGrid(ctx, historyService, openOrders); err != nil { - return errors.Wrap(err, "recover grid error") - } - } - } - } - bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() @@ -1492,6 +1472,14 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. // if TriggerPrice is zero, that means we need to open the grid when start up if s.TriggerPrice.IsZero() { session.UserDataStream.OnStart(func() { + // do recover only when triggerPrice is not set. + if s.RecoverOrdersWhenStart { + s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") + if err := s.recoverGrid(ctx, session); err != nil { + log.WithError(err).Errorf("recover error") + } + } + if err := s.openGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("failed to setup grid orders") } @@ -1501,6 +1489,30 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. return nil } +func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } + + s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) + + // do recover only when openOrders > 0 + if len(openOrders) > 0 { + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) + if !implemented { + s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") + return nil + } + + if err := s.recoverGridWithOpenOrders(ctx, historyService, openOrders); err != nil { + return errors.Wrap(err, "recover grid error") + } + } + + return nil +} + // openOrdersMismatches verifies if the open orders are on the grid pins // return true if mismatches func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.ExchangeSession) (bool, error) { From ec8e50822a6dcc325926bf6ca24f88dd584f9b53 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 21:51:22 +0800 Subject: [PATCH 0400/1392] grid2: do not place sell order at price[0] --- pkg/strategy/grid2/strategy.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 4ef5eb38b4..553dc49ea7 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -955,6 +955,12 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... if price.Compare(lastPrice) >= 0 { si = i + + // do not place sell order when i == 0 + if i == 0 { + continue + } + if usedBase.Add(quantity).Compare(totalBase) < 0 { submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, @@ -967,7 +973,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin Tag: orderTag, }) usedBase = usedBase.Add(quantity) - } else if i > 0 { + } else { // if we don't have enough base asset // then we need to place a buy order at the next price. nextPin := pins[i-1] @@ -984,8 +990,6 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin }) quoteQuantity := quantity.Mul(price) usedQuote = usedQuote.Add(quoteQuantity) - } else if i == 0 { - // skip i == 0 } } else { // if price spread is not enabled, and we have already placed a sell order index on the top of this price, From 9ab4c457274bfc180ae4ac5efc657e3ace760a83 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 22:17:36 +0800 Subject: [PATCH 0401/1392] grid2: remove buildGridPriceMap since we have HasPrice method --- pkg/strategy/grid2/pricemap.go | 11 ----------- pkg/strategy/grid2/strategy.go | 25 ++++++++++++++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/strategy/grid2/pricemap.go b/pkg/strategy/grid2/pricemap.go index 04ae59711c..e961630767 100644 --- a/pkg/strategy/grid2/pricemap.go +++ b/pkg/strategy/grid2/pricemap.go @@ -3,14 +3,3 @@ package grid2 import "github.com/c9s/bbgo/pkg/fixedpoint" type PriceMap map[string]fixedpoint.Value - -func buildGridPriceMap(grid *Grid) PriceMap { - // Add all open orders to the local order book - gridPriceMap := make(PriceMap) - for _, pin := range grid.Pins { - price := fixedpoint.Value(pin) - gridPriceMap[price.String()] = price - } - - return gridPriceMap -} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 553dc49ea7..934115c183 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -36,6 +36,19 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +type PrettyPins []Pin + +func (pp PrettyPins) String() string { + var ss []string + + for _, p := range pp { + price := fixedpoint.Value(p) + ss = append(ss, price.String()) + } + + return fmt.Sprintf("%v", ss) +} + //go:generate mockgen -destination=mocks/order_executor.go -package=mocks . OrderExecutor type OrderExecutor interface { SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) @@ -1085,9 +1098,6 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { grid := s.newGrid() - // Add all open orders to the local order book - gridPriceMap := buildGridPriceMap(grid) - lastOrderID := uint64(1) now := time.Now() firstOrderTime := now.AddDate(0, 0, -7) @@ -1115,7 +1125,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService // Ensure that orders are grid orders // The price must be at the grid pin for _, openOrder := range openOrders { - if _, exists := gridPriceMap[openOrder.Price.String()]; exists { + if grid.HasPrice(openOrder.Price) { orderBook.Add(openOrder) // put the order back to the active order book so that we can receive order update @@ -1124,11 +1134,13 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService } // if all open orders are the grid orders, then we don't have to recover + s.logger.Infof("GRID RECOVER: verifying pins %v", PrettyPins(grid.Pins)) missingPrices := scanMissingPinPrices(orderBook, grid.Pins) if numMissing := len(missingPrices); numMissing <= 1 { s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") return nil } else { + s.logger.Infof("GRID RECOVER: found missing prices: %v", missingPrices) // Note that for MAX Exchange, the order history API only uses fromID parameter to query history order. // The time range does not matter. // TODO: handle context correctly @@ -1200,8 +1212,6 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService // replayOrderHistory queries the closed order history from the API and rebuild the orderbook from the order history. // startTime, endTime is the time range of the order history. func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook *bbgo.ActiveOrderBook, historyService types.ExchangeTradeHistoryService, startTime, endTime time.Time, lastOrderID uint64) error { - gridPriceMap := buildGridPriceMap(grid) - // a simple guard, in reality, this startTime is not possible to exceed the endTime // because the queries closed orders might still in the range. orderIdChanged := true @@ -1241,7 +1251,8 @@ func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook } // skip non-grid order prices - if _, ok := gridPriceMap[closedOrder.Price.String()]; !ok { + + if !grid.HasPrice(closedOrder.Price) { continue } From 62b8863ca69106627827911ef59b83525123e4ac Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 22:32:55 +0800 Subject: [PATCH 0402/1392] grid2: fix pin price precision --- pkg/strategy/grid2/grid.go | 9 ++++++--- pkg/strategy/grid2/grid_test.go | 21 +++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 2 ++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 4cf611e5f6..517dd443c4 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -4,6 +4,7 @@ import ( "fmt" "math" "sort" + "strconv" "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -43,8 +44,10 @@ func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) [] for p := lower; p.Compare(upper.Sub(spread)) <= 0; p = p.Add(spread) { pp := math.Round(p.Float64()*pow10*10.0) / 10.0 pp = math.Trunc(pp) / pow10 - pin := Pin(fixedpoint.NewFromFloat(pp)) - pins = append(pins, pin) + + pps := strconv.FormatFloat(pp, 'f', prec, 64) + price := fixedpoint.MustNewFromString(pps) + pins = append(pins, Pin(price)) } // this makes sure there is no error at the upper price @@ -210,5 +213,5 @@ func (g *Grid) updatePinsCache() { } func (g *Grid) String() string { - return fmt.Sprintf("GRID: priceRange: %f <=> %f size: %f spread: %f", g.LowerPrice.Float64(), g.UpperPrice.Float64(), g.Size.Float64(), g.Spread.Float64()) + return fmt.Sprintf("GRID: priceRange: %f <=> %f size: %f spread: %f tickSize: %f", g.LowerPrice.Float64(), g.UpperPrice.Float64(), g.Size.Float64(), g.Spread.Float64(), g.TickSize.Float64()) } diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 9c653485e0..20de41626c 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -187,6 +187,27 @@ func TestGrid_HasPrice(t *testing.T) { assert.True(t, grid.HasPrice(number(0.2333)), "found 0.2333 price ok") }) + t.Run("case4", func(t *testing.T) { + upper := number(90.0) + lower := number(10.0) + size := number(7.0) + grid := NewGrid(lower, upper, size, number(0.001)) + grid.CalculateArithmeticPins() + + assert.Equal(t, []Pin{ + Pin(number("10.0")), + Pin(number("23.333")), + Pin(number("36.666")), + Pin(number("50.00")), + Pin(number("63.333")), + Pin(number("76.666")), + Pin(number("90.0")), + }, grid.Pins) + + assert.False(t, grid.HasPrice(number(200.0)), "out of range") + assert.True(t, grid.HasPrice(number("36.666")), "found 36.666 price ok") + }) + } func TestGrid_NextHigherPin(t *testing.T) { diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 934115c183..cc1695ae0a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1098,6 +1098,8 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { grid := s.newGrid() + s.logger.Infof("GRID RECOVER: %s", grid.String()) + lastOrderID := uint64(1) now := time.Now() firstOrderTime := now.AddDate(0, 0, -7) From a9f1aab4b1f2b2e2656e498b7490823dfa218bf2 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 22:42:46 +0800 Subject: [PATCH 0403/1392] grid2: add user data stream on start log --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index cc1695ae0a..e0e7e5ae9f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1489,6 +1489,8 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. // if TriggerPrice is zero, that means we need to open the grid when start up if s.TriggerPrice.IsZero() { session.UserDataStream.OnStart(func() { + s.logger.Infof("user data stream started, initializing grid...") + // do recover only when triggerPrice is not set. if s.RecoverOrdersWhenStart { s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") From fa3106eefa19709140d31e55871dedb5af94db4f Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Feb 2023 22:44:07 +0800 Subject: [PATCH 0404/1392] grid2: set the grid field if there is no missing orders --- pkg/strategy/grid2/strategy.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e0e7e5ae9f..816508494b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1140,6 +1140,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService missingPrices := scanMissingPinPrices(orderBook, grid.Pins) if numMissing := len(missingPrices); numMissing <= 1 { s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") + s.setGrid(grid) return nil } else { s.logger.Infof("GRID RECOVER: found missing prices: %v", missingPrices) @@ -1181,6 +1182,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService // if all orders on the order book are active orders, we don't need to recover. if isCompleteGridOrderBook(orderBook, s.GridNum) { s.logger.Infof("GRID RECOVER: all orders are active orders, do not need recover") + s.setGrid(grid) return nil } @@ -1196,10 +1198,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService filledOrders := types.OrdersFilled(tmpOrders) s.logger.Infof("GRID RECOVER: found %d filled grid orders", len(filledOrders)) - - s.mu.Lock() - s.grid = grid - s.mu.Unlock() + s.setGrid(grid) for _, o := range filledOrders { s.processFilledOrder(o) @@ -1211,6 +1210,12 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService return nil } +func (s *Strategy) setGrid(grid *Grid) { + s.mu.Lock() + s.grid = grid + s.mu.Unlock() +} + // replayOrderHistory queries the closed order history from the API and rebuild the orderbook from the order history. // startTime, endTime is the time range of the order history. func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook *bbgo.ActiveOrderBook, historyService types.ExchangeTradeHistoryService, startTime, endTime time.Time, lastOrderID uint64) error { From 9b69fa54652d325d36b257cf8fffd7a986b63f75 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 16 Feb 2023 14:56:28 +0800 Subject: [PATCH 0405/1392] grid2: log calculateQuoteInvestmentQuantity result --- pkg/strategy/grid2/strategy.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 816508494b..29baa71e5c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -552,25 +552,30 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am } func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { - // quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + .... // => // quoteInvestment = (p1 + p2 + p3) * q // q = quoteInvestment / (p1 + p2 + p3) totalQuotePrice := fixedpoint.Zero - si := -1 + si := len(pins) for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) if price.Compare(lastPrice) >= 0 { si = i + + // do not place sell order on the bottom price + if i == 0 { + continue + } + // for orders that sell // if we still have the base balance // quantity := amount.Div(lastPrice) if s.ProfitSpread.Sign() > 0 { totalQuotePrice = totalQuotePrice.Add(price) - } else if i > 0 { // we do not want to sell at i == 0 + } else { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) @@ -591,7 +596,9 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f } } - return quoteInvestment.Div(totalQuotePrice), nil + q := quoteInvestment.Div(totalQuotePrice) + s.logger.Infof("calculateQuoteInvestmentQuantity: sumOfPrice=%f quantity=%f", totalQuotePrice.Float64(), q.Float64()) + return q, nil } func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { From 9e3383606ed8b2598cfdadac2109e65f10a29fca Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 16 Feb 2023 18:11:04 +0800 Subject: [PATCH 0406/1392] grid2: add quote quantity test case --- pkg/strategy/grid2/strategy_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 7782de10db..14882e9cc0 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -304,6 +304,35 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { assert.Equal(t, number(1.17647058).String(), quantity.String()) }) + t.Run("quote quantity #3", func(t *testing.T) { + s := newTestStrategy() + lastPrice := number(22000.0) + quoteInvestment := number(100.0) + pins := []Pin{ + Pin(number(0.1)), + Pin(number(0.23)), + Pin(number(0.36)), + Pin(number(0.50)), + Pin(number(0.63)), + Pin(number(0.76)), + Pin(number(0.90)), + } + quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, pins) + assert.NoError(t, err) + assert.InDelta(t, 38.75968992, quantity.Float64(), 0.0001) + + var totalQuoteUsed = fixedpoint.Zero + for i, pin := range pins { + if i == len(pins)-1 { + continue + } + + price := fixedpoint.Value(pin) + totalQuoteUsed = totalQuoteUsed.Add(price.Mul(quantity)) + } + assert.LessOrEqualf(t, totalQuoteUsed, number(100.0), "total quote used: %f", totalQuoteUsed.Float64()) + }) + t.Run("profit spread", func(t *testing.T) { // quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000) * q // q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000) From 7ba0e86605460f986f1448fddd742ce215844183 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 16 Feb 2023 18:11:38 +0800 Subject: [PATCH 0407/1392] grid2: use setGrid with mutex --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 29baa71e5c..62cde968c1 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -785,7 +785,7 @@ func (s *Strategy) CloseGrid(ctx context.Context) error { } // free the grid object - s.grid = nil + s.setGrid(nil) s.EmitGridClosed() return nil } From f039c97e63ad9923dfeb54c292eac67e032f246a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 16 Feb 2023 18:12:08 +0800 Subject: [PATCH 0408/1392] grid2: defer EmitCloseGrid callback earlier --- pkg/strategy/grid2/strategy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 62cde968c1..378fc0893e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -776,6 +776,8 @@ func (s *Strategy) OpenGrid(ctx context.Context) error { func (s *Strategy) CloseGrid(ctx context.Context) error { s.logger.Infof("closing %s grid", s.Symbol) + defer s.EmitGridClosed() + bbgo.Sync(ctx, s) // now we can cancel the open orders @@ -786,7 +788,6 @@ func (s *Strategy) CloseGrid(ctx context.Context) error { // free the grid object s.setGrid(nil) - s.EmitGridClosed() return nil } From eb4e25c0089485dc17fdc5e60459b6da9d6ff767 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 16 Feb 2023 18:13:51 +0800 Subject: [PATCH 0409/1392] grid2: emit grid ready earlier --- pkg/strategy/grid2/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 378fc0893e..8b2fc49dd5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -884,6 +884,9 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) s.debugGridOrders(submitOrders, lastPrice) + // try to always emit grid ready + defer s.EmitGridReady() + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...) if err2 != nil { return err @@ -911,7 +914,6 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) } s.logger.Infof("ALL GRID ORDERS SUBMITTED") - s.EmitGridReady() s.updateOpenOrderPricesMetrics(createdOrders) return nil From 2aee3cea5991148c81a57aed517e483f58a4807c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 16 Feb 2023 21:33:42 +0800 Subject: [PATCH 0410/1392] grid2: emit grid ready once the grid is recovered --- pkg/strategy/grid2/strategy.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 8b2fc49dd5..3dc7e9aeb8 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1151,6 +1151,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService if numMissing := len(missingPrices); numMissing <= 1 { s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") s.setGrid(grid) + s.EmitGridReady() return nil } else { s.logger.Infof("GRID RECOVER: found missing prices: %v", missingPrices) @@ -1193,6 +1194,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService if isCompleteGridOrderBook(orderBook, s.GridNum) { s.logger.Infof("GRID RECOVER: all orders are active orders, do not need recover") s.setGrid(grid) + s.EmitGridReady() return nil } @@ -1209,6 +1211,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.logger.Infof("GRID RECOVER: found %d filled grid orders", len(filledOrders)) s.setGrid(grid) + s.EmitGridReady() for _, o := range filledOrders { s.processFilledOrder(o) From 156da926701d38480b422a63613fe2d08c92872f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 16 Feb 2023 21:38:48 +0800 Subject: [PATCH 0411/1392] grid2: check used quote balance before we generate the grid order --- pkg/strategy/grid2/strategy.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3dc7e9aeb8..eaa41ea4f9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1026,6 +1026,13 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin continue } + quoteQuantity := quantity.Mul(price) + + if usedQuote.Add(quoteQuantity).Compare(totalQuote) > 0 { + s.logger.Warnf("used quote %f > total quote %f, this should not happen", usedQuote.Float64(), totalQuote.Float64()) + continue + } + submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, Type: types.OrderTypeLimit, @@ -1036,7 +1043,6 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin TimeInForce: types.TimeInForceGTC, Tag: orderTag, }) - quoteQuantity := quantity.Mul(price) usedQuote = usedQuote.Add(quoteQuantity) } } From 55476e417677cf9325d89bb2821c729ec0ddd1db Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 16 Feb 2023 21:55:53 +0800 Subject: [PATCH 0412/1392] grid2: include the order dust for the quote investment calculation --- pkg/strategy/grid2/strategy.go | 11 +++++++++-- pkg/strategy/grid2/strategy_test.go | 10 +++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index eaa41ea4f9..56796d98c2 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -3,6 +3,7 @@ package grid2 import ( "context" "fmt" + "math" "sort" "strconv" "sync" @@ -558,6 +559,7 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f // q = quoteInvestment / (p1 + p2 + p3) totalQuotePrice := fixedpoint.Zero si := len(pins) + cntOrder := 0 for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) @@ -581,6 +583,8 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f nextLowerPrice := fixedpoint.Value(nextLowerPin) totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) } + + cntOrder++ } else { // for orders that buy if s.ProfitSpread.IsZero() && i+1 == si { @@ -593,11 +597,14 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f } totalQuotePrice = totalQuotePrice.Add(price) + cntOrder++ } } - q := quoteInvestment.Div(totalQuotePrice) - s.logger.Infof("calculateQuoteInvestmentQuantity: sumOfPrice=%f quantity=%f", totalQuotePrice.Float64(), q.Float64()) + orderDusts := fixedpoint.NewFromFloat(math.Pow10(-s.Market.PricePrecision) * float64(cntOrder)) + adjustedQuoteInvestment := quoteInvestment.Sub(orderDusts) + q := adjustedQuoteInvestment.Div(totalQuotePrice) + s.logger.Infof("calculateQuoteInvestmentQuantity: adjustedQuoteInvestment=%f sumOfPrice=%f quantity=%f", adjustedQuoteInvestment.Float64(), totalQuotePrice.Float64(), q.Float64()) return q, nil } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 14882e9cc0..bcafd4ad78 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -125,7 +125,7 @@ func TestStrategy_generateGridOrders(t *testing.T) { quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, s.grid.Pins) assert.NoError(t, err) - assert.Equal(t, number(38.75968992).String(), quantity.String()) + assert.InDelta(t, 38.7364341, quantity.Float64(), 0.00001) s.QuantityOrAmount.Quantity = quantity @@ -284,7 +284,7 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { Pin(number(15_000.0)), }) assert.NoError(t, err) - assert.Equal(t, number(0.2).String(), quantity.String()) + assert.InDelta(t, 0.199999916, quantity.Float64(), 0.0001) }) t.Run("quote quantity #2", func(t *testing.T) { @@ -301,7 +301,7 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { Pin(number(200.00)), }) assert.NoError(t, err) - assert.Equal(t, number(1.17647058).String(), quantity.String()) + assert.InDelta(t, 1.1764, quantity.Float64(), 0.00001) }) t.Run("quote quantity #3", func(t *testing.T) { @@ -319,7 +319,7 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { } quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, pins) assert.NoError(t, err) - assert.InDelta(t, 38.75968992, quantity.Float64(), 0.0001) + assert.InDelta(t, 38.736434, quantity.Float64(), 0.0001) var totalQuoteUsed = fixedpoint.Zero for i, pin := range pins { @@ -351,7 +351,7 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { Pin(number(15_000.0)), // sell order @ 17_000 }) assert.NoError(t, err) - assert.Equal(t, number(0.1).String(), quantity.String()) + assert.InDelta(t, 0.099992, quantity.Float64(), 0.0001) }) } From 56628aca739ee3b9cb70510acbcaf8cdfcceb1e1 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 16 Feb 2023 22:49:22 +0800 Subject: [PATCH 0413/1392] grid2: emit grid ready only when there is no error --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 56796d98c2..11ecc086da 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -891,14 +891,14 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) s.debugGridOrders(submitOrders, lastPrice) - // try to always emit grid ready - defer s.EmitGridReady() - createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...) if err2 != nil { return err } + // try to always emit grid ready + defer s.EmitGridReady() + // update the number of orders to metrics baseLabels := s.newPrometheusLabels() metricsGridNumOfOrders.With(baseLabels).Set(float64(len(createdOrders))) From 29692b0e1a8f00f86c83ecd2fa272403ea8819fe Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 17 Feb 2023 17:16:25 +0800 Subject: [PATCH 0414/1392] grid2: fix MinimalQuoteInvestment check --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 11ecc086da..6b3902496b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1100,7 +1100,7 @@ func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.Exchange } func calculateMinimalQuoteInvestment(market types.Market, lowerPrice, upperPrice fixedpoint.Value, gridNum int64) fixedpoint.Value { - num := fixedpoint.NewFromInt(gridNum) + num := fixedpoint.NewFromInt(gridNum - 1) minimalAmountLowerPrice := fixedpoint.Max(lowerPrice.Mul(market.MinQuantity), market.MinNotional) minimalAmountUpperPrice := fixedpoint.Max(upperPrice.Mul(market.MinQuantity), market.MinNotional) return fixedpoint.Max(minimalAmountLowerPrice, minimalAmountUpperPrice).Mul(num) From a5e134e98d8b5188fbbb038f713e9d57d72eb3cf Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 17 Feb 2023 17:32:43 +0800 Subject: [PATCH 0415/1392] grid2: fix calculateMinimalQuoteInvestment tests --- pkg/strategy/grid2/strategy_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index bcafd4ad78..0e5408c13f 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -895,7 +895,7 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { s.QuoteInvestment = number(10_000) s.GridNum = 10 minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, s.LowerPrice, s.UpperPrice, s.GridNum) - assert.Equal(t, "200", minQuoteInvestment.String()) + assert.Equal(t, "180", minQuoteInvestment.String()) err := s.checkMinimalQuoteInvestment() assert.NoError(t, err) @@ -905,11 +905,11 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { s.QuoteInvestment = number(10_000) s.GridNum = 1000 minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, s.LowerPrice, s.UpperPrice, s.GridNum) - assert.Equal(t, "20000", minQuoteInvestment.String()) + assert.Equal(t, "19980", minQuoteInvestment.String()) err := s.checkMinimalQuoteInvestment() assert.Error(t, err) - assert.EqualError(t, err, "need at least 20000.000000 USDT for quote investment, 10000.000000 USDT given") + assert.EqualError(t, err, "need at least 19980.000000 USDT for quote investment, 10000.000000 USDT given") }) } From 9d2c742496ce2de971948267a836b7b87a368dab Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 17 Feb 2023 18:35:42 +0800 Subject: [PATCH 0416/1392] grid2: avoid using totalBase when one of quote investment or base investment is defined --- pkg/strategy/grid2/strategy.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6b3902496b..25552040e2 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -878,13 +878,16 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) if s.QuoteInvestment.Compare(totalQuote) > 0 { return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", s.QuoteInvestment.Float64(), totalQuote.Float64()) } + } - if !s.QuantityOrAmount.IsSet() { - // TODO: calculate and override the quantity here - } + var submitOrders []types.SubmitOrder + + if !s.BaseInvestment.IsZero() || !s.QuoteInvestment.IsZero() { + submitOrders, err = s.generateGridOrders(s.QuoteInvestment, s.BaseInvestment, lastPrice) + } else { + submitOrders, err = s.generateGridOrders(totalQuote, totalBase, lastPrice) } - submitOrders, err := s.generateGridOrders(totalQuote, totalBase, lastPrice) if err != nil { return err } From 21cdb7afe89672cac6fd863e022e15380bd935e2 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 17 Feb 2023 18:54:47 +0800 Subject: [PATCH 0417/1392] grid2: split SubmitOrders calls --- pkg/strategy/grid2/strategy.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 25552040e2..26b4e09289 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -894,9 +894,13 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) s.debugGridOrders(submitOrders, lastPrice) - createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...) - if err2 != nil { - return err + var createdOrders []types.Order + for _, submitOrder := range submitOrders { + ret, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrder) + if err2 != nil { + return err2 + } + createdOrders = append(createdOrders, ret...) } // try to always emit grid ready From cf1be9fc6fac254e56b8043c1e6246490132bceb Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 17 Feb 2023 19:15:00 +0800 Subject: [PATCH 0418/1392] bbgo: process pending order update for active order book --- pkg/bbgo/activeorderbook.go | 39 ++++++++++++++++++++++++++++++++----- pkg/bbgo/order_execution.go | 27 +++++++++++++++++++++++++ pkg/types/ordermap.go | 12 ++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 3558a54737..3c3d63d7e8 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -25,6 +25,8 @@ type ActiveOrderBook struct { filledCallbacks []func(o types.Order) canceledCallbacks []func(o types.Order) + pendingOrderUpdates *types.SyncOrderMap + // sig is the order update signal // this signal will be emitted when a new order is added or removed. C sigchan.Chan @@ -32,9 +34,10 @@ type ActiveOrderBook struct { func NewActiveOrderBook(symbol string) *ActiveOrderBook { return &ActiveOrderBook{ - Symbol: symbol, - orders: types.NewSyncOrderMap(), - C: sigchan.New(1), + Symbol: symbol, + orders: types.NewSyncOrderMap(), + pendingOrderUpdates: types.NewSyncOrderMap(), + C: sigchan.New(1), } } @@ -230,6 +233,11 @@ func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) { return } + if !b.orders.Exists(order.OrderID) { + b.pendingOrderUpdates.Add(order) + return + } + switch order.Status { case types.OrderStatusFilled: // make sure we have the order and we remove it @@ -277,7 +285,11 @@ func (b *ActiveOrderBook) Print() { func (b *ActiveOrderBook) Update(orders ...types.Order) { hasSymbol := len(b.Symbol) > 0 for _, order := range orders { - if hasSymbol && b.Symbol == order.Symbol { + if hasSymbol { + if b.Symbol == order.Symbol { + b.orders.Update(order) + } + } else { b.orders.Update(order) } } @@ -286,9 +298,26 @@ func (b *ActiveOrderBook) Update(orders ...types.Order) { func (b *ActiveOrderBook) Add(orders ...types.Order) { hasSymbol := len(b.Symbol) > 0 for _, order := range orders { - if hasSymbol && b.Symbol == order.Symbol { + if hasSymbol { + if b.Symbol == order.Symbol { + b.add(order) + } + } else { + b.add(order) + } + } +} + +// add the order to the active order book and check the pending order +func (b *ActiveOrderBook) add(order types.Order) { + if pendingOrder, ok := b.pendingOrderUpdates.Get(order.OrderID); ok { + if pendingOrder.UpdateTime.Time().After(order.UpdateTime.Time()) { + b.orders.Add(pendingOrder) + } else { b.orders.Add(order) } + } else { + b.orders.Add(order) } } diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 1caab6b5be..9cfd4b702f 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -62,6 +62,33 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ return createdOrders, err } +// BatchPlaceOrderChan post orders with a channel, the created order will be sent to this channel immediately, so that +// the caller can add the created order to the active order book or the order store to collect trades. +// this method is used when you have large amount of orders to be sent and most of the orders might be filled as taker order. +// channel orderC will be closed when all the submit orders are submitted. +func BatchPlaceOrderChan(ctx context.Context, exchange types.Exchange, orderC chan types.Order, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { + defer close(orderC) + + var createdOrders types.OrderSlice + var err error + var errIndexes []int + for i, submitOrder := range submitOrders { + createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) + if err2 != nil { + err = multierr.Append(err, err2) + errIndexes = append(errIndexes, i) + } else if createdOrder != nil { + createdOrder.Tag = submitOrder.Tag + + orderC <- *createdOrder + + createdOrders = append(createdOrders, *createdOrder) + } + } + + return createdOrders, errIndexes, err +} + // BatchPlaceOrder func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { var createdOrders types.OrderSlice diff --git a/pkg/types/ordermap.go b/pkg/types/ordermap.go index fbd1928013..358d640a4a 100644 --- a/pkg/types/ordermap.go +++ b/pkg/types/ordermap.go @@ -56,6 +56,11 @@ func (m OrderMap) Exists(orderID uint64) bool { return ok } +func (m OrderMap) Get(orderID uint64) (Order, bool) { + order, ok := m[orderID] + return order, ok +} + func (m OrderMap) FindByStatus(status OrderStatus) (orders OrderSlice) { for _, o := range m { if o.Status == status { @@ -165,6 +170,13 @@ func (m *SyncOrderMap) Exists(orderID uint64) (exists bool) { return exists } +func (m *SyncOrderMap) Get(orderID uint64) (Order, bool) { + m.Lock() + order, ok := m.orders.Get(orderID) + m.Unlock() + return order, ok +} + func (m *SyncOrderMap) Lookup(f func(o Order) bool) *Order { m.Lock() defer m.Unlock() From 10eba876c476962dd7032a6e194468beb01bb5c9 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 17 Feb 2023 19:24:08 +0800 Subject: [PATCH 0419/1392] bbgo: simplify order symbol filtering condition --- pkg/bbgo/activeorderbook.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 3c3d63d7e8..0a2461cbbe 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -285,26 +285,22 @@ func (b *ActiveOrderBook) Print() { func (b *ActiveOrderBook) Update(orders ...types.Order) { hasSymbol := len(b.Symbol) > 0 for _, order := range orders { - if hasSymbol { - if b.Symbol == order.Symbol { - b.orders.Update(order) - } - } else { - b.orders.Update(order) + if hasSymbol && b.Symbol != order.Symbol { + continue } + + b.orders.Update(order) } } func (b *ActiveOrderBook) Add(orders ...types.Order) { hasSymbol := len(b.Symbol) > 0 for _, order := range orders { - if hasSymbol { - if b.Symbol == order.Symbol { - b.add(order) - } - } else { - b.add(order) + if hasSymbol && b.Symbol != order.Symbol { + continue } + + b.add(order) } } From 4dc4f738345ef992577aa56bdebfd250a9f48806 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 17 Feb 2023 19:50:46 +0800 Subject: [PATCH 0420/1392] bbgo: add pending order test cases --- pkg/bbgo/activeorderbook.go | 6 ++++ pkg/bbgo/activeorderbook_test.go | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 pkg/bbgo/activeorderbook_test.go diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 0a2461cbbe..f53f982654 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -312,6 +312,8 @@ func (b *ActiveOrderBook) add(order types.Order) { } else { b.orders.Add(order) } + + b.pendingOrderUpdates.Remove(order.OrderID) } else { b.orders.Add(order) } @@ -321,6 +323,10 @@ func (b *ActiveOrderBook) Exists(order types.Order) bool { return b.orders.Exists(order.OrderID) } +func (b *ActiveOrderBook) Get(orderID uint64) (types.Order, bool) { + return b.orders.Get(orderID) +} + func (b *ActiveOrderBook) Remove(order types.Order) bool { return b.orders.Remove(order.OrderID) } diff --git a/pkg/bbgo/activeorderbook_test.go b/pkg/bbgo/activeorderbook_test.go new file mode 100644 index 0000000000..ffdfb2fe63 --- /dev/null +++ b/pkg/bbgo/activeorderbook_test.go @@ -0,0 +1,57 @@ +package bbgo + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func TestActiveOrderBook_pendingOrders(t *testing.T) { + now := time.Now() + ob := NewActiveOrderBook("") + + // if we received filled order first + // should be added to pending orders + ob.orderUpdateHandler(types.Order{ + OrderID: 99, + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(0.01), + Price: number(19000.0), + AveragePrice: fixedpoint.Zero, + StopPrice: fixedpoint.Zero, + }, + Status: types.OrderStatusFilled, + CreationTime: types.Time(now), + UpdateTime: types.Time(now), + }) + + assert.Len(t, ob.pendingOrderUpdates.Orders(), 1) + + // should be added to pending orders + ob.Add(types.Order{ + OrderID: 99, + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(0.01), + Price: number(19000.0), + AveragePrice: fixedpoint.Zero, + StopPrice: fixedpoint.Zero, + }, + Status: types.OrderStatusNew, + CreationTime: types.Time(now), + UpdateTime: types.Time(now.Add(-time.Second)), + }) + + o99, ok := ob.Get(99) + assert.True(t, ok) + assert.Equal(t, types.OrderStatusFilled, o99.Status) +} From 85d002eabcb1f37111057f80db2c46d9f2a398f5 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Fri, 17 Feb 2023 22:52:58 +0800 Subject: [PATCH 0421/1392] FIX: [grid2] fix quote accumulation --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 26b4e09289..69db5fea89 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1025,7 +1025,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin TimeInForce: types.TimeInForceGTC, Tag: orderTag, }) - quoteQuantity := quantity.Mul(price) + quoteQuantity := quantity.Mul(nextPrice) usedQuote = usedQuote.Add(quoteQuantity) } } else { @@ -1043,7 +1043,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin quoteQuantity := quantity.Mul(price) if usedQuote.Add(quoteQuantity).Compare(totalQuote) > 0 { - s.logger.Warnf("used quote %f > total quote %f, this should not happen", usedQuote.Float64(), totalQuote.Float64()) + s.logger.Warnf("used quote %f > total quote %f, this should not happen", usedQuote.Add(quoteQuantity).Float64(), totalQuote.Float64()) continue } @@ -1592,4 +1592,4 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha } return false, nil -} +} \ No newline at end of file From a6047c48405a33a17f74318c775f2c780096800d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 20 Feb 2023 16:52:39 +0800 Subject: [PATCH 0422/1392] grid2: implement CleanUp interface --- pkg/strategy/grid2/strategy.go | 38 ++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 69db5fea89..bd055e6c4c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -145,6 +145,9 @@ type Strategy struct { GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"` Position *types.Position `persistence:"position"` + // ExchangeSession is an injection field + ExchangeSession *bbgo.ExchangeSession + grid *Grid session *bbgo.ExchangeSession orderQueryService types.ExchangeOrderQueryService @@ -200,6 +203,17 @@ func (s *Strategy) Validate() error { } func (s *Strategy) Defaults() error { + if s.LogFields == nil { + s.LogFields = logrus.Fields{} + } + + s.LogFields["symbol"] = s.Symbol + s.LogFields["strategy"] = ID + return nil +} + +func (s *Strategy) Initialize() error { + s.logger = log.WithFields(s.LogFields) return nil } @@ -1392,6 +1406,20 @@ func (s *Strategy) newPrometheusLabels() prometheus.Labels { return mergeLabels(s.PrometheusLabels, labels) } +func (s *Strategy) CleanUp(ctx context.Context) error { + if s.ExchangeSession == nil { + return errors.New("ExchangeSession is nil, can not clean up") + } + + openOrders, err := s.ExchangeSession.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } + + err = s.ExchangeSession.Exchange.CancelOrders(ctx, openOrders...) + return errors.Wrapf(err, "can not cancel %s orders", s.Symbol) +} + func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() @@ -1401,14 +1429,6 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.orderQueryService = service } - if s.LogFields == nil { - s.LogFields = logrus.Fields{} - } - - s.LogFields["symbol"] = s.Symbol - s.LogFields["strategy"] = ID - s.logger = log.WithFields(s.LogFields) - if s.OrderGroupID == 0 { s.OrderGroupID = util.FNV32(instanceID) } @@ -1592,4 +1612,4 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha } return false, nil -} \ No newline at end of file +} From 08cc99c3006ed60c6008d6e601fe483e707ba82c Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 20 Feb 2023 22:25:00 +0800 Subject: [PATCH 0423/1392] grid2: add recover debug log --- pkg/strategy/grid2/strategy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index bd055e6c4c..3651ff0ef5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1243,7 +1243,11 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService // we will only submit reverse orders for filled orders filledOrders := types.OrdersFilled(tmpOrders) - s.logger.Infof("GRID RECOVER: found %d filled grid orders", len(filledOrders)) + s.logger.Infof("GRID RECOVER: found %d filled grid orders, will re-replay the order event in the following order:", len(filledOrders)) + for i, o := range filledOrders { + s.logger.Infof("- %d) %s", i, o.String()) + } + s.setGrid(grid) s.EmitGridReady() From d53b41f4fd161b15624a57c7903748133a728598 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 21 Feb 2023 01:05:56 +0800 Subject: [PATCH 0424/1392] grid2: use go routine to recover grid to avoid order update delay issue --- pkg/strategy/grid2/strategy.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3651ff0ef5..b1dfb39ade 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1245,7 +1245,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.logger.Infof("GRID RECOVER: found %d filled grid orders, will re-replay the order event in the following order:", len(filledOrders)) for i, o := range filledOrders { - s.logger.Infof("- %d) %s", i, o.String()) + s.logger.Infof("%d) %s", i, o.String()) } s.setGrid(grid) @@ -1553,17 +1553,21 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. session.UserDataStream.OnStart(func() { s.logger.Infof("user data stream started, initializing grid...") - // do recover only when triggerPrice is not set. - if s.RecoverOrdersWhenStart { - s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") - if err := s.recoverGrid(ctx, session); err != nil { - log.WithError(err).Errorf("recover error") + // avoid blocking the user data stream + // callbacks are blocking operation + go func() { + // do recover only when triggerPrice is not set. + if s.RecoverOrdersWhenStart { + s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") + if err := s.recoverGrid(ctx, session); err != nil { + log.WithError(err).Errorf("recover error") + } } - } - if err := s.openGrid(ctx, session); err != nil { - s.logger.WithError(err).Errorf("failed to setup grid orders") - } + if err := s.openGrid(ctx, session); err != nil { + s.logger.WithError(err).Errorf("failed to setup grid orders") + } + }() }) } From 0402fddea3ecb289c01dbc2a5b7c8118545aaeee Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 21 Feb 2023 15:50:25 +0800 Subject: [PATCH 0425/1392] grid2: pull out order filtering --- pkg/strategy/grid2/grid.go | 13 +++++++++++++ pkg/strategy/grid2/strategy.go | 22 ++++++++++++---------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 517dd443c4..e5f9978c99 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -7,6 +7,7 @@ import ( "strconv" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) type PinCalculator func() []Pin @@ -143,6 +144,18 @@ func (g *Grid) NextLowerPin(price fixedpoint.Value) (Pin, bool) { return Pin(fixedpoint.Zero), false } +func (g *Grid) FilterOrders(orders []types.Order) (ret []types.Order) { + for _, o := range orders { + if !g.HasPrice(o.Price) { + continue + } + + ret = append(ret, o) + } + + return ret +} + func (g *Grid) HasPrice(price fixedpoint.Value) bool { if _, exists := g.pinsCache[Pin(price)]; exists { return exists diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b1dfb39ade..5d0e505430 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1163,20 +1163,14 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService } } - activeOrderBook := s.orderExecutor.ActiveMakerOrders() - - // Allocate a local order book + // Allocate a local order book for querying the history orders orderBook := bbgo.NewActiveOrderBook(s.Symbol) // Ensure that orders are grid orders // The price must be at the grid pin - for _, openOrder := range openOrders { - if grid.HasPrice(openOrder.Price) { - orderBook.Add(openOrder) - - // put the order back to the active order book so that we can receive order update - activeOrderBook.Add(openOrder) - } + gridOrders := grid.FilterOrders(openOrders) + for _, gridOrder := range gridOrders { + orderBook.Add(gridOrder) } // if all open orders are the grid orders, then we don't have to recover @@ -1248,6 +1242,14 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.logger.Infof("%d) %s", i, o.String()) } + // before we re-play the orders, + // we need to add these open orders to the active order book + activeOrderBook := s.orderExecutor.ActiveMakerOrders() + for _, gridOrder := range gridOrders { + // put the order back to the active order book so that we can receive order update + activeOrderBook.Add(gridOrder) + } + s.setGrid(grid) s.EmitGridReady() From 9c1110fb44050c4579fbdcd620e893532792775d Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 21 Feb 2023 17:48:40 +0800 Subject: [PATCH 0426/1392] grid2: fix calculateMinimalQuoteInvestment --- pkg/strategy/grid2/strategy.go | 29 +++++++++++++++++++++-------- pkg/strategy/grid2/strategy_test.go | 17 ++++++++++------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5d0e505430..8b17e6bee4 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1120,15 +1120,27 @@ func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.Exchange return fixedpoint.Zero, fmt.Errorf("%s ticker price not found", s.Symbol) } -func calculateMinimalQuoteInvestment(market types.Market, lowerPrice, upperPrice fixedpoint.Value, gridNum int64) fixedpoint.Value { - num := fixedpoint.NewFromInt(gridNum - 1) - minimalAmountLowerPrice := fixedpoint.Max(lowerPrice.Mul(market.MinQuantity), market.MinNotional) - minimalAmountUpperPrice := fixedpoint.Max(upperPrice.Mul(market.MinQuantity), market.MinNotional) - return fixedpoint.Max(minimalAmountLowerPrice, minimalAmountUpperPrice).Mul(num) +func calculateMinimalQuoteInvestment(market types.Market, grid *Grid) fixedpoint.Value { + // upperPrice for buy order + upperPrice := grid.UpperPrice.Sub(grid.Spread) + minQuantity := fixedpoint.Max( + fixedpoint.Max(upperPrice.Mul(market.MinQuantity), market.MinNotional).Div(upperPrice), + market.MinQuantity, + ) + + var pins = grid.Pins + var totalQuote = fixedpoint.Zero + for i := len(pins) - 2; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + totalQuote = totalQuote.Add(price.Mul(minQuantity)) + } + + return totalQuote } -func (s *Strategy) checkMinimalQuoteInvestment() error { - minimalQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, s.LowerPrice, s.UpperPrice, s.GridNum) +func (s *Strategy) checkMinimalQuoteInvestment(grid *Grid) error { + minimalQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid) if s.QuoteInvestment.Compare(minimalQuoteInvestment) <= 0 { return fmt.Errorf("need at least %f %s for quote investment, %f %s given", minimalQuoteInvestment.Float64(), @@ -1475,7 +1487,8 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. // we need to check the minimal quote investment here, because we need the market info if s.QuoteInvestment.Sign() > 0 { - if err := s.checkMinimalQuoteInvestment(); err != nil { + grid := s.newGrid() + if err := s.checkMinimalQuoteInvestment(grid); err != nil { return err } } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 0e5408c13f..1e46d9cd06 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -894,22 +894,25 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { // hence we should have at least: 20USDT * 10 grids s.QuoteInvestment = number(10_000) s.GridNum = 10 - minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, s.LowerPrice, s.UpperPrice, s.GridNum) - assert.Equal(t, "180", minQuoteInvestment.String()) + grid := s.newGrid() + minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid) + assert.InDelta(t, 129.9999, minQuoteInvestment.Float64(), 0.01) - err := s.checkMinimalQuoteInvestment() + err := s.checkMinimalQuoteInvestment(grid) assert.NoError(t, err) }) t.Run("1000 grids", func(t *testing.T) { s.QuoteInvestment = number(10_000) s.GridNum = 1000 - minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, s.LowerPrice, s.UpperPrice, s.GridNum) - assert.Equal(t, "19980", minQuoteInvestment.String()) - err := s.checkMinimalQuoteInvestment() + grid := s.newGrid() + minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid) + assert.InDelta(t, 14979.995499, minQuoteInvestment.Float64(), 0.001) + + err := s.checkMinimalQuoteInvestment(grid) assert.Error(t, err) - assert.EqualError(t, err, "need at least 19980.000000 USDT for quote investment, 10000.000000 USDT given") + assert.EqualError(t, err, "need at least 14979.995500 USDT for quote investment, 10000.000000 USDT given") }) } From 03dfb4386e3f4a92980ddf18bc37c5486c890d9d Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 21 Feb 2023 17:58:11 +0800 Subject: [PATCH 0427/1392] grid2: simplify and fix calculateMinimalQuoteInvestment --- pkg/strategy/grid2/strategy.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 8b17e6bee4..615c9e3dac 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1122,11 +1122,8 @@ func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.Exchange func calculateMinimalQuoteInvestment(market types.Market, grid *Grid) fixedpoint.Value { // upperPrice for buy order - upperPrice := grid.UpperPrice.Sub(grid.Spread) - minQuantity := fixedpoint.Max( - fixedpoint.Max(upperPrice.Mul(market.MinQuantity), market.MinNotional).Div(upperPrice), - market.MinQuantity, - ) + lowerPrice := grid.LowerPrice + minQuantity := fixedpoint.Max(market.MinNotional.Div(lowerPrice), market.MinQuantity) var pins = grid.Pins var totalQuote = fixedpoint.Zero From bc98fe3bcc48e0a79247389e0346a1f83ca2f05b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Feb 2023 00:50:00 +0800 Subject: [PATCH 0428/1392] grid2: fix recover sorting --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 615c9e3dac..df2109c789 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1238,9 +1238,9 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService // for reverse order recovering, we need the orders to be sort by update time ascending-ly types.SortOrdersUpdateTimeAscending(tmpOrders) - if len(tmpOrders) > 1 && len(tmpOrders) == int(s.GridNum)+1 { + if len(tmpOrders) > 1 && len(tmpOrders) == int(s.GridNum) { // remove the latest updated order because it's near the empty slot - tmpOrders = tmpOrders[:len(tmpOrders)-1] + tmpOrders = tmpOrders[1:] } // we will only submit reverse orders for filled orders From d2d818a6bcc53045f0729dda5beb4c600b648172 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Feb 2023 00:54:12 +0800 Subject: [PATCH 0429/1392] bbgo: sleep 200ms before we retry submiting the order --- pkg/bbgo/order_executor_general.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 751366b2b8..14ef0ba3ca 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -223,6 +223,8 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. } if len(errIdx) > 0 { + time.Sleep(200 * time.Millisecond) + createdOrders2, err2 := BatchRetryPlaceOrder(ctx, e.session.Exchange, errIdx, formattedOrders...) if err2 != nil { err = multierr.Append(err, err2) From bee7b593d243f53ac21da940c60e1bc7ebcd58a0 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Feb 2023 01:08:19 +0800 Subject: [PATCH 0430/1392] grid2: fix log index number --- pkg/bbgo/order_executor_general.go | 2 +- pkg/strategy/grid2/strategy.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 14ef0ba3ca..8d5a1fbc01 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -224,7 +224,7 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. if len(errIdx) > 0 { time.Sleep(200 * time.Millisecond) - + createdOrders2, err2 := BatchRetryPlaceOrder(ctx, e.session.Exchange, errIdx, formattedOrders...) if err2 != nil { err = multierr.Append(err, err2) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index df2109c789..7fc15ebd83 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1248,7 +1248,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.logger.Infof("GRID RECOVER: found %d filled grid orders, will re-replay the order event in the following order:", len(filledOrders)) for i, o := range filledOrders { - s.logger.Infof("%d) %s", i, o.String()) + s.logger.Infof("%d) %s", i+1, o.String()) } // before we re-play the orders, From 9e5717ab831709d267936fd39acb5b02ecd38fe0 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Feb 2023 01:10:49 +0800 Subject: [PATCH 0431/1392] grid2: sleep 2 seconds to wait for the reverse order to be placed --- pkg/strategy/grid2/strategy.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7fc15ebd83..c60458b9ed 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1266,6 +1266,9 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.processFilledOrder(o) } + // wait for the reverse order to be placed + time.Sleep(2 * time.Second) + s.logger.Infof("GRID RECOVER COMPLETE") debugGrid(grid, s.orderExecutor.ActiveMakerOrders()) From 67d84b97168a49f975e8d27af9a89eb307851437 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Feb 2023 01:11:34 +0800 Subject: [PATCH 0432/1392] grid2: sleep 100ms between the recover orders --- pkg/strategy/grid2/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c60458b9ed..908ef0b324 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1264,6 +1264,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService for _, o := range filledOrders { s.processFilledOrder(o) + time.Sleep(100 * time.Millisecond) } // wait for the reverse order to be placed From 9d218d93ac6215bddcee45b45a89dce1332e1c59 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Feb 2023 15:11:47 +0800 Subject: [PATCH 0433/1392] grid2: use string builder for debugGrid --- pkg/strategy/grid2/debug.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/grid2/debug.go b/pkg/strategy/grid2/debug.go index 56fbf92cb4..611c600682 100644 --- a/pkg/strategy/grid2/debug.go +++ b/pkg/strategy/grid2/debug.go @@ -2,6 +2,7 @@ package grid2 import ( "fmt" + "strings" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -9,7 +10,9 @@ import ( ) func debugGrid(grid *Grid, book *bbgo.ActiveOrderBook) { - fmt.Println("================== GRID ORDERS ==================") + var sb strings.Builder + + sb.WriteString("================== GRID ORDERS ==================\n") pins := grid.Pins missingPins := scanMissingPinPrices(book, pins) @@ -19,30 +22,33 @@ func debugGrid(grid *Grid, book *bbgo.ActiveOrderBook) { pin := pins[i] price := fixedpoint.Value(pin) - fmt.Printf("%s -> ", price.String()) + sb.WriteString(fmt.Sprintf("%s -> ", price.String())) existingOrder := book.Lookup(func(o types.Order) bool { return o.Price.Eq(price) }) if existingOrder != nil { - fmt.Printf("%s", existingOrder.String()) + sb.WriteString(fmt.Sprintf("%s", existingOrder.String())) switch existingOrder.Status { case types.OrderStatusFilled: - fmt.Printf(" | 🔧") + sb.WriteString(" | 🔧") case types.OrderStatusCanceled: - fmt.Printf(" | 🔄") + sb.WriteString(" | 🔄") default: - fmt.Printf(" | ✅") + sb.WriteString(" | ✅") } } else { - fmt.Printf("ORDER MISSING ⚠️ ") + sb.WriteString("ORDER MISSING ⚠️ ") if missing == 1 { - fmt.Printf(" COULD BE EMPTY SLOT") + sb.WriteString(" COULD BE EMPTY SLOT") } } - fmt.Printf("\n") + sb.WriteString("\n") } - fmt.Println("================== END OF GRID ORDERS ===================") + + sb.WriteString("================== END OF GRID ORDERS ===================") + + fmt.Println(sb.String()) } From 6dc92bea164ddd38e1b87a3924be86bb07bf4286 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Feb 2023 15:16:47 +0800 Subject: [PATCH 0434/1392] grid2: pass logger entry to debugGrid --- pkg/strategy/grid2/debug.go | 6 ++++-- pkg/strategy/grid2/strategy.go | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/debug.go b/pkg/strategy/grid2/debug.go index 611c600682..1a57eeb444 100644 --- a/pkg/strategy/grid2/debug.go +++ b/pkg/strategy/grid2/debug.go @@ -4,12 +4,14 @@ import ( "fmt" "strings" + "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) -func debugGrid(grid *Grid, book *bbgo.ActiveOrderBook) { +func debugGrid(logger logrus.FieldLogger, grid *Grid, book *bbgo.ActiveOrderBook) { var sb strings.Builder sb.WriteString("================== GRID ORDERS ==================\n") @@ -50,5 +52,5 @@ func debugGrid(grid *Grid, book *bbgo.ActiveOrderBook) { sb.WriteString("================== END OF GRID ORDERS ===================") - fmt.Println(sb.String()) + logger.Infoln(sb.String()) } diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 908ef0b324..f917fe9831 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1223,7 +1223,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService } } - debugGrid(grid, orderBook) + debugGrid(s.logger, grid, orderBook) tmpOrders := orderBook.Orders() @@ -1272,7 +1272,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.logger.Infof("GRID RECOVER COMPLETE") - debugGrid(grid, s.orderExecutor.ActiveMakerOrders()) + debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) return nil } From e3fa4587d99829076806bc754514dc7ca03eda53 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Feb 2023 15:18:48 +0800 Subject: [PATCH 0435/1392] bbgo: add logging config struct --- pkg/bbgo/config.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/bbgo/config.go b/pkg/bbgo/config.go index 9da4275581..2f13d20bb6 100644 --- a/pkg/bbgo/config.go +++ b/pkg/bbgo/config.go @@ -89,6 +89,11 @@ type NotificationConfig struct { Switches *NotificationSwitches `json:"switches" yaml:"switches"` } +type LoggingConfig struct { + Trade bool `json:"trade,omitempty"` + Order bool `json:"order,omitempty"` +} + type Session struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` ExchangeName string `json:"exchange" yaml:"exchange"` @@ -326,6 +331,8 @@ type Config struct { RiskControls *RiskControls `json:"riskControls,omitempty" yaml:"riskControls,omitempty"` + Logging *LoggingConfig `json:"logging,omitempty"` + ExchangeStrategies []ExchangeStrategyMount `json:"-" yaml:"-"` CrossExchangeStrategies []CrossExchangeStrategy `json:"-" yaml:"-"` From 905b25655da8cd7cb875a7d753aaaaa460242e6e Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Feb 2023 15:25:39 +0800 Subject: [PATCH 0436/1392] bbgo: provide logging configuration --- pkg/bbgo/bootstrap.go | 8 ++++++++ pkg/bbgo/environment.go | 6 ++++++ pkg/bbgo/session.go | 23 +++++++++++++++++++---- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/bootstrap.go b/pkg/bbgo/bootstrap.go index 59d5576926..717b1447a5 100644 --- a/pkg/bbgo/bootstrap.go +++ b/pkg/bbgo/bootstrap.go @@ -14,6 +14,10 @@ func BootstrapEnvironmentLightweight(ctx context.Context, environ *Environment, return errors.Wrap(err, "exchange session configure error") } + if userConfig.Logging != nil { + environ.SetLogging(userConfig.Logging) + } + if userConfig.Persistence != nil { if err := ConfigurePersistence(ctx, userConfig.Persistence); err != nil { return errors.Wrap(err, "persistence configure error") @@ -32,6 +36,10 @@ func BootstrapEnvironment(ctx context.Context, environ *Environment, userConfig return errors.Wrap(err, "exchange session configure error") } + if userConfig.Logging != nil { + environ.SetLogging(userConfig.Logging) + } + if userConfig.Persistence != nil { if err := ConfigurePersistence(ctx, userConfig.Persistence); err != nil { return errors.Wrap(err, "persistence configure error") diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index 9ac8ccca60..ba41a051e5 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -102,6 +102,8 @@ type Environment struct { syncStatus SyncStatus syncConfig *SyncConfig + loggingConfig *LoggingConfig + sessions map[string]*ExchangeSession } @@ -127,6 +129,10 @@ func (environ *Environment) Sessions() map[string]*ExchangeSession { return environ.sessions } +func (environ *Environment) SetLogging(config *LoggingConfig) { + environ.loggingConfig = config +} + func (environ *Environment) SelectSessions(names ...string) map[string]*ExchangeSession { if len(names) == 0 { return environ.sessions diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index e305adef54..13e500dbda 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -261,10 +261,25 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) } } - // add trade logger - session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { - log.Info(trade.String()) - }) + if environ.loggingConfig != nil { + if environ.loggingConfig.Trade { + session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { + log.Info(trade.String()) + }) + } + + if environ.loggingConfig.Order { + session.UserDataStream.OnOrderUpdate(func(order types.Order) { + log.Info(order.String()) + }) + } + } else { + // if logging config is nil, then apply default logging setup + // add trade logger + session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { + log.Info(trade.String()) + }) + } if viper.GetBool("debug-kline") { session.MarketDataStream.OnKLine(func(kline types.KLine) { From ef771546e32791799f07e7237e176d3e2c424e65 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Feb 2023 15:45:33 +0800 Subject: [PATCH 0437/1392] grid2: simplify WriteString call --- pkg/strategy/grid2/debug.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/debug.go b/pkg/strategy/grid2/debug.go index 1a57eeb444..5545b9e799 100644 --- a/pkg/strategy/grid2/debug.go +++ b/pkg/strategy/grid2/debug.go @@ -31,7 +31,7 @@ func debugGrid(logger logrus.FieldLogger, grid *Grid, book *bbgo.ActiveOrderBook }) if existingOrder != nil { - sb.WriteString(fmt.Sprintf("%s", existingOrder.String())) + sb.WriteString(existingOrder.String()) switch existingOrder.Status { case types.OrderStatusFilled: From 31c9ebf34b59eeda9cf6c772a9dd073758152738 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Feb 2023 11:19:10 +0800 Subject: [PATCH 0438/1392] grid2: update metrics after recovering the grid orders --- pkg/strategy/grid2/strategy.go | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f917fe9831..fcc99d257e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1578,6 +1578,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. if err := s.recoverGrid(ctx, session); err != nil { log.WithError(err).Errorf("recover error") } + } if err := s.openGrid(ctx, session); err != nil { @@ -1596,22 +1597,24 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio return err } + // do recover only when openOrders > 0 + if len(openOrders) == 0 { + return nil + } + s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) - // do recover only when openOrders > 0 - if len(openOrders) > 0 { - historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) - if !implemented { - s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") - return nil - } + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) + if !implemented { + s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") + return nil + } - if err := s.recoverGridWithOpenOrders(ctx, historyService, openOrders); err != nil { - return errors.Wrap(err, "recover grid error") - } + if err := s.recoverGridWithOpenOrders(ctx, historyService, openOrders); err != nil { + return errors.Wrap(err, "recover grid error") } - return nil + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) } // openOrdersMismatches verifies if the open orders are on the grid pins From b666c8bf40c0d1f5d388f6790926d192fb59f53c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Feb 2023 18:08:21 +0800 Subject: [PATCH 0439/1392] bbgo: triggering pending order update event ot the handler --- pkg/bbgo/activeorderbook.go | 18 ++++++++++++++---- pkg/strategy/grid2/strategy.go | 1 + 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index f53f982654..782dfd48a0 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -307,13 +307,23 @@ func (b *ActiveOrderBook) Add(orders ...types.Order) { // add the order to the active order book and check the pending order func (b *ActiveOrderBook) add(order types.Order) { if pendingOrder, ok := b.pendingOrderUpdates.Get(order.OrderID); ok { + // if the pending order update time is newer than the adding order + // we should use the pending order rather than the adding order. + // if pending order is older, than we should add the new one, and drop the pending order if pendingOrder.UpdateTime.Time().After(order.UpdateTime.Time()) { - b.orders.Add(pendingOrder) - } else { - b.orders.Add(order) + order = pendingOrder + } + + b.orders.Add(order) + b.pendingOrderUpdates.Remove(pendingOrder.OrderID) + + // when using add(order), it's usually a new maker order on the order book. + // so, when it's not status=new, we should trigger order update handler + if order.Status != types.OrderStatusNew { + // emit the order update handle function to trigger callback + b.orderUpdateHandler(order) } - b.pendingOrderUpdates.Remove(order.OrderID) } else { b.orders.Add(order) } diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index fcc99d257e..93f45d2372 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1615,6 +1615,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio } s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) + return nil } // openOrdersMismatches verifies if the open orders are on the grid pins From 7532c316311cfb211cd33bbe4518570e0e696479 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Feb 2023 21:46:57 +0800 Subject: [PATCH 0440/1392] bbgo: fix pending order event trigger --- pkg/bbgo/activeorderbook_test.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/bbgo/activeorderbook_test.go b/pkg/bbgo/activeorderbook_test.go index ffdfb2fe63..fdd4ea9e95 100644 --- a/pkg/bbgo/activeorderbook_test.go +++ b/pkg/bbgo/activeorderbook_test.go @@ -12,8 +12,14 @@ import ( func TestActiveOrderBook_pendingOrders(t *testing.T) { now := time.Now() + ob := NewActiveOrderBook("") + filled := false + ob.OnFilled(func(o types.Order) { + filled = true + }) + // if we received filled order first // should be added to pending orders ob.orderUpdateHandler(types.Order{ @@ -34,6 +40,11 @@ func TestActiveOrderBook_pendingOrders(t *testing.T) { assert.Len(t, ob.pendingOrderUpdates.Orders(), 1) + o99, ok := ob.pendingOrderUpdates.Get(99) + if assert.True(t, ok) { + assert.Equal(t, types.OrderStatusFilled, o99.Status) + } + // should be added to pending orders ob.Add(types.Order{ OrderID: 99, @@ -51,7 +62,6 @@ func TestActiveOrderBook_pendingOrders(t *testing.T) { UpdateTime: types.Time(now.Add(-time.Second)), }) - o99, ok := ob.Get(99) - assert.True(t, ok) - assert.Equal(t, types.OrderStatusFilled, o99.Status) + assert.True(t, filled, "filled event should be fired") + } From 5e2add8765875685096ab32a751fa2f9e55d87e3 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Feb 2023 22:39:47 +0800 Subject: [PATCH 0441/1392] grid2: add the missing metrics update --- pkg/strategy/grid2/strategy.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 93f45d2372..5622e42cfe 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -434,6 +434,9 @@ func (s *Strategy) processFilledOrder(o types.Order) { } else { s.logger.Infof("GRID REVERSE ORDER IS CREATED: %+v", createdOrders) } + + s.updateGridNumOfOrdersMetrics() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) } // handleOrderFilled is called when an order status is FILLED @@ -732,8 +735,6 @@ func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.Exch return func(o types.Order) { s.handleOrderFilled(o) bbgo.Sync(ctx, s) - - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) } } @@ -809,6 +810,7 @@ func (s *Strategy) CloseGrid(ctx context.Context) error { // free the grid object s.setGrid(nil) + s.updateGridNumOfOrdersMetrics() return nil } @@ -947,6 +949,11 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return nil } +func (s *Strategy) updateGridNumOfOrdersMetrics() { + baseLabels := s.newPrometheusLabels() + metricsGridNumOfOrders.With(baseLabels).Set(float64(s.orderExecutor.ActiveMakerOrders().NumOfOrders())) +} + func (s *Strategy) updateOpenOrderPricesMetrics(orders []types.Order) { orders = sortOrdersByPriceAscending(orders) num := len(orders) @@ -1273,6 +1280,9 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.logger.Infof("GRID RECOVER COMPLETE") debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) + + s.updateGridNumOfOrdersMetrics() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } @@ -1614,7 +1624,6 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio return errors.Wrap(err, "recover grid error") } - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } From 2a47d390f02f5d15a642a5fee9bf180d3f94a204 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Feb 2023 22:49:03 +0800 Subject: [PATCH 0442/1392] grid2: update grid2 metrics --- pkg/strategy/grid2/metrics.go | 32 +++++++++++++++++++++++++++++--- pkg/strategy/grid2/strategy.go | 17 ++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/metrics.go b/pkg/strategy/grid2/metrics.go index 854a810ccb..db995f6d0b 100644 --- a/pkg/strategy/grid2/metrics.go +++ b/pkg/strategy/grid2/metrics.go @@ -3,9 +3,11 @@ package grid2 import "github.com/prometheus/client_golang/prometheus" var ( - metricsGridNumOfOrders *prometheus.GaugeVec - metricsGridOrderPrices *prometheus.GaugeVec - metricsGridProfit *prometheus.GaugeVec + metricsGridNumOfOrders *prometheus.GaugeVec + metricsGridNum *prometheus.GaugeVec + metricsGridNumOfMissingOrders *prometheus.GaugeVec + metricsGridOrderPrices *prometheus.GaugeVec + metricsGridProfit *prometheus.GaugeVec ) func labelKeys(labels prometheus.Labels) []string { @@ -30,6 +32,17 @@ func mergeLabels(a, b prometheus.Labels) prometheus.Labels { } func initMetrics(extendedLabels []string) { + metricsGridNum = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_num", + Help: "number of grids", + }, + append([]string{ + "exchange", // exchange name + "symbol", // symbol of the market + }, extendedLabels...), + ) + metricsGridNumOfOrders = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "bbgo_grid2_num_of_orders", @@ -41,6 +54,17 @@ func initMetrics(extendedLabels []string) { }, extendedLabels...), ) + metricsGridNumOfMissingOrders = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_num_of_missing_orders", + Help: "number of missing orders", + }, + append([]string{ + "exchange", // exchange name + "symbol", // symbol of the market + }, extendedLabels...), + ) + metricsGridOrderPrices = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "bbgo_grid2_order_prices", @@ -74,7 +98,9 @@ func registerMetrics() { } prometheus.MustRegister( + metricsGridNum, metricsGridNumOfOrders, + metricsGridNumOfMissingOrders, metricsGridProfit, metricsGridOrderPrices, ) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5622e42cfe..7fc3c37545 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -951,7 +951,15 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) func (s *Strategy) updateGridNumOfOrdersMetrics() { baseLabels := s.newPrometheusLabels() - metricsGridNumOfOrders.With(baseLabels).Set(float64(s.orderExecutor.ActiveMakerOrders().NumOfOrders())) + numOfOrders := s.orderExecutor.ActiveMakerOrders().NumOfOrders() + metricsGridNumOfOrders.With(baseLabels).Set(float64(numOfOrders)) + + if grid := s.getGrid(); grid != nil { + gridNum := grid.Size.Int() + metricsGridNum.With(baseLabels).Set(float64(gridNum)) + numOfMissingOrders := gridNum - 1 - numOfOrders + metricsGridNumOfMissingOrders.With(baseLabels).Set(float64(numOfMissingOrders)) + } } func (s *Strategy) updateOpenOrderPricesMetrics(orders []types.Order) { @@ -1292,6 +1300,13 @@ func (s *Strategy) setGrid(grid *Grid) { s.mu.Unlock() } +func (s *Strategy) getGrid() *Grid { + s.mu.Lock() + grid := s.grid + s.mu.Unlock() + return grid +} + // replayOrderHistory queries the closed order history from the API and rebuild the orderbook from the order history. // startTime, endTime is the time range of the order history. func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook *bbgo.ActiveOrderBook, historyService types.ExchangeTradeHistoryService, startTime, endTime time.Time, lastOrderID uint64) error { From ed61f70d7474b8fedc74de5236573305891d329d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Feb 2023 23:17:04 +0800 Subject: [PATCH 0443/1392] bbgo: rewrite BatchRetryPlaceOrder to make it retry with err index --- pkg/bbgo/order_execution.go | 127 ++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 65 deletions(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 9cfd4b702f..f7253b17d9 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -47,67 +47,6 @@ func (e *ExchangeOrderExecutionRouter) SubmitOrdersTo(ctx context.Context, sessi return createdOrders, err } -func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { - var createdOrders types.OrderSlice - var err error - for _, idx := range errIdx { - createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrders[idx]) - if err2 != nil { - err = multierr.Append(err, err2) - } else if createdOrder != nil { - createdOrders = append(createdOrders, *createdOrder) - } - } - - return createdOrders, err -} - -// BatchPlaceOrderChan post orders with a channel, the created order will be sent to this channel immediately, so that -// the caller can add the created order to the active order book or the order store to collect trades. -// this method is used when you have large amount of orders to be sent and most of the orders might be filled as taker order. -// channel orderC will be closed when all the submit orders are submitted. -func BatchPlaceOrderChan(ctx context.Context, exchange types.Exchange, orderC chan types.Order, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { - defer close(orderC) - - var createdOrders types.OrderSlice - var err error - var errIndexes []int - for i, submitOrder := range submitOrders { - createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) - if err2 != nil { - err = multierr.Append(err, err2) - errIndexes = append(errIndexes, i) - } else if createdOrder != nil { - createdOrder.Tag = submitOrder.Tag - - orderC <- *createdOrder - - createdOrders = append(createdOrders, *createdOrder) - } - } - - return createdOrders, errIndexes, err -} - -// BatchPlaceOrder -func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { - var createdOrders types.OrderSlice - var err error - var errIndexes []int - for i, submitOrder := range submitOrders { - createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) - if err2 != nil { - err = multierr.Append(err, err2) - errIndexes = append(errIndexes, i) - } else if createdOrder != nil { - createdOrder.Tag = submitOrder.Tag - createdOrders = append(createdOrders, *createdOrder) - } - } - - return createdOrders, errIndexes, err -} - func (e *ExchangeOrderExecutionRouter) CancelOrdersTo(ctx context.Context, session string, orders ...types.Order) error { if executor, ok := e.executors[session]; ok { return executor.CancelOrders(ctx, orders...) @@ -347,9 +286,67 @@ func (c *BasicRiskController) ProcessOrders(session *ExchangeSession, orders ... return outOrders, nil } -func max(a, b int64) int64 { - if a > b { - return a +// BatchPlaceOrder +func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { + var createdOrders types.OrderSlice + var err error + var errIndexes []int + for i, submitOrder := range submitOrders { + createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) + if err2 != nil { + err = multierr.Append(err, err2) + errIndexes = append(errIndexes, i) + } else if createdOrder != nil { + createdOrder.Tag = submitOrder.Tag + createdOrders = append(createdOrders, *createdOrder) + } + } + + return createdOrders, errIndexes, err +} + +// BatchRetryPlaceOrder places the orders and retries the failed orders +func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { + var createdOrders types.OrderSlice + var err error + + // if the errIdx is nil, then we should iterate all the submit orders + if len(errIdx) == 0 { + for i, submitOrder := range submitOrders { + createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) + if err2 != nil { + err = multierr.Append(err, err2) + errIdx = append(errIdx, i) + } else if createdOrder != nil { + // if the order is successfully created, than we should copy the order tag + createdOrder.Tag = submitOrder.Tag + createdOrders = append(createdOrders, *createdOrder) + } + } + } + + // if we got any error, we should re-iterate the errored orders + for len(errIdx) > 0 { + // allocate a variable for new error index + var errIdxNext []int + + // iterate the error index and re-submit the order + for _, idx := range errIdx { + submitOrder := submitOrders[idx] + createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) + if err2 != nil { + err = multierr.Append(err, err2) + errIdxNext = append(errIdxNext, idx) + } else if createdOrder != nil { + // if the order is successfully created, than we should copy the order tag + createdOrder.Tag = submitOrder.Tag + createdOrders = append(createdOrders, *createdOrder) + } + } + + // update the error index + errIdx = errIdxNext } - return b + + return createdOrders, err } From d89d0cf0ff8452a847d83632fa1bb86adcf83753 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Feb 2023 23:34:26 +0800 Subject: [PATCH 0444/1392] bbgo: refactor SubmitOrders method for retry --- pkg/bbgo/order_execution.go | 17 ++++++++++++- pkg/bbgo/order_executor_general.go | 20 +++------------ pkg/grpc/server.go | 12 ++------- pkg/strategy/grid2/strategy.go | 40 ++++++++++++++++-------------- 4 files changed, 44 insertions(+), 45 deletions(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index f7253b17d9..6a72b23fbd 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -3,6 +3,7 @@ package bbgo import ( "context" "fmt" + "time" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -305,8 +306,10 @@ func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders return createdOrders, errIndexes, err } +type OrderCallback func(order types.Order) + // BatchRetryPlaceOrder places the orders and retries the failed orders -func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { +func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, orderCallback OrderCallback, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { var createdOrders types.OrderSlice var err error @@ -320,6 +323,11 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ } else if createdOrder != nil { // if the order is successfully created, than we should copy the order tag createdOrder.Tag = submitOrder.Tag + + if orderCallback != nil { + orderCallback(*createdOrder) + } + createdOrders = append(createdOrders, *createdOrder) } } @@ -327,6 +335,8 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ // if we got any error, we should re-iterate the errored orders for len(errIdx) > 0 { + time.Sleep(200 * time.Millisecond) + // allocate a variable for new error index var errIdxNext []int @@ -340,6 +350,11 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ } else if createdOrder != nil { // if the order is successfully created, than we should copy the order tag createdOrder.Tag = submitOrder.Tag + + if orderCallback != nil { + orderCallback(*createdOrder) + } + createdOrders = append(createdOrders, *createdOrder) } } diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 8d5a1fbc01..28ddce61ee 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -217,24 +217,12 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. return nil, err } - createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, formattedOrders...) - if err != nil { - log.WithError(err).Errorf("place order error, will retry orders: %v", errIdx) - } - - if len(errIdx) > 0 { - time.Sleep(200 * time.Millisecond) - - createdOrders2, err2 := BatchRetryPlaceOrder(ctx, e.session.Exchange, errIdx, formattedOrders...) - if err2 != nil { - err = multierr.Append(err, err2) - } else { - createdOrders = append(createdOrders, createdOrders2...) - } + orderCreateCallback := func(createdOrder types.Order) { + e.orderStore.Add(createdOrder) + e.activeMakerOrders.Add(createdOrder) } - e.orderStore.Add(createdOrders...) - e.activeMakerOrders.Add(createdOrders...) + createdOrders, err := BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, formattedOrders...) e.tradeCollector.Process() return createdOrders, err } diff --git a/pkg/grpc/server.go b/pkg/grpc/server.go index c7e31bbfcf..21162bda71 100644 --- a/pkg/grpc/server.go +++ b/pkg/grpc/server.go @@ -9,7 +9,6 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "go.uber.org/multierr" "google.golang.org/grpc" "google.golang.org/grpc/reflection" @@ -47,15 +46,8 @@ func (s *TradingService) SubmitOrder(ctx context.Context, request *pb.SubmitOrde } } - createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, session.Exchange, submitOrders...) - if len(errIdx) > 0 { - createdOrders2, err2 := bbgo.BatchRetryPlaceOrder(ctx, session.Exchange, errIdx, submitOrders...) - if err2 != nil { - err = multierr.Append(err, err2) - } else { - createdOrders = append(createdOrders, createdOrders2...) - } - } + // we will return this error later because some orders could be succeeded + createdOrders, err := bbgo.BatchRetryPlaceOrder(ctx, session.Exchange, nil, nil, submitOrders...) // convert response resp := &pb.SubmitOrderResponse{ diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7fc3c37545..92bdb2e413 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1546,24 +1546,6 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. metricsGridProfit.With(labels).Set(stats.TotalQuoteProfit.Float64()) }) - // detect if there are previous grid orders on the order book - if s.ClearOpenOrdersWhenStart { - if err := s.clearOpenOrders(ctx, session); err != nil { - return err - } - } - - if s.ClearOpenOrdersIfMismatch { - mismatch, err := s.openOrdersMismatches(ctx, session) - if err != nil { - s.logger.WithError(err).Errorf("clearOpenOrdersIfMismatch error") - } else if mismatch { - if err2 := s.clearOpenOrders(ctx, session); err2 != nil { - s.logger.WithError(err2).Errorf("clearOpenOrders error") - } - } - } - bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() @@ -1589,6 +1571,28 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. session.MarketDataStream.OnKLineClosed(s.newTakeProfitHandler(ctx, session)) } + // detect if there are previous grid orders on the order book + session.UserDataStream.OnStart(func() { + if s.ClearOpenOrdersWhenStart { + s.logger.Infof("clearOpenOrdersWhenStart is set, clearing open orders...") + if err := s.clearOpenOrders(ctx, session); err != nil { + s.logger.WithError(err).Errorf("clearOpenOrdersWhenStart error") + } + } + + if s.ClearOpenOrdersIfMismatch { + s.logger.Infof("clearOpenOrdersIfMismatch is set, checking mismatched orders...") + mismatch, err := s.openOrdersMismatches(ctx, session) + if err != nil { + s.logger.WithError(err).Errorf("clearOpenOrdersIfMismatch error") + } else if mismatch { + if err2 := s.clearOpenOrders(ctx, session); err2 != nil { + s.logger.WithError(err2).Errorf("clearOpenOrders error") + } + } + } + }) + // if TriggerPrice is zero, that means we need to open the grid when start up if s.TriggerPrice.IsZero() { session.UserDataStream.OnStart(func() { From 59ff86e4bbde469633dda956e16b029e91d5e45c Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Feb 2023 00:44:50 +0800 Subject: [PATCH 0445/1392] grid2: fix metrics for tests --- pkg/strategy/grid2/metrics.go | 7 ++++++- pkg/strategy/grid2/strategy.go | 11 ++++++++--- pkg/strategy/grid2/strategy_test.go | 4 ++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/metrics.go b/pkg/strategy/grid2/metrics.go index db995f6d0b..a73a795f7b 100644 --- a/pkg/strategy/grid2/metrics.go +++ b/pkg/strategy/grid2/metrics.go @@ -3,8 +3,8 @@ package grid2 import "github.com/prometheus/client_golang/prometheus" var ( - metricsGridNumOfOrders *prometheus.GaugeVec metricsGridNum *prometheus.GaugeVec + metricsGridNumOfOrders *prometheus.GaugeVec metricsGridNumOfMissingOrders *prometheus.GaugeVec metricsGridOrderPrices *prometheus.GaugeVec metricsGridProfit *prometheus.GaugeVec @@ -97,6 +97,11 @@ func registerMetrics() { return } + if metricsGridNum == nil { + // default setup + initMetrics(nil) + } + prometheus.MustRegister( metricsGridNum, metricsGridNumOfOrders, diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 92bdb2e413..3f0fa32b20 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -435,8 +435,6 @@ func (s *Strategy) processFilledOrder(o types.Order) { s.logger.Infof("GRID REVERSE ORDER IS CREATED: %+v", createdOrders) } - s.updateGridNumOfOrdersMetrics() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) } // handleOrderFilled is called when an order status is FILLED @@ -735,6 +733,9 @@ func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.Exch return func(o types.Order) { s.handleOrderFilled(o) bbgo.Sync(ctx, s) + + s.updateGridNumOfOrdersMetrics() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) } } @@ -1439,10 +1440,14 @@ func scanMissingPinPrices(orderBook *bbgo.ActiveOrderBook, pins []Pin) PriceMap func (s *Strategy) newPrometheusLabels() prometheus.Labels { labels := prometheus.Labels{ - "exchange": s.session.Name, + "exchange": "default", "symbol": s.Symbol, } + if s.session != nil { + labels["exchange"] = s.session.Name + } + if s.PrometheusLabels == nil { return labels } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 1e46d9cd06..388e1c9cef 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -20,6 +20,10 @@ import ( gridmocks "github.com/c9s/bbgo/pkg/strategy/grid2/mocks" ) +func init() { + registerMetrics() +} + func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { s := &Strategy{ logger: logrus.NewEntry(logrus.New()), From 37535e9f3e71fb496a54c6df15d29d2edfbf006a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Feb 2023 12:24:43 +0800 Subject: [PATCH 0446/1392] grid2: fix fee reduction by rounding --- pkg/strategy/grid2/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7fc3c37545..5fb48ffeb3 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -373,9 +373,11 @@ func (s *Strategy) processFilledOrder(o types.Order) { // if we don't reduce the sell quantity, than we might fail to place the sell order if o.Side == types.SideTypeBuy { baseSellQuantityReduction = s.aggregateOrderBaseFee(o) - s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) + baseSellQuantityReduction = baseSellQuantityReduction.Round(s.Market.VolumePrecision, fixedpoint.HalfUp) + s.logger.Infof("GRID BUY ORDER BASE FEE (Rounding): %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) + newQuantity = newQuantity.Sub(baseSellQuantityReduction) } From 5b903cd4ed1e1f95c176d70dc73069ade34b5f8a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Feb 2023 12:46:38 +0800 Subject: [PATCH 0447/1392] grid2: always round up --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5fb48ffeb3..c4a531a919 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -375,7 +375,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { baseSellQuantityReduction = s.aggregateOrderBaseFee(o) s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) - baseSellQuantityReduction = baseSellQuantityReduction.Round(s.Market.VolumePrecision, fixedpoint.HalfUp) + baseSellQuantityReduction = baseSellQuantityReduction.Round(s.Market.VolumePrecision, fixedpoint.Up) s.logger.Infof("GRID BUY ORDER BASE FEE (Rounding): %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) newQuantity = newQuantity.Sub(baseSellQuantityReduction) From 18478cf4c8c56a0736ab2a658d3c63fb080323ef Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Feb 2023 12:50:43 +0800 Subject: [PATCH 0448/1392] bbgo: apply backoff to submitOrders --- go.mod | 1 + go.sum | 2 ++ pkg/bbgo/order_execution.go | 36 +++++++++++++++++++----------- pkg/bbgo/order_executor_general.go | 5 ++--- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index a5141178f6..e1b60948af 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +68,7 @@ require ( github.com/bitly/go-simplejson v0.5.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.2.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cockroachdb/apd v1.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect diff --git a/go.sum b/go.sum index 37585ff52c..b5698fb3d0 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,8 @@ github.com/c9s/requestgen v1.3.0 h1:3cTHvWIlrc37nGEdJLIO07XaVidDeOwcew06csBz++U= github.com/c9s/requestgen v1.3.0/go.mod h1:5n9FU3hr5307IiXAmbMiZbHYaPiys1u9jCWYexZr9qA= github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b h1:wT8c03PHLv7+nZUIGqxAzRvIfYHNxMCNVWwvdGkOXTs= github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b/go.mod h1:EKObf66Cp7erWxym2de+07qNN5T1N9PXxHdh97N44EQ= +github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= +github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 6a72b23fbd..fe3e98169e 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/cenkalti/backoff/v4" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "go.uber.org/multierr" @@ -311,14 +312,14 @@ type OrderCallback func(order types.Order) // BatchRetryPlaceOrder places the orders and retries the failed orders func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, orderCallback OrderCallback, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { var createdOrders types.OrderSlice - var err error + var werr error // if the errIdx is nil, then we should iterate all the submit orders if len(errIdx) == 0 { for i, submitOrder := range submitOrders { createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) if err2 != nil { - err = multierr.Append(err, err2) + werr = multierr.Append(werr, err2) errIdx = append(errIdx, i) } else if createdOrder != nil { // if the order is successfully created, than we should copy the order tag @@ -343,19 +344,28 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ // iterate the error index and re-submit the order for _, idx := range errIdx { submitOrder := submitOrders[idx] - createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) - if err2 != nil { - err = multierr.Append(err, err2) - errIdxNext = append(errIdxNext, idx) - } else if createdOrder != nil { - // if the order is successfully created, than we should copy the order tag - createdOrder.Tag = submitOrder.Tag - if orderCallback != nil { - orderCallback(*createdOrder) + op := func() error { + // can allocate permanent error backoff.Permanent(err) to stop backoff + createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) + if err2 == nil && createdOrder != nil { + // if the order is successfully created, than we should copy the order tag + createdOrder.Tag = submitOrder.Tag + + if orderCallback != nil { + orderCallback(*createdOrder) + } + + createdOrders = append(createdOrders, *createdOrder) } - createdOrders = append(createdOrders, *createdOrder) + return err2 + } + + // if err2 := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 99)); err2 != nil { + if err2 := backoff.Retry(op, backoff.NewExponentialBackOff()); err2 != nil { + werr = multierr.Append(werr, err2) + errIdxNext = append(errIdxNext, idx) } } @@ -363,5 +373,5 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ errIdx = errIdxNext } - return createdOrders, err + return createdOrders, werr } diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 28ddce61ee..746c45ed1a 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -220,11 +220,10 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. orderCreateCallback := func(createdOrder types.Order) { e.orderStore.Add(createdOrder) e.activeMakerOrders.Add(createdOrder) + e.tradeCollector.Process() } - createdOrders, err := BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, formattedOrders...) - e.tradeCollector.Process() - return createdOrders, err + return BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, formattedOrders...) } type OpenPositionOptions struct { From 1ac0b5b545e1cb4dbf3aaf2dce94ce9f3a69b78f Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Feb 2023 13:26:19 +0800 Subject: [PATCH 0449/1392] remove go 1.17 from ci --- .github/workflows/go.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 313e926c5f..7413c6e128 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,7 +16,6 @@ jobs: redis-version: - 6.2 go-version: - - 1.17 - 1.18 env: MYSQL_DATABASE: bbgo From 6fc45e66ddde505c9ec86a187d1bdd10204c8225 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Feb 2023 17:08:38 +0800 Subject: [PATCH 0450/1392] grid2: for non-compound or earn base mode we should always use the original buy quantity --- pkg/strategy/grid2/strategy.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 9cd46af678..9ab1646966 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -396,8 +396,11 @@ func (s *Strategy) processFilledOrder(o types.Order) { // use the profit to buy more inventory in the grid if s.Compound || s.EarnBase { newQuantity = fixedpoint.Max(orderQuoteQuantity.Div(newPrice), s.Market.MinQuantity) + } else if s.QuantityOrAmount.Quantity.Sign() > 0 { + newQuantity = s.QuantityOrAmount.Quantity } + // TODO: need to consider sell order fee for the profit calculation profit := s.calculateProfit(o, newPrice, newQuantity) s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) s.GridProfitStats.AddProfit(profit) From 7eb953093c3f865b17e33e6610eed216168c09dc Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Feb 2023 18:39:44 +0800 Subject: [PATCH 0451/1392] grid2: merge baseSellQuantityReduction section --- pkg/strategy/grid2/strategy.go | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 9ab1646966..216f332dad 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -368,19 +368,6 @@ func (s *Strategy) processFilledOrder(o types.Order) { // collect trades baseSellQuantityReduction := fixedpoint.Zero - // baseSellQuantityReduction calculation should be only for BUY order - // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC - // if we don't reduce the sell quantity, than we might fail to place the sell order - if o.Side == types.SideTypeBuy { - baseSellQuantityReduction = s.aggregateOrderBaseFee(o) - s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) - - baseSellQuantityReduction = baseSellQuantityReduction.Round(s.Market.VolumePrecision, fixedpoint.Up) - s.logger.Infof("GRID BUY ORDER BASE FEE (Rounding): %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) - - newQuantity = newQuantity.Sub(baseSellQuantityReduction) - } - switch o.Side { case types.SideTypeSell: newSide = types.SideTypeBuy @@ -407,6 +394,17 @@ func (s *Strategy) processFilledOrder(o types.Order) { s.EmitGridProfit(s.GridProfitStats, profit) case types.SideTypeBuy: + // baseSellQuantityReduction calculation should be only for BUY order + // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC + // if we don't reduce the sell quantity, than we might fail to place the sell order + baseSellQuantityReduction = s.aggregateOrderBaseFee(o) + s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) + + baseSellQuantityReduction = baseSellQuantityReduction.Round(s.Market.VolumePrecision, fixedpoint.Up) + s.logger.Infof("GRID BUY ORDER BASE FEE (Rounding): %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) + + newQuantity = newQuantity.Sub(baseSellQuantityReduction) + newSide = types.SideTypeSell if !s.ProfitSpread.IsZero() { newPrice = newPrice.Add(s.ProfitSpread) From 04da988639d4424939525ecceb4bec43253a880d Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Feb 2023 18:45:18 +0800 Subject: [PATCH 0452/1392] grid2: check if we have o.AveragePrice, use it for newQuantity --- pkg/strategy/grid2/strategy.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 216f332dad..b2ac9f3ffa 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -363,7 +363,13 @@ func (s *Strategy) processFilledOrder(o types.Order) { newSide := types.SideTypeSell newPrice := o.Price newQuantity := o.Quantity - orderQuoteQuantity := o.Quantity.Mul(o.Price) + executedPrice := o.Price + if o.AveragePrice.Sign() > 0 { + executedPrice = o.AveragePrice + } + + // will be used for calculating quantity + orderExecutedQuoteAmount := o.Quantity.Mul(executedPrice) // collect trades baseSellQuantityReduction := fixedpoint.Zero @@ -382,7 +388,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { // use the profit to buy more inventory in the grid if s.Compound || s.EarnBase { - newQuantity = fixedpoint.Max(orderQuoteQuantity.Div(newPrice), s.Market.MinQuantity) + newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice), s.Market.MinQuantity) } else if s.QuantityOrAmount.Quantity.Sign() > 0 { newQuantity = s.QuantityOrAmount.Quantity } @@ -415,7 +421,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { } if s.EarnBase { - newQuantity = fixedpoint.Max(orderQuoteQuantity.Div(newPrice).Sub(baseSellQuantityReduction), s.Market.MinQuantity) + newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice).Sub(baseSellQuantityReduction), s.Market.MinQuantity) } } From 98739cc8a1d5cadaab1d1131364c5e37b59ab2b6 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 25 Feb 2023 02:24:39 +0800 Subject: [PATCH 0453/1392] grid2: avoid using loop iterator var --- pkg/strategy/grid2/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b2ac9f3ffa..46509a4f3a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1287,7 +1287,9 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.setGrid(grid) s.EmitGridReady() - for _, o := range filledOrders { + for i := range filledOrders { + // avoid using the iterator + o := filledOrders[i] s.processFilledOrder(o) time.Sleep(100 * time.Millisecond) } From 6f30b4551c1d09b48e3d234d825c2da6ab391c26 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 25 Feb 2023 02:56:08 +0800 Subject: [PATCH 0454/1392] upgrade golang sdk for docker image --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index acc49db3d3..d3f932f5a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # First stage container -FROM golang:1.17.11-alpine3.16 AS builder +FROM golang:1.18.10-alpine3.17 AS builder RUN apk add --no-cache git ca-certificates gcc libc-dev pkgconfig # gcc is for github.com/mattn/go-sqlite3 # ADD . $GOPATH/src/github.com/c9s/bbgo @@ -16,7 +16,7 @@ ADD . . RUN go build -o $GOPATH_ORIG/bin/bbgo ./cmd/bbgo # Second stage container -FROM alpine:3.16 +FROM alpine:3.17 # Create the default user 'bbgo' and assign to env 'USER' ENV USER=bbgo From 4f0c986709f634b7f2150b5f211e1900ee662695 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 25 Feb 2023 03:15:34 +0800 Subject: [PATCH 0455/1392] fix dockerfile for go-sqlite3 --- .dockerignore | 2 ++ Dockerfile | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index bccbeed7a5..6dfdf4f205 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,5 @@ /linode /frontend /desktop +/data +/output diff --git a/Dockerfile b/Dockerfile index d3f932f5a0..09a1f6482f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ # First stage container FROM golang:1.18.10-alpine3.17 AS builder -RUN apk add --no-cache git ca-certificates gcc libc-dev pkgconfig +RUN apk add --no-cache git ca-certificates gcc musl-dev libc-dev pkgconfig # gcc is for github.com/mattn/go-sqlite3 # ADD . $GOPATH/src/github.com/c9s/bbgo + WORKDIR $GOPATH/src/github.com/c9s/bbgo ARG GO_MOD_CACHE ENV WORKDIR=$GOPATH/src/github.com/c9s/bbgo @@ -11,8 +12,9 @@ ENV GOPATH_ORIG=$GOPATH ENV GOPATH=${GO_MOD_CACHE:+$WORKDIR/$GO_MOD_CACHE} ENV GOPATH=${GOPATH:-$GOPATH_ORIG} ENV CGO_ENABLED=1 -RUN cd $WORKDIR && go get github.com/mattn/go-sqlite3 +RUN cd $WORKDIR ADD . . +RUN go get github.com/mattn/go-sqlite3 RUN go build -o $GOPATH_ORIG/bin/bbgo ./cmd/bbgo # Second stage container From c1cc008ecc4380cd299d8bc75114d3dcd38f502d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Mar 2023 15:29:26 +0800 Subject: [PATCH 0456/1392] bbgo: add retry limit and exponential backoff to retry order --- pkg/bbgo/order_execution.go | 40 +++++++++++++++++++++--------- pkg/bbgo/order_executor_general.go | 3 ++- pkg/grpc/server.go | 2 +- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index fe3e98169e..9ff56d4dc0 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -310,17 +310,19 @@ func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders type OrderCallback func(order types.Order) // BatchRetryPlaceOrder places the orders and retries the failed orders -func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, orderCallback OrderCallback, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { +func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, orderCallback OrderCallback, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { var createdOrders types.OrderSlice var werr error // if the errIdx is nil, then we should iterate all the submit orders + // allocate a variable for new error index + var errIdxNext []int if len(errIdx) == 0 { for i, submitOrder := range submitOrders { createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) if err2 != nil { werr = multierr.Append(werr, err2) - errIdx = append(errIdx, i) + errIdxNext = append(errIdxNext, i) } else if createdOrder != nil { // if the order is successfully created, than we should copy the order tag createdOrder.Tag = submitOrder.Tag @@ -332,24 +334,36 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ createdOrders = append(createdOrders, *createdOrder) } } + + errIdx = errIdxNext } + timeoutCtx, cancelTimeout := context.WithTimeout(ctx, 30*time.Minute) + defer cancelTimeout() + // if we got any error, we should re-iterate the errored orders - for len(errIdx) > 0 { - time.Sleep(200 * time.Millisecond) + coolDownTime := 200 * time.Millisecond + + // set backoff max retries to 101 because https://ja.wikipedia.org/wiki/101%E5%9B%9E%E7%9B%AE%E3%81%AE%E3%83%97%E3%83%AD%E3%83%9D%E3%83%BC%E3%82%BA + backoffMaxRetries := uint64(101) + for retryRound := 0; len(errIdx) > 0 && retryRound < 10; retryRound++ { + // sleep for 200 millisecond between each retry + log.Warnf("retry round #%d, cooling down for %s", retryRound+1, coolDownTime) + time.Sleep(coolDownTime) - // allocate a variable for new error index - var errIdxNext []int + // reset error index since it's a new retry + errIdxNext = nil // iterate the error index and re-submit the order - for _, idx := range errIdx { + log.Warnf("starting retry round #%d...", retryRound+1) + for _, idx := range errIdxNext { submitOrder := submitOrders[idx] op := func() error { // can allocate permanent error backoff.Permanent(err) to stop backoff - createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) + createdOrder, err2 := exchange.SubmitOrder(timeoutCtx, submitOrder) if err2 == nil && createdOrder != nil { - // if the order is successfully created, than we should copy the order tag + // if the order is successfully created, then we should copy the order tag createdOrder.Tag = submitOrder.Tag if orderCallback != nil { @@ -362,8 +376,10 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ return err2 } - // if err2 := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 99)); err2 != nil { - if err2 := backoff.Retry(op, backoff.NewExponentialBackOff()); err2 != nil { + var bo backoff.BackOff = backoff.NewExponentialBackOff() + bo = backoff.WithMaxRetries(bo, backoffMaxRetries) + bo = backoff.WithContext(bo, timeoutCtx) + if err2 := backoff.Retry(op, bo); err2 != nil { werr = multierr.Append(werr, err2) errIdxNext = append(errIdxNext, idx) } @@ -373,5 +389,5 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ errIdx = errIdxNext } - return createdOrders, werr + return createdOrders, errIdx, werr } diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 746c45ed1a..916efcf0ed 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -223,7 +223,8 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. e.tradeCollector.Process() } - return BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, formattedOrders...) + createdOrders, _, err := BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, formattedOrders...) + return createdOrders, err } type OpenPositionOptions struct { diff --git a/pkg/grpc/server.go b/pkg/grpc/server.go index 21162bda71..7f22eed003 100644 --- a/pkg/grpc/server.go +++ b/pkg/grpc/server.go @@ -47,7 +47,7 @@ func (s *TradingService) SubmitOrder(ctx context.Context, request *pb.SubmitOrde } // we will return this error later because some orders could be succeeded - createdOrders, err := bbgo.BatchRetryPlaceOrder(ctx, session.Exchange, nil, nil, submitOrders...) + createdOrders, _, err := bbgo.BatchRetryPlaceOrder(ctx, session.Exchange, nil, nil, submitOrders...) // convert response resp := &pb.SubmitOrderResponse{ From f82af6e6ddfa73b8eba85845fb6c5038154d21ac Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Mar 2023 16:16:26 +0800 Subject: [PATCH 0457/1392] grid2: use UseCancelAllOrdersApiWhenClose --- pkg/strategy/grid2/strategy.go | 58 ++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 46509a4f3a..33399f74bc 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -9,9 +9,11 @@ import ( "sync" "time" + "github.com/cenkalti/backoff/v4" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.uber.org/multierr" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -58,6 +60,11 @@ type OrderExecutor interface { ActiveMakerOrders() *bbgo.ActiveOrderBook } +type advancedOrderCancelApi interface { + CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error) + CancelOrdersByGroupID(ctx context.Context, groupID int64) ([]types.Order, error) +} + //go:generate callbackgen -type Strategy type Strategy struct { Environment *bbgo.Environment @@ -126,6 +133,9 @@ type Strategy struct { ClearOpenOrdersIfMismatch bool `json:"clearOpenOrdersIfMismatch"` + // UseCancelAllOrdersApiWhenClose uses a different API to cancel all the orders on the market when closing a grid + UseCancelAllOrdersApiWhenClose bool `json:"useCancelAllOrdersApiWhenClose"` + ResetPositionWhenStart bool `json:"resetPositionWhenStart"` // PrometheusLabels will be used as the base prometheus labels @@ -812,16 +822,58 @@ func (s *Strategy) CloseGrid(ctx context.Context) error { bbgo.Sync(ctx, s) + var werr error + // now we can cancel the open orders s.logger.Infof("canceling grid orders...") - if err := s.orderExecutor.GracefulCancel(ctx); err != nil { - return err + + if s.UseCancelAllOrdersApiWhenClose { + s.logger.Infof("useCancelAllOrdersApiWhenClose is set, using advanced order cancel api for canceling...") + if service, ok := s.session.Exchange.(advancedOrderCancelApi); ok { + if s.OrderGroupID > 0 { + s.logger.Infof("found OrderGroupID (%d), using group ID for canceling orders...", s.OrderGroupID) + + op := func() error { + _, cancelErr := service.CancelOrdersByGroupID(ctx, int64(s.OrderGroupID)) + return cancelErr + } + err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) + if err != nil { + werr = multierr.Append(werr, err) + } + } + + time.Sleep(5 * time.Second) + + s.logger.Infof("checking %s open orders...", s.Symbol) + + openOrders, err := s.session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } + + if len(openOrders) > 0 { + s.logger.Infof("found %d open orders left, using cancel all orders api", len(openOrders)) + + op := func() error { + _, cancelErr := service.CancelOrdersBySymbol(ctx, s.Symbol) + return cancelErr + } + if err2 := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)); err2 != nil { + werr = multierr.Append(werr, err2) + } + } + } + } else { + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + werr = multierr.Append(werr, err) + } } // free the grid object s.setGrid(nil) s.updateGridNumOfOrdersMetrics() - return nil + return werr } func (s *Strategy) newGrid() *Grid { From 06eff47058680b5b20e1001a339aac5614a9934e Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Mar 2023 16:35:09 +0800 Subject: [PATCH 0458/1392] grid2: improve UseCancelAllOrdersApiWhenClose process --- pkg/exchange/max/convert.go | 6 +-- pkg/strategy/grid2/strategy.go | 71 +++++++++++++++++++++------------- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index dd1c8b39d3..80e01bab8f 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -100,7 +100,7 @@ func toGlobalOrderStatus(orderState max.OrderState, executedVolume, remainingVol } - log.Errorf("unknown order status: %v", orderState) + log.Errorf("can not convert MAX exchange order status, unknown order state: %q", orderState) return types.OrderStatus(orderState) } @@ -157,9 +157,9 @@ func toGlobalOrders(maxOrders []max.Order) (orders []types.Order, err error) { o, err := toGlobalOrder(localOrder) if err != nil { log.WithError(err).Error("order convert error") + } else { + orders = append(orders, *o) } - - orders = append(orders, *o) } return orders, err diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 33399f74bc..3c3657539c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -61,8 +61,9 @@ type OrderExecutor interface { } type advancedOrderCancelApi interface { + CancelAllOrders(ctx context.Context) ([]types.Order, error) CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error) - CancelOrdersByGroupID(ctx context.Context, groupID int64) ([]types.Order, error) + CancelOrdersByGroupID(ctx context.Context, groupID uint32) ([]types.Order, error) } //go:generate callbackgen -type Strategy @@ -827,41 +828,57 @@ func (s *Strategy) CloseGrid(ctx context.Context) error { // now we can cancel the open orders s.logger.Infof("canceling grid orders...") + service, support := s.session.Exchange.(advancedOrderCancelApi) + + if s.UseCancelAllOrdersApiWhenClose && !support { + s.logger.Warnf("advancedOrderCancelApi interface is not implemented, fallback to default graceful cancel, exchange %T", s.session.Exchange) + s.UseCancelAllOrdersApiWhenClose = false + } + if s.UseCancelAllOrdersApiWhenClose { s.logger.Infof("useCancelAllOrdersApiWhenClose is set, using advanced order cancel api for canceling...") - if service, ok := s.session.Exchange.(advancedOrderCancelApi); ok { - if s.OrderGroupID > 0 { - s.logger.Infof("found OrderGroupID (%d), using group ID for canceling orders...", s.OrderGroupID) - op := func() error { - _, cancelErr := service.CancelOrdersByGroupID(ctx, int64(s.OrderGroupID)) - return cancelErr - } - err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) - if err != nil { - werr = multierr.Append(werr, err) - } + if s.OrderGroupID > 0 { + s.logger.Infof("found OrderGroupID (%d), using group ID for canceling orders...", s.OrderGroupID) + + op := func() error { + _, cancelErr := service.CancelOrdersByGroupID(ctx, s.OrderGroupID) + return cancelErr + } + err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) + if err != nil { + werr = multierr.Append(werr, err) + } + } else { + s.logger.Infof("canceling all orders...") + op := func() error { + _, cancelErr := service.CancelAllOrders(ctx) + return cancelErr + } + err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) + if err != nil { + werr = multierr.Append(werr, err) } + } - time.Sleep(5 * time.Second) + time.Sleep(5 * time.Second) - s.logger.Infof("checking %s open orders...", s.Symbol) + s.logger.Infof("checking %s open orders...", s.Symbol) - openOrders, err := s.session.Exchange.QueryOpenOrders(ctx, s.Symbol) - if err != nil { - return err - } + openOrders, err := s.session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } - if len(openOrders) > 0 { - s.logger.Infof("found %d open orders left, using cancel all orders api", len(openOrders)) + if len(openOrders) > 0 { + s.logger.Infof("found %d open orders left, using cancel all orders api", len(openOrders)) - op := func() error { - _, cancelErr := service.CancelOrdersBySymbol(ctx, s.Symbol) - return cancelErr - } - if err2 := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)); err2 != nil { - werr = multierr.Append(werr, err2) - } + op := func() error { + _, cancelErr := service.CancelOrdersBySymbol(ctx, s.Symbol) + return cancelErr + } + if err2 := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)); err2 != nil { + werr = multierr.Append(werr, err2) } } } else { From 6137905f4226b02da3bb4831bbafff0b49c9d674 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Mar 2023 16:45:33 +0800 Subject: [PATCH 0459/1392] max: fix max v3 order cancel api --- pkg/exchange/max/exchange.go | 27 ++++++++++++++++--- .../v3/cancel_wallet_order_all_request.go | 7 ++++- ...cel_wallet_order_all_request_requestgen.go | 9 ++++--- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 0b5c0f8b45..0e4800ee9a 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -293,11 +293,18 @@ func (e *Exchange) CancelAllOrders(ctx context.Context) ([]types.Order, error) { } req := e.v3order.NewCancelWalletOrderAllRequest(walletType) - var maxOrders, err = req.Do(ctx) + var orderResponses, err = req.Do(ctx) if err != nil { return nil, err } + var maxOrders []maxapi.Order + for _, resp := range orderResponses { + if resp.Error == nil { + maxOrders = append(maxOrders, resp.Order) + } + } + return toGlobalOrders(maxOrders) } @@ -311,11 +318,18 @@ func (e *Exchange) CancelOrdersBySymbol(ctx context.Context, symbol string) ([]t req := e.v3order.NewCancelWalletOrderAllRequest(walletType) req.Market(market) - maxOrders, err := req.Do(ctx) + var orderResponses, err = req.Do(ctx) if err != nil { return nil, err } + var maxOrders []maxapi.Order + for _, resp := range orderResponses { + if resp.Error == nil { + maxOrders = append(maxOrders, resp.Order) + } + } + return toGlobalOrders(maxOrders) } @@ -328,11 +342,18 @@ func (e *Exchange) CancelOrdersByGroupID(ctx context.Context, groupID uint32) ([ req := e.v3order.NewCancelWalletOrderAllRequest(walletType) req.GroupID(groupID) - maxOrders, err := req.Do(ctx) + var orderResponses, err = req.Do(ctx) if err != nil { return nil, err } + var maxOrders []maxapi.Order + for _, resp := range orderResponses { + if resp.Error == nil { + maxOrders = append(maxOrders, resp.Order) + } + } + return toGlobalOrders(maxOrders) } diff --git a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go index 04825d3866..dffd263579 100644 --- a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go +++ b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go @@ -6,11 +6,16 @@ import "github.com/c9s/requestgen" //go:generate -command PostRequest requestgen -method POST //go:generate -command DeleteRequest requestgen -method DELETE +type OrderCancelResponse struct { + Order Order + Error *string +} + func (s *OrderService) NewCancelWalletOrderAllRequest(walletType WalletType) *CancelWalletOrderAllRequest { return &CancelWalletOrderAllRequest{client: s.Client, walletType: walletType} } -//go:generate DeleteRequest -url "/api/v3/wallet/:walletType/orders" -type CancelWalletOrderAllRequest -responseType []Order +//go:generate DeleteRequest -url "/api/v3/wallet/:walletType/orders" -type CancelWalletOrderAllRequest -responseType []OrderCancelResponse type CancelWalletOrderAllRequest struct { client requestgen.AuthenticatedAPIClient diff --git a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go index 0015693f3e..7184372a6d 100644 --- a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method DELETE -url /api/v3/wallet/:walletType/orders -type CancelWalletOrderAllRequest -responseType []Order"; DO NOT EDIT. +// Code generated by "requestgen -method DELETE -url /api/v3/wallet/:walletType/orders -type CancelWalletOrderAllRequest -responseType []OrderCancelResponse"; DO NOT EDIT. package v3 @@ -6,10 +6,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/c9s/bbgo/pkg/exchange/max/maxapi" "net/url" "reflect" "regexp" + + max "github.com/c9s/bbgo/pkg/exchange/max/maxapi" ) func (c *CancelWalletOrderAllRequest) Side(side string) *CancelWalletOrderAllRequest { @@ -165,7 +166,7 @@ func (c *CancelWalletOrderAllRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } -func (c *CancelWalletOrderAllRequest) Do(ctx context.Context) ([]max.Order, error) { +func (c *CancelWalletOrderAllRequest) Do(ctx context.Context) ([]OrderCancelResponse, error) { params, err := c.GetParameters() if err != nil { @@ -191,7 +192,7 @@ func (c *CancelWalletOrderAllRequest) Do(ctx context.Context) ([]max.Order, erro return nil, err } - var apiResponse []max.Order + var apiResponse []OrderCancelResponse if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } From 39f8557231e301f2ec829ecf556cc1493980a559 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Mar 2023 17:42:01 +0800 Subject: [PATCH 0460/1392] bbgo: if the error is context.Canceled, exit the retry loop --- pkg/bbgo/order_execution.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 9ff56d4dc0..b98f1a42ba 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -346,6 +346,7 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ // set backoff max retries to 101 because https://ja.wikipedia.org/wiki/101%E5%9B%9E%E7%9B%AE%E3%81%AE%E3%83%97%E3%83%AD%E3%83%9D%E3%83%BC%E3%82%BA backoffMaxRetries := uint64(101) +batchRetryOrder: for retryRound := 0; len(errIdx) > 0 && retryRound < 10; retryRound++ { // sleep for 200 millisecond between each retry log.Warnf("retry round #%d, cooling down for %s", retryRound+1, coolDownTime) @@ -380,6 +381,10 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ bo = backoff.WithMaxRetries(bo, backoffMaxRetries) bo = backoff.WithContext(bo, timeoutCtx) if err2 := backoff.Retry(op, bo); err2 != nil { + if err2 == context.Canceled { + break batchRetryOrder + } + werr = multierr.Append(werr, err2) errIdxNext = append(errIdxNext, idx) } From b13efdf30eb52719ba06fe150f40e28c259d37b5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Mar 2023 20:05:35 +0800 Subject: [PATCH 0461/1392] grid2: calculate grid profit only when the reverse order is placed --- pkg/strategy/grid2/strategy.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3c3657539c..8154eeaffa 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -370,6 +370,8 @@ func (s *Strategy) aggregateOrderBaseFee(o types.Order) fixedpoint.Value { } func (s *Strategy) processFilledOrder(o types.Order) { + var profit *GridProfit = nil + // check order fee newSide := types.SideTypeSell newPrice := o.Price @@ -405,10 +407,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { } // TODO: need to consider sell order fee for the profit calculation - profit := s.calculateProfit(o, newPrice, newQuantity) - s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) - s.GridProfitStats.AddProfit(profit) - s.EmitGridProfit(s.GridProfitStats, profit) + profit = s.calculateProfit(o, newPrice, newQuantity) case types.SideTypeBuy: // baseSellQuantityReduction calculation should be only for BUY order @@ -449,12 +448,20 @@ func (s *Strategy) processFilledOrder(o types.Order) { s.logger.Infof("SUBMIT GRID REVERSE ORDER: %s", orderForm.String()) - if createdOrders, err := s.orderExecutor.SubmitOrders(context.Background(), orderForm); err != nil { - s.logger.WithError(err).Errorf("can not submit arbitrage order") - } else { - s.logger.Infof("GRID REVERSE ORDER IS CREATED: %+v", createdOrders) + createdOrders, err := s.orderExecutor.SubmitOrders(context.Background(), orderForm) + if err != nil { + s.logger.WithError(err).Errorf("GRID REVERSE ORDER SUBMISSION ERROR: order: %s", orderForm.String()) + return } + s.logger.Infof("GRID REVERSE ORDER IS CREATED: %+v", createdOrders) + + // we calculate profit only when the order is placed successfully + if profit != nil { + s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) + s.GridProfitStats.AddProfit(profit) + s.EmitGridProfit(s.GridProfitStats, profit) + } } // handleOrderFilled is called when an order status is FILLED From b2bbf2d6ca60bb3048a5eab19b1ff592985d87bf Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Mar 2023 21:05:28 +0800 Subject: [PATCH 0462/1392] grid2: add comment to the sync call --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 8154eeaffa..3dbf239272 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -759,6 +759,8 @@ func (s *Strategy) newTriggerPriceHandler(ctx context.Context, session *bbgo.Exc func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.ExchangeSession) func(o types.Order) { return func(o types.Order) { s.handleOrderFilled(o) + + // sync the profits to redis bbgo.Sync(ctx, s) s.updateGridNumOfOrdersMetrics() From f553ee05a095929dd1aa833d8ee56f443ea5cc9d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Mar 2023 21:49:15 +0800 Subject: [PATCH 0463/1392] grid2: log base fee rounding precision --- pkg/strategy/grid2/backtest_test.go | 28 +++++++++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 5 ++++- pkg/strategy/grid2/strategy_test.go | 30 +---------------------------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/pkg/strategy/grid2/backtest_test.go b/pkg/strategy/grid2/backtest_test.go index 8c75f88c9b..aad63261e5 100644 --- a/pkg/strategy/grid2/backtest_test.go +++ b/pkg/strategy/grid2/backtest_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/backtest" @@ -14,6 +15,7 @@ import ( "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" ) func RunBacktest(t *testing.T, strategy bbgo.SingleExchangeStrategy) { @@ -154,3 +156,29 @@ func RunBacktest(t *testing.T, strategy bbgo.SingleExchangeStrategy) { <-doneC // }}} } + +func TestBacktestStrategy(t *testing.T) { + if v, ok := util.GetEnvVarBool("TEST_BACKTEST"); !ok || !v { + t.Skip("backtest flag is required") + return + } + + market := types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + TickSize: number(0.01), + PricePrecision: 2, + VolumePrecision: 8, + } + strategy := &Strategy{ + logger: logrus.NewEntry(logrus.New()), + Symbol: "BTCUSDT", + Market: market, + GridProfitStats: newGridProfitStats(market), + UpperPrice: number(60_000), + LowerPrice: number(28_000), + GridNum: 100, + QuoteInvestment: number(9000.0), + } + RunBacktest(t, strategy) +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3dbf239272..91c5866c82 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -417,7 +417,10 @@ func (s *Strategy) processFilledOrder(o types.Order) { s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) baseSellQuantityReduction = baseSellQuantityReduction.Round(s.Market.VolumePrecision, fixedpoint.Up) - s.logger.Infof("GRID BUY ORDER BASE FEE (Rounding): %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) + s.logger.Infof("GRID BUY ORDER BASE FEE (Rounding with precision %d): %s %s", + s.Market.VolumePrecision, + baseSellQuantityReduction.String(), + s.Market.BaseCurrency) newQuantity = newQuantity.Sub(baseSellQuantityReduction) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 388e1c9cef..242b83ee01 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -13,11 +13,9 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" + gridmocks "github.com/c9s/bbgo/pkg/strategy/grid2/mocks" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types/mocks" - "github.com/c9s/bbgo/pkg/util" - - gridmocks "github.com/c9s/bbgo/pkg/strategy/grid2/mocks" ) func init() { @@ -919,29 +917,3 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { assert.EqualError(t, err, "need at least 14979.995500 USDT for quote investment, 10000.000000 USDT given") }) } - -func TestBacktestStrategy(t *testing.T) { - if v, ok := util.GetEnvVarBool("TEST_BACKTEST"); !ok || !v { - t.Skip("backtest flag is required") - return - } - - market := types.Market{ - BaseCurrency: "BTC", - QuoteCurrency: "USDT", - TickSize: number(0.01), - PricePrecision: 2, - VolumePrecision: 8, - } - strategy := &Strategy{ - logger: logrus.NewEntry(logrus.New()), - Symbol: "BTCUSDT", - Market: market, - GridProfitStats: newGridProfitStats(market), - UpperPrice: number(60_000), - LowerPrice: number(28_000), - GridNum: 100, - QuoteInvestment: number(9000.0), - } - RunBacktest(t, strategy) -} From 1d8df08a74da33b37ba7a12c80a226987476fff5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Mar 2023 22:21:24 +0800 Subject: [PATCH 0464/1392] fixedpoint: fix fixedpoint rounding --- pkg/fixedpoint/convert.go | 12 +++++++----- pkg/fixedpoint/dec_test.go | 11 +++++++++-- pkg/strategy/grid2/strategy.go | 6 +++++- pkg/strategy/grid2/strategy_test.go | 10 ++++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/pkg/fixedpoint/convert.go b/pkg/fixedpoint/convert.go index 1d025abf12..b4023c555f 100644 --- a/pkg/fixedpoint/convert.go +++ b/pkg/fixedpoint/convert.go @@ -41,16 +41,18 @@ func (v Value) Trunc() Value { func (v Value) Round(r int, mode RoundingMode) Value { pow := math.Pow10(r) - result := v.Float64() * pow + f := v.Float64() * pow switch mode { case Up: - return NewFromFloat(math.Ceil(result) / pow) + f = math.Ceil(f) / pow case HalfUp: - return NewFromFloat(math.Floor(result+0.5) / pow) + f = math.Floor(f+0.5) / pow case Down: - return NewFromFloat(math.Floor(result) / pow) + f = math.Floor(f) / pow } - return v + + s := strconv.FormatFloat(f, 'f', r, 64) + return MustNewFromString(s) } func (v Value) Value() (driver.Value, error) { diff --git a/pkg/fixedpoint/dec_test.go b/pkg/fixedpoint/dec_test.go index 8c72582f61..e86e911436 100644 --- a/pkg/fixedpoint/dec_test.go +++ b/pkg/fixedpoint/dec_test.go @@ -2,10 +2,11 @@ package fixedpoint import ( "encoding/json" - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" "math/big" "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) const Delta = 1e-9 @@ -124,6 +125,12 @@ func TestRound(t *testing.T) { assert.Equal(t, "1.23", s.Round(2, Down).String()) } +func TestNewFromString(t *testing.T) { + f, err := NewFromString("0.00000003") + assert.NoError(t, err) + assert.Equal(t, "0.00000003", f.String()) +} + func TestFromString(t *testing.T) { f := MustNewFromString("0.004075") assert.Equal(t, "0.004075", f.String()) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 91c5866c82..5a7b889019 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -416,7 +416,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { baseSellQuantityReduction = s.aggregateOrderBaseFee(o) s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) - baseSellQuantityReduction = baseSellQuantityReduction.Round(s.Market.VolumePrecision, fixedpoint.Up) + baseSellQuantityReduction = roundUpMarketQuantity(s.Market, baseSellQuantityReduction) s.logger.Infof("GRID BUY ORDER BASE FEE (Rounding with precision %d): %s %s", s.Market.VolumePrecision, baseSellQuantityReduction.String(), @@ -1766,3 +1766,7 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha return false, nil } + +func roundUpMarketQuantity(market types.Market, v fixedpoint.Value) fixedpoint.Value { + return v.Round(market.VolumePrecision, fixedpoint.Up) +} diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 242b83ee01..a52281bd3d 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -917,3 +917,13 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { assert.EqualError(t, err, "need at least 14979.995500 USDT for quote investment, 10000.000000 USDT given") }) } + +func Test_roundUpMarketQuantity(t *testing.T) { + q := number("0.00000003") + assert.Equal(t, "0.00000003", q.String()) + + q3 := roundUpMarketQuantity(types.Market{ + VolumePrecision: 8, + }, q) + assert.Equal(t, "0.00000003", q3.String(), "rounding prec 8") +} From 6ee2c34649368b382308c7e6c7a550b01645cc48 Mon Sep 17 00:00:00 2001 From: Owen Wu <1449069+yubing744@users.noreply.github.com> Date: Wed, 1 Mar 2023 23:12:13 +0800 Subject: [PATCH 0465/1392] feat: add private strategy demo trading-gpt --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 25806e1b0a..d1b2163726 100644 --- a/README.md +++ b/README.md @@ -445,6 +445,7 @@ See also: - - - +- ## Command Usages From 4aa25db3ed9e7a9b658389d7bffc269229121b89 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 14:03:22 +0800 Subject: [PATCH 0466/1392] grid2: add one more calculateMinimalQuoteInvestment test case --- pkg/strategy/grid2/strategy_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index a52281bd3d..15b2a94f19 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -888,9 +888,23 @@ func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { } func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { - s := newTestStrategy() + + t.Run("7 grids", func(t *testing.T) { + s := newTestStrategy() + s.UpperPrice = number(1660) + s.LowerPrice = number(1630) + s.QuoteInvestment = number(61) + s.GridNum = 7 + grid := s.newGrid() + minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid) + assert.InDelta(t, 60.46, minQuoteInvestment.Float64(), 0.01) + + err := s.checkMinimalQuoteInvestment(grid) + assert.NoError(t, err) + }) t.Run("10 grids", func(t *testing.T) { + s := newTestStrategy() // 10_000 * 0.001 = 10USDT // 20_000 * 0.001 = 20USDT // hence we should have at least: 20USDT * 10 grids @@ -905,6 +919,7 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { }) t.Run("1000 grids", func(t *testing.T) { + s := newTestStrategy() s.QuoteInvestment = number(10_000) s.GridNum = 1000 From 729d32af70832429ce67403b3509ebf2fc8e300e Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 15:14:21 +0800 Subject: [PATCH 0467/1392] grid2: add minimal quote investment check error log --- pkg/strategy/grid2/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5a7b889019..00f659b5aa 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1612,6 +1612,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. if s.QuoteInvestment.Sign() > 0 { grid := s.newGrid() if err := s.checkMinimalQuoteInvestment(grid); err != nil { + s.logger.Errorf("check minimal quote investment failed, market info: %+v", s.Market) return err } } From 01ecdc8d6b623746e54f0af55371e36d840798f9 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 15:41:11 +0800 Subject: [PATCH 0468/1392] fix order submit retry --- pkg/bbgo/order_execution.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index b98f1a42ba..695c2067b9 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -357,12 +357,16 @@ batchRetryOrder: // iterate the error index and re-submit the order log.Warnf("starting retry round #%d...", retryRound+1) - for _, idx := range errIdxNext { + for _, idx := range errIdx { submitOrder := submitOrders[idx] op := func() error { // can allocate permanent error backoff.Permanent(err) to stop backoff createdOrder, err2 := exchange.SubmitOrder(timeoutCtx, submitOrder) + if err2 != nil { + log.WithError(err2).Errorf("submit order error") + } + if err2 == nil && createdOrder != nil { // if the order is successfully created, then we should copy the order tag createdOrder.Tag = submitOrder.Tag @@ -379,9 +383,10 @@ batchRetryOrder: var bo backoff.BackOff = backoff.NewExponentialBackOff() bo = backoff.WithMaxRetries(bo, backoffMaxRetries) - bo = backoff.WithContext(bo, timeoutCtx) + // bo = backoff.WithContext(bo, timeoutCtx) if err2 := backoff.Retry(op, bo); err2 != nil { if err2 == context.Canceled { + log.Warnf("context canceled error, stop retry") break batchRetryOrder } From 11329dffe73ff7fb1401021db6749232d1692381 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 15:50:10 +0800 Subject: [PATCH 0469/1392] grid2: add StopIfLessThanMinimalQuoteInvestment option --- pkg/strategy/grid2/strategy.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 00f659b5aa..63a8a76302 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -137,8 +137,11 @@ type Strategy struct { // UseCancelAllOrdersApiWhenClose uses a different API to cancel all the orders on the market when closing a grid UseCancelAllOrdersApiWhenClose bool `json:"useCancelAllOrdersApiWhenClose"` + // ResetPositionWhenStart resets the position when the strategy is started ResetPositionWhenStart bool `json:"resetPositionWhenStart"` + StopIfLessThanMinimalQuoteInvestment bool `json:"stopIfLessThanMinimalQuoteInvestment"` + // PrometheusLabels will be used as the base prometheus labels PrometheusLabels prometheus.Labels `json:"prometheusLabels"` @@ -1612,8 +1615,13 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. if s.QuoteInvestment.Sign() > 0 { grid := s.newGrid() if err := s.checkMinimalQuoteInvestment(grid); err != nil { - s.logger.Errorf("check minimal quote investment failed, market info: %+v", s.Market) - return err + if s.StopIfLessThanMinimalQuoteInvestment { + s.logger.WithError(err).Errorf("check minimal quote investment failed, market info: %+v", s.Market) + return err + } else { + // if no, just warning + s.logger.WithError(err).Warnf("minimal quote investment may be not enough, market info: %+v", s.Market) + } } } From 385a97448d45316d5f664a45b7250289c806e42d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 15:53:42 +0800 Subject: [PATCH 0470/1392] grid2: add StopIfLessThanMinimalQuoteInvestment doc comment --- pkg/strategy/grid2/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 63a8a76302..c655416666 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -140,6 +140,7 @@ type Strategy struct { // ResetPositionWhenStart resets the position when the strategy is started ResetPositionWhenStart bool `json:"resetPositionWhenStart"` + // StopIfLessThanMinimalQuoteInvestment stops the strategy if the quote investment does not match StopIfLessThanMinimalQuoteInvestment bool `json:"stopIfLessThanMinimalQuoteInvestment"` // PrometheusLabels will be used as the base prometheus labels From 3cb190c2c76d6eca1ce7784495486017a8610500 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 16:16:14 +0800 Subject: [PATCH 0471/1392] bbgo: apply logger into the order executor --- pkg/bbgo/order_execution.go | 12 ++++++++---- pkg/bbgo/order_executor_general.go | 8 +++++++- pkg/grpc/server.go | 2 +- pkg/strategy/grid2/strategy.go | 4 ++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 695c2067b9..feaf131800 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -310,7 +310,11 @@ func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders type OrderCallback func(order types.Order) // BatchRetryPlaceOrder places the orders and retries the failed orders -func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, orderCallback OrderCallback, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { +func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, orderCallback OrderCallback, logger log.FieldLogger, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { + if logger == nil { + logger = log.StandardLogger() + } + var createdOrders types.OrderSlice var werr error @@ -349,14 +353,14 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ batchRetryOrder: for retryRound := 0; len(errIdx) > 0 && retryRound < 10; retryRound++ { // sleep for 200 millisecond between each retry - log.Warnf("retry round #%d, cooling down for %s", retryRound+1, coolDownTime) + logger.Warnf("retry round #%d, cooling down for %s", retryRound+1, coolDownTime) time.Sleep(coolDownTime) // reset error index since it's a new retry errIdxNext = nil // iterate the error index and re-submit the order - log.Warnf("starting retry round #%d...", retryRound+1) + logger.Warnf("starting retry round #%d...", retryRound+1) for _, idx := range errIdx { submitOrder := submitOrders[idx] @@ -386,7 +390,7 @@ batchRetryOrder: // bo = backoff.WithContext(bo, timeoutCtx) if err2 := backoff.Retry(op, bo); err2 != nil { if err2 == context.Canceled { - log.Warnf("context canceled error, stop retry") + logger.Warnf("context canceled error, stop retry") break batchRetryOrder } diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 916efcf0ed..ab0eab7c29 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -36,6 +36,8 @@ type GeneralOrderExecutor struct { orderStore *OrderStore tradeCollector *TradeCollector + logger log.FieldLogger + marginBaseMaxBorrowable, marginQuoteMaxBorrowable fixedpoint.Value disableNotify bool @@ -211,6 +213,10 @@ func (e *GeneralOrderExecutor) FastSubmitOrders(ctx context.Context, submitOrder } +func (e *GeneralOrderExecutor) SetLogger(logger log.FieldLogger) { + e.logger = logger +} + func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { formattedOrders, err := e.session.FormatOrders(submitOrders) if err != nil { @@ -223,7 +229,7 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. e.tradeCollector.Process() } - createdOrders, _, err := BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, formattedOrders...) + createdOrders, _, err := BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, log.StandardLogger(), formattedOrders...) return createdOrders, err } diff --git a/pkg/grpc/server.go b/pkg/grpc/server.go index 7f22eed003..3998c1709d 100644 --- a/pkg/grpc/server.go +++ b/pkg/grpc/server.go @@ -47,7 +47,7 @@ func (s *TradingService) SubmitOrder(ctx context.Context, request *pb.SubmitOrde } // we will return this error later because some orders could be succeeded - createdOrders, _, err := bbgo.BatchRetryPlaceOrder(ctx, session.Exchange, nil, nil, submitOrders...) + createdOrders, _, err := bbgo.BatchRetryPlaceOrder(ctx, session.Exchange, nil, nil, log.StandardLogger(), submitOrders...) // convert response resp := &pb.SubmitOrderResponse{ diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c655416666..0b42ade449 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1641,6 +1641,10 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. }) orderExecutor.ActiveMakerOrders().OnFilled(s.newOrderUpdateHandler(ctx, session)) + if s.logger != nil { + orderExecutor.SetLogger(s.logger) + } + s.orderExecutor = orderExecutor s.OnGridProfit(func(stats *GridProfitStats, profit *GridProfit) { From a2c6b0d9ac36f48d22e7235cd742d4b8c49f3c27 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 16:19:59 +0800 Subject: [PATCH 0472/1392] go: mod tidy --- go.mod | 4 ++-- go.sum | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index e1b60948af..ee4b893c32 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/c-bata/goptuna v0.8.1 github.com/c9s/requestgen v1.3.0 github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b + github.com/cenkalti/backoff/v4 v4.2.0 github.com/cheggaaa/pb/v3 v3.0.8 github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 github.com/ethereum/go-ethereum v1.10.23 @@ -26,6 +27,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 + github.com/heroku/rollrus v0.2.0 github.com/jedib0t/go-pretty/v6 v6.3.6 github.com/jmoiron/sqlx v1.3.4 github.com/joho/godotenv v1.3.0 @@ -68,7 +70,6 @@ require ( github.com/bitly/go-simplejson v0.5.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cockroachdb/apd v1.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect @@ -91,7 +92,6 @@ require ( github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/heroku/rollrus v0.2.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/go.sum b/go.sum index b5698fb3d0..3e90668ad9 100644 --- a/go.sum +++ b/go.sum @@ -49,10 +49,6 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= -github.com/adshao/go-binance/v2 v2.3.5 h1:WVYZecm0w8l14YoWlnKZj6xxZT2AKMTHpMQSqIX1xxA= -github.com/adshao/go-binance/v2 v2.3.5/go.mod h1:8Pg/FGTLyAhq8QXA0IkoReKyRpoxJcK3LVujKDAZV/c= -github.com/adshao/go-binance/v2 v2.3.8 h1:9VsAX4jUopnIOlzrvnKUFUf9SWB/nwPgJtUsM2dkj6A= -github.com/adshao/go-binance/v2 v2.3.8/go.mod h1:Z3MCnWI0gHC4Rea8TWiF3aN1t4nV9z3CaU/TeHcKsLM= github.com/adshao/go-binance/v2 v2.3.10 h1:iWtHD/sQ8GK6r+cSMMdOynpGI/4Q6P5LZtiEHdWOjag= github.com/adshao/go-binance/v2 v2.3.10/go.mod h1:Z3MCnWI0gHC4Rea8TWiF3aN1t4nV9z3CaU/TeHcKsLM= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -343,7 +339,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -409,7 +404,6 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -511,7 +505,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= @@ -625,8 +618,6 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slack-go/slack v0.10.1 h1:BGbxa0kMsGEvLOEoZmYs8T1wWfoZXwmQFBb6FgYCXUA= @@ -643,7 +634,6 @@ github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= @@ -918,8 +908,6 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 5c3a01e65b156212462c0ba0ed30a6543f9789b0 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 16:57:29 +0800 Subject: [PATCH 0473/1392] bbgo: fix logger usage --- pkg/bbgo/order_executor_general.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index ab0eab7c29..219955c15f 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -229,7 +229,7 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. e.tradeCollector.Process() } - createdOrders, _, err := BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, log.StandardLogger(), formattedOrders...) + createdOrders, _, err := BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, e.logger, formattedOrders...) return createdOrders, err } From f4b012623f6d50bb7f0d3abc84ad5e2d4f1008ca Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 16:58:14 +0800 Subject: [PATCH 0474/1392] bbgo: add back retry timeout context --- pkg/bbgo/order_execution.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index feaf131800..a4a52c0954 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -387,7 +387,7 @@ batchRetryOrder: var bo backoff.BackOff = backoff.NewExponentialBackOff() bo = backoff.WithMaxRetries(bo, backoffMaxRetries) - // bo = backoff.WithContext(bo, timeoutCtx) + bo = backoff.WithContext(bo, timeoutCtx) if err2 := backoff.Retry(op, bo); err2 != nil { if err2 == context.Canceled { logger.Warnf("context canceled error, stop retry") From ae5bd507a861ed2b288a867dd9106f22cdf8b83b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 17:17:18 +0800 Subject: [PATCH 0475/1392] bbgo: add BBGO_SUBMIT_ORDER_RETRY_TIMEOUT env var for overriding timeout --- pkg/bbgo/order_execution.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index a4a52c0954..d5f8fc2a06 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -12,8 +12,17 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" ) +var DefaultSubmitOrderRetryTimeout = 5 * time.Minute + +func init() { + if du, ok := util.GetEnvVarDuration("BBGO_SUBMIT_ORDER_RETRY_TIMEOUT"); ok && du > 0 { + DefaultSubmitOrderRetryTimeout = du + } +} + type OrderExecutor interface { SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) CancelOrders(ctx context.Context, orders ...types.Order) error @@ -342,7 +351,7 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ errIdx = errIdxNext } - timeoutCtx, cancelTimeout := context.WithTimeout(ctx, 30*time.Minute) + timeoutCtx, cancelTimeout := context.WithTimeout(ctx, DefaultSubmitOrderRetryTimeout) defer cancelTimeout() // if we got any error, we should re-iterate the errored orders From 6947c8b10468feacdabd90425ad42a84f01c340d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 17:33:58 +0800 Subject: [PATCH 0476/1392] grid2: improve clean up --- pkg/strategy/grid2/strategy.go | 45 +++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 0b42ade449..597d548c8a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -831,20 +831,15 @@ func (s *Strategy) OpenGrid(ctx context.Context) error { return s.openGrid(ctx, s.session) } -// CloseGrid closes the grid orders -func (s *Strategy) CloseGrid(ctx context.Context) error { - s.logger.Infof("closing %s grid", s.Symbol) - - defer s.EmitGridClosed() - - bbgo.Sync(ctx, s) - +func (s *Strategy) cancelAll(ctx context.Context) error { var werr error - // now we can cancel the open orders - s.logger.Infof("canceling grid orders...") + session := s.session + if session == nil { + session = s.ExchangeSession + } - service, support := s.session.Exchange.(advancedOrderCancelApi) + service, support := session.Exchange.(advancedOrderCancelApi) if s.UseCancelAllOrdersApiWhenClose && !support { s.logger.Warnf("advancedOrderCancelApi interface is not implemented, fallback to default graceful cancel, exchange %T", s.session.Exchange) @@ -903,10 +898,26 @@ func (s *Strategy) CloseGrid(ctx context.Context) error { } } + return werr +} + +// CloseGrid closes the grid orders +func (s *Strategy) CloseGrid(ctx context.Context) error { + s.logger.Infof("closing %s grid", s.Symbol) + + defer s.EmitGridClosed() + + bbgo.Sync(ctx, s) + + // now we can cancel the open orders + s.logger.Infof("canceling grid orders...") + + err := s.cancelAll(ctx) + // free the grid object s.setGrid(nil) s.updateGridNumOfOrdersMetrics() - return werr + return err } func (s *Strategy) newGrid() *Grid { @@ -1555,14 +1566,8 @@ func (s *Strategy) CleanUp(ctx context.Context) error { if s.ExchangeSession == nil { return errors.New("ExchangeSession is nil, can not clean up") } - - openOrders, err := s.ExchangeSession.Exchange.QueryOpenOrders(ctx, s.Symbol) - if err != nil { - return err - } - - err = s.ExchangeSession.Exchange.CancelOrders(ctx, openOrders...) - return errors.Wrapf(err, "can not cancel %s orders", s.Symbol) + + return s.cancelAll(ctx) } func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { From 5212365d2ffff734295ad0cf0dc18132dd72e81b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 17:40:44 +0800 Subject: [PATCH 0477/1392] grid2: remove s.ExchangeSession check --- pkg/strategy/grid2/strategy.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 597d548c8a..7c5e21511d 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1563,10 +1563,6 @@ func (s *Strategy) newPrometheusLabels() prometheus.Labels { } func (s *Strategy) CleanUp(ctx context.Context) error { - if s.ExchangeSession == nil { - return errors.New("ExchangeSession is nil, can not clean up") - } - return s.cancelAll(ctx) } From 86584b01b9d716dcc088d6995545d7a2ba21235d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 18:05:48 +0800 Subject: [PATCH 0478/1392] grid2: fix exchange session field --- pkg/strategy/grid2/strategy.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7c5e21511d..6bd9fcca48 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -842,7 +842,7 @@ func (s *Strategy) cancelAll(ctx context.Context) error { service, support := session.Exchange.(advancedOrderCancelApi) if s.UseCancelAllOrdersApiWhenClose && !support { - s.logger.Warnf("advancedOrderCancelApi interface is not implemented, fallback to default graceful cancel, exchange %T", s.session.Exchange) + s.logger.Warnf("advancedOrderCancelApi interface is not implemented, fallback to default graceful cancel, exchange %T", session) s.UseCancelAllOrdersApiWhenClose = false } @@ -876,7 +876,7 @@ func (s *Strategy) cancelAll(ctx context.Context) error { s.logger.Infof("checking %s open orders...", s.Symbol) - openOrders, err := s.session.Exchange.QueryOpenOrders(ctx, s.Symbol) + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { return err } @@ -1563,6 +1563,10 @@ func (s *Strategy) newPrometheusLabels() prometheus.Labels { } func (s *Strategy) CleanUp(ctx context.Context) error { + if s.ExchangeSession != nil { + s.session = s.ExchangeSession + } + return s.cancelAll(ctx) } From c5e2acf0f5f122d5388066dedd387459e241694c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 18:08:26 +0800 Subject: [PATCH 0479/1392] grid2: call Initialize in clean up --- pkg/strategy/grid2/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6bd9fcca48..36a0d3b7e3 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1567,6 +1567,7 @@ func (s *Strategy) CleanUp(ctx context.Context) error { s.session = s.ExchangeSession } + _ = s.Initialize() return s.cancelAll(ctx) } From 904491e750c54a2975b1d71f8fa133f5d96bd79e Mon Sep 17 00:00:00 2001 From: narumi Date: Thu, 2 Mar 2023 15:56:25 +0800 Subject: [PATCH 0480/1392] add orderType parameter --- config/rebalance.yaml | 4 ++-- pkg/strategy/rebalance/strategy.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/rebalance.yaml b/config/rebalance.yaml index 340c02dc4e..e9a01e94b7 100644 --- a/config/rebalance.yaml +++ b/config/rebalance.yaml @@ -36,6 +36,6 @@ exchangeStrategies: MAX: 15% USDT: 10% threshold: 1% - # max amount to buy or sell per order - maxAmount: 1_000 + maxAmount: 1_000 # max amount to buy or sell per order + orderType: LIMIT_MAKER # LIMIT, LIMIT_MAKER or MARKET dryRun: false diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index cb1c395593..41eef02dcd 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -24,9 +24,9 @@ type Strategy struct { QuoteCurrency string `json:"quoteCurrency"` TargetWeights types.ValueMap `json:"targetWeights"` Threshold fixedpoint.Value `json:"threshold"` + MaxAmount fixedpoint.Value `json:"maxAmount"` // max amount to buy or sell per order + OrderType types.OrderType `json:"orderType"` DryRun bool `json:"dryRun"` - // max amount to buy or sell per order - MaxAmount fixedpoint.Value `json:"maxAmount"` activeOrderBook *bbgo.ActiveOrderBook } @@ -199,7 +199,7 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context, session *bbgo.Excha order := types.SubmitOrder{ Symbol: symbol, Side: side, - Type: types.OrderTypeLimit, + Type: s.OrderType, Quantity: quantity, Price: currentPrice, } From e915825ac6963d259299e0987b1d475333e7720c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Mar 2023 18:16:09 +0800 Subject: [PATCH 0481/1392] grid2: defer call grid closed --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 36a0d3b7e3..a7945acef7 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1568,6 +1568,8 @@ func (s *Strategy) CleanUp(ctx context.Context) error { } _ = s.Initialize() + + defer s.EmitGridClosed() return s.cancelAll(ctx) } From bc7a071dbde12a737862016b4d8480d2d9546b49 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Thu, 2 Mar 2023 22:33:33 +0800 Subject: [PATCH 0482/1392] FIX: add persistence service to environment --- pkg/bbgo/bootstrap.go | 6 +++--- pkg/bbgo/environment.go | 27 ++++++++++++++------------- pkg/bbgo/persistence.go | 5 +++-- pkg/strategy/grid2/strategy.go | 2 +- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/pkg/bbgo/bootstrap.go b/pkg/bbgo/bootstrap.go index 717b1447a5..78ec8eaebe 100644 --- a/pkg/bbgo/bootstrap.go +++ b/pkg/bbgo/bootstrap.go @@ -19,7 +19,7 @@ func BootstrapEnvironmentLightweight(ctx context.Context, environ *Environment, } if userConfig.Persistence != nil { - if err := ConfigurePersistence(ctx, userConfig.Persistence); err != nil { + if err := ConfigurePersistence(ctx, environ, userConfig.Persistence); err != nil { return errors.Wrap(err, "persistence configure error") } } @@ -41,7 +41,7 @@ func BootstrapEnvironment(ctx context.Context, environ *Environment, userConfig } if userConfig.Persistence != nil { - if err := ConfigurePersistence(ctx, userConfig.Persistence); err != nil { + if err := ConfigurePersistence(ctx, environ, userConfig.Persistence); err != nil { return errors.Wrap(err, "persistence configure error") } } @@ -55,4 +55,4 @@ func BootstrapEnvironment(ctx context.Context, environ *Environment, userConfig func BootstrapBacktestEnvironment(ctx context.Context, environ *Environment) error { return environ.ConfigureDatabase(ctx) -} +} \ No newline at end of file diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index ba41a051e5..d35e125964 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -78,18 +78,19 @@ const ( // Environment presents the real exchange data layer type Environment struct { - DatabaseService *service.DatabaseService - OrderService *service.OrderService - TradeService *service.TradeService - ProfitService *service.ProfitService - PositionService *service.PositionService - BacktestService *service.BacktestService - RewardService *service.RewardService - MarginService *service.MarginService - SyncService *service.SyncService - AccountService *service.AccountService - WithdrawService *service.WithdrawService - DepositService *service.DepositService + DatabaseService *service.DatabaseService + OrderService *service.OrderService + TradeService *service.TradeService + ProfitService *service.ProfitService + PositionService *service.PositionService + BacktestService *service.BacktestService + RewardService *service.RewardService + MarginService *service.MarginService + SyncService *service.SyncService + AccountService *service.AccountService + WithdrawService *service.WithdrawService + DepositService *service.DepositService + PersistentService *service.PersistenceServiceFacade // startTime is the time of start point (which is used in the backtest) startTime time.Time @@ -984,4 +985,4 @@ func (session *ExchangeSession) getSessionSymbols(defaultSymbols ...string) ([]s } return session.FindPossibleSymbols() -} +} \ No newline at end of file diff --git a/pkg/bbgo/persistence.go b/pkg/bbgo/persistence.go index e87fd92a7e..bbc7c75b42 100644 --- a/pkg/bbgo/persistence.go +++ b/pkg/bbgo/persistence.go @@ -102,7 +102,7 @@ func NewPersistenceServiceFacade(conf *PersistenceConfig) (*service.PersistenceS return facade, nil } -func ConfigurePersistence(ctx context.Context, conf *PersistenceConfig) error { +func ConfigurePersistence(ctx context.Context, environ *Environment, conf *PersistenceConfig) error { facade, err := NewPersistenceServiceFacade(conf) if err != nil { return err @@ -112,5 +112,6 @@ func ConfigurePersistence(ctx context.Context, conf *PersistenceConfig) error { isolation.persistenceServiceFacade = facade persistenceServiceFacade = facade + environ.PersistentService = facade return nil -} +} \ No newline at end of file diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index a7945acef7..1f356d23e6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1791,4 +1791,4 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha func roundUpMarketQuantity(market types.Market, v fixedpoint.Value) fixedpoint.Value { return v.Round(market.VolumePrecision, fixedpoint.Up) -} +} \ No newline at end of file From 5cbc6f191fac9950ad8bcad10e4304366deb381b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 13:11:56 +0800 Subject: [PATCH 0483/1392] grid2: aggregate order fee instead of only base fee --- pkg/strategy/grid2/strategy.go | 13 +++++++++---- pkg/strategy/grid2/strategy_test.go | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 1f356d23e6..7d01c0dabb 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -330,21 +330,26 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { return true } -// aggregateOrderBaseFee collects the base fee quantity from the given order +// aggregateOrderFee collects the base fee quantity from the given order // it falls back to query the trades via the RESTful API when the websocket trades are not all received. -func (s *Strategy) aggregateOrderBaseFee(o types.Order) fixedpoint.Value { +func (s *Strategy) aggregateOrderFee(o types.Order) fixedpoint.Value { // try to get the received trades (websocket trades) orderTrades := s.historicalTrades.GetOrderTrades(o) if len(orderTrades) > 0 { s.logger.Infof("found filled order trades: %+v", orderTrades) } + feeCurrency := s.Market.BaseCurrency + if o.Side == types.SideTypeSell { + feeCurrency = s.Market.QuoteCurrency + } + for maxTries := maxNumberOfOrderTradesQueryTries; maxTries > 0; maxTries-- { // if one of the trades is missing, we need to query the trades from the RESTful API if s.verifyOrderTrades(o, orderTrades) { // if trades are verified fees := collectTradeFee(orderTrades) - if fee, ok := fees[s.Market.BaseCurrency]; ok { + if fee, ok := fees[feeCurrency]; ok { return fee } return fixedpoint.Zero @@ -417,7 +422,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { // baseSellQuantityReduction calculation should be only for BUY order // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC // if we don't reduce the sell quantity, than we might fail to place the sell order - baseSellQuantityReduction = s.aggregateOrderBaseFee(o) + baseSellQuantityReduction = s.aggregateOrderFee(o) s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) baseSellQuantityReduction = roundUpMarketQuantity(s.Market, baseSellQuantityReduction) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 15b2a94f19..bdb3ff2225 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -478,7 +478,7 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) { }, }, nil) - baseFee := s.aggregateOrderBaseFee(types.Order{ + baseFee := s.aggregateOrderFee(types.Order{ SubmitOrder: types.SubmitOrder{ Symbol: "BTCUSDT", Side: types.SideTypeBuy, @@ -865,7 +865,7 @@ func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { }, }, nil) - baseFee := s.aggregateOrderBaseFee(types.Order{ + baseFee := s.aggregateOrderFee(types.Order{ SubmitOrder: types.SubmitOrder{ Symbol: "BTCUSDT", Side: types.SideTypeBuy, From bd86a8966763af5fca9fdcf7a2af7fffd40ed41b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 13:13:12 +0800 Subject: [PATCH 0484/1392] grid2: return fee currency --- pkg/strategy/grid2/strategy.go | 12 ++++++------ pkg/strategy/grid2/strategy_test.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7d01c0dabb..2d37496d95 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -332,7 +332,7 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { // aggregateOrderFee collects the base fee quantity from the given order // it falls back to query the trades via the RESTful API when the websocket trades are not all received. -func (s *Strategy) aggregateOrderFee(o types.Order) fixedpoint.Value { +func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { // try to get the received trades (websocket trades) orderTrades := s.historicalTrades.GetOrderTrades(o) if len(orderTrades) > 0 { @@ -350,14 +350,14 @@ func (s *Strategy) aggregateOrderFee(o types.Order) fixedpoint.Value { // if trades are verified fees := collectTradeFee(orderTrades) if fee, ok := fees[feeCurrency]; ok { - return fee + return fee, "" } - return fixedpoint.Zero + return fixedpoint.Zero, feeCurrency } // if we don't support orderQueryService, then we should just skip if s.orderQueryService == nil { - return fixedpoint.Zero + return fixedpoint.Zero, feeCurrency } s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") @@ -375,7 +375,7 @@ func (s *Strategy) aggregateOrderFee(o types.Order) fixedpoint.Value { } } - return fixedpoint.Zero + return fixedpoint.Zero, feeCurrency } func (s *Strategy) processFilledOrder(o types.Order) { @@ -422,7 +422,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { // baseSellQuantityReduction calculation should be only for BUY order // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC // if we don't reduce the sell quantity, than we might fail to place the sell order - baseSellQuantityReduction = s.aggregateOrderFee(o) + baseSellQuantityReduction, _ = s.aggregateOrderFee(o) s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) baseSellQuantityReduction = roundUpMarketQuantity(s.Market, baseSellQuantityReduction) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index bdb3ff2225..4136e4d878 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -478,7 +478,7 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) { }, }, nil) - baseFee := s.aggregateOrderFee(types.Order{ + baseFee, _ := s.aggregateOrderFee(types.Order{ SubmitOrder: types.SubmitOrder{ Symbol: "BTCUSDT", Side: types.SideTypeBuy, @@ -865,7 +865,7 @@ func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { }, }, nil) - baseFee := s.aggregateOrderFee(types.Order{ + baseFee, _ := s.aggregateOrderFee(types.Order{ SubmitOrder: types.SubmitOrder{ Symbol: "BTCUSDT", Side: types.SideTypeBuy, From 9a89237c241ecf43af64894775dbbe8c370e7578 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 13:51:50 +0800 Subject: [PATCH 0485/1392] grid2: fix base/quote fee reduction --- pkg/strategy/grid2/strategy.go | 59 ++++++++++++++++++----------- pkg/strategy/grid2/strategy_test.go | 53 ++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2d37496d95..747398a34c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -321,9 +321,9 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { tq := aggregateTradesQuantity(trades) if tq.Compare(o.Quantity) != 0 { - s.logger.Warnf("order trades missing. expected: %f actual: %f", - o.Quantity.Float64(), - tq.Float64()) + s.logger.Warnf("order trades missing. expected: %s got: %s", + o.Quantity.String(), + tq.String()) return false } @@ -350,7 +350,7 @@ func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { // if trades are verified fees := collectTradeFee(orderTrades) if fee, ok := fees[feeCurrency]; ok { - return fee, "" + return fee, feeCurrency } return fixedpoint.Zero, feeCurrency } @@ -394,7 +394,24 @@ func (s *Strategy) processFilledOrder(o types.Order) { orderExecutedQuoteAmount := o.Quantity.Mul(executedPrice) // collect trades - baseSellQuantityReduction := fixedpoint.Zero + feeQuantityReduction := fixedpoint.Zero + feeCurrency := "" + feePrec := 2 + + // feeQuantityReduction calculation is used to reduce the order quantity + // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC + // if we don't reduce the sell quantity, than we might fail to place the sell order + feeQuantityReduction, feeCurrency = s.aggregateOrderFee(o) + s.logger.Infof("GRID ORDER #%d %s FEE: %s %s", + o.OrderID, o.Side, + feeQuantityReduction.String(), feeCurrency) + + feeQuantityReduction, feePrec = roundUpMarketQuantity(s.Market, feeQuantityReduction, feeCurrency) + s.logger.Infof("GRID ORDER #%d %s FEE (rounding precision %d): %s %s", + o.OrderID, o.Side, + feePrec, + feeQuantityReduction.String(), + feeCurrency) switch o.Side { case types.SideTypeSell: @@ -410,6 +427,11 @@ func (s *Strategy) processFilledOrder(o types.Order) { // use the profit to buy more inventory in the grid if s.Compound || s.EarnBase { + // if it's not using the platform fee currency, reduce the quote quantity for the buy order + if feeCurrency == s.Market.QuoteCurrency { + orderExecutedQuoteAmount = orderExecutedQuoteAmount.Sub(feeQuantityReduction) + } + newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice), s.Market.MinQuantity) } else if s.QuantityOrAmount.Quantity.Sign() > 0 { newQuantity = s.QuantityOrAmount.Quantity @@ -419,19 +441,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { profit = s.calculateProfit(o, newPrice, newQuantity) case types.SideTypeBuy: - // baseSellQuantityReduction calculation should be only for BUY order - // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC - // if we don't reduce the sell quantity, than we might fail to place the sell order - baseSellQuantityReduction, _ = s.aggregateOrderFee(o) - s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) - - baseSellQuantityReduction = roundUpMarketQuantity(s.Market, baseSellQuantityReduction) - s.logger.Infof("GRID BUY ORDER BASE FEE (Rounding with precision %d): %s %s", - s.Market.VolumePrecision, - baseSellQuantityReduction.String(), - s.Market.BaseCurrency) - - newQuantity = newQuantity.Sub(baseSellQuantityReduction) + newQuantity = newQuantity.Sub(feeQuantityReduction) newSide = types.SideTypeSell if !s.ProfitSpread.IsZero() { @@ -443,7 +453,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { } if s.EarnBase { - newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice).Sub(baseSellQuantityReduction), s.Market.MinQuantity) + newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice).Sub(feeQuantityReduction), s.Market.MinQuantity) } } @@ -1794,6 +1804,11 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha return false, nil } -func roundUpMarketQuantity(market types.Market, v fixedpoint.Value) fixedpoint.Value { - return v.Round(market.VolumePrecision, fixedpoint.Up) -} \ No newline at end of file +func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (fixedpoint.Value, int) { + prec := market.VolumePrecision + if c == market.QuoteCurrency { + prec = market.PricePrecision + } + + return v.Round(prec, fixedpoint.Up), prec +} diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 4136e4d878..a1f65d4185 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -646,6 +646,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { defer mockCtrl.Finish() mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl) + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ Symbol: "BTCUSDT", OrderID: "1", @@ -655,14 +656,32 @@ func TestStrategy_handleOrderFilled(t *testing.T) { OrderID: orderID, Exchange: "binance", Price: number(11000.0), - Quantity: gridQuantity, + Quantity: number("0.1"), Symbol: "BTCUSDT", Side: types.SideTypeBuy, IsBuyer: true, FeeCurrency: "BTC", Fee: fixedpoint.Zero, }, - }, nil) + }, nil).Times(1) + + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "2", + }).Return([]types.Trade{ + { + ID: 2, + OrderID: orderID, + Exchange: "binance", + Price: number(12000.0), + Quantity: number(0.09166666666), + Symbol: "BTCUSDT", + Side: types.SideTypeSell, + IsBuyer: true, + FeeCurrency: "BTC", + Fee: fixedpoint.Zero, + }, + }, nil).Times(1) s.orderQueryService = mockService @@ -735,6 +754,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { defer mockCtrl.Finish() mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl) + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ Symbol: "BTCUSDT", OrderID: "1", @@ -749,7 +769,25 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Side: types.SideTypeBuy, IsBuyer: true, FeeCurrency: "BTC", - Fee: fixedpoint.Zero, + Fee: number("0.00001"), + }, + }, nil) + + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "2", + }).Return([]types.Trade{ + { + ID: 2, + OrderID: orderID, + Exchange: "binance", + Price: number(12000.0), + Quantity: gridQuantity, + Symbol: "BTCUSDT", + Side: types.SideTypeSell, + IsBuyer: true, + FeeCurrency: "USDT", + Fee: number("0.01"), }, }, nil) @@ -759,7 +797,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Symbol: "BTCUSDT", Type: types.OrderTypeLimit, Price: number(12_000.0), - Quantity: gridQuantity, + Quantity: number(0.09998999), Side: types.SideTypeSell, TimeInForce: types.TimeInForceGTC, Market: s.Market, @@ -775,7 +813,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Symbol: "BTCUSDT", Type: types.OrderTypeLimit, Price: number(11_000.0), - Quantity: number(0.1090909), + Quantity: number(0.10909), Side: types.SideTypeBuy, TimeInForce: types.TimeInForceGTC, Market: s.Market, @@ -937,8 +975,9 @@ func Test_roundUpMarketQuantity(t *testing.T) { q := number("0.00000003") assert.Equal(t, "0.00000003", q.String()) - q3 := roundUpMarketQuantity(types.Market{ + q3, prec := roundUpMarketQuantity(types.Market{ VolumePrecision: 8, - }, q) + }, q, "BTC") assert.Equal(t, "0.00000003", q3.String(), "rounding prec 8") + assert.Equal(t, 8, prec) } From ca741f91eb62f3306cc392a7fca0dca6d318ee91 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 14:18:37 +0800 Subject: [PATCH 0486/1392] grid2: add fee currency check for buy order --- pkg/strategy/grid2/strategy.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 747398a34c..6fb5303f2c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -386,9 +386,12 @@ func (s *Strategy) processFilledOrder(o types.Order) { newPrice := o.Price newQuantity := o.Quantity executedPrice := o.Price - if o.AveragePrice.Sign() > 0 { - executedPrice = o.AveragePrice - } + + /* + if o.AveragePrice.Sign() > 0 { + executedPrice = o.AveragePrice + } + */ // will be used for calculating quantity orderExecutedQuoteAmount := o.Quantity.Mul(executedPrice) @@ -441,7 +444,9 @@ func (s *Strategy) processFilledOrder(o types.Order) { profit = s.calculateProfit(o, newPrice, newQuantity) case types.SideTypeBuy: - newQuantity = newQuantity.Sub(feeQuantityReduction) + if feeCurrency == s.Market.BaseCurrency { + newQuantity = newQuantity.Sub(feeQuantityReduction) + } newSide = types.SideTypeSell if !s.ProfitSpread.IsZero() { From bf4553d767b3d77debe1311f76db68b7a87c945e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 14:10:34 +0800 Subject: [PATCH 0487/1392] grid2: add OrderFillDelay option --- pkg/strategy/grid2/strategy.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6fb5303f2c..e490980d9a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -143,6 +143,8 @@ type Strategy struct { // StopIfLessThanMinimalQuoteInvestment stops the strategy if the quote investment does not match StopIfLessThanMinimalQuoteInvestment bool `json:"stopIfLessThanMinimalQuoteInvestment"` + OrderFillDelay types.Duration `json:"orderFillDelay"` + // PrometheusLabels will be used as the base prometheus labels PrometheusLabels prometheus.Labels `json:"prometheusLabels"` @@ -785,6 +787,10 @@ func (s *Strategy) newTriggerPriceHandler(ctx context.Context, session *bbgo.Exc func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.ExchangeSession) func(o types.Order) { return func(o types.Order) { + if s.OrderFillDelay > 0 { + time.Sleep(s.OrderFillDelay.Duration()) + } + s.handleOrderFilled(o) // sync the profits to redis From 4deefefe0f2a4f962e09e74edb486ffe40148e64 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Fri, 3 Mar 2023 15:42:29 +0800 Subject: [PATCH 0488/1392] FEATURE: save expiring data to redis --- pkg/service/persistence.go | 8 +++++++- pkg/service/persistence_redis.go | 12 +++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pkg/service/persistence.go b/pkg/service/persistence.go index a4a08356cd..3e82ffde49 100644 --- a/pkg/service/persistence.go +++ b/pkg/service/persistence.go @@ -1,5 +1,7 @@ package service +import "time" + type PersistenceService interface { NewStore(id string, subIDs ...string) Store } @@ -10,6 +12,10 @@ type Store interface { Reset() error } +type Expirable interface { + Expiration() time.Duration +} + type RedisPersistenceConfig struct { Host string `yaml:"host" json:"host" env:"REDIS_HOST"` Port string `yaml:"port" json:"port" env:"REDIS_PORT"` @@ -20,4 +26,4 @@ type RedisPersistenceConfig struct { type JsonPersistenceConfig struct { Directory string `yaml:"directory" json:"directory"` -} +} \ No newline at end of file diff --git a/pkg/service/persistence_redis.go b/pkg/service/persistence_redis.go index b7171e4e47..0c00351921 100644 --- a/pkg/service/persistence_redis.go +++ b/pkg/service/persistence_redis.go @@ -6,6 +6,7 @@ import ( "errors" "net" "strings" + "time" "github.com/go-redis/redis/v8" log "github.com/sirupsen/logrus" @@ -87,15 +88,20 @@ func (store *RedisStore) Save(val interface{}) error { return nil } + var expiration time.Duration + if expiringData, ok := val.(Expirable); ok { + expiration = expiringData.Expiration() + } + data, err := json.Marshal(val) if err != nil { return err } - cmd := store.redis.Set(context.Background(), store.ID, data, 0) + cmd := store.redis.Set(context.Background(), store.ID, data, expiration) _, err = cmd.Result() - redisLogger.Debugf("[redis] set key %q, data = %s", store.ID, string(data)) + redisLogger.Debugf("[redis] set key %q, data = %s, expiration = %s", store.ID, string(data), expiration) return err } @@ -103,4 +109,4 @@ func (store *RedisStore) Save(val interface{}) error { func (store *RedisStore) Reset() error { _, err := store.redis.Del(context.Background(), store.ID).Result() return err -} +} \ No newline at end of file From 0d41f0261ae60f830693cf9fdb088ed3abba2f53 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 18:18:39 +0800 Subject: [PATCH 0489/1392] grid2: rewrite cancel all check loop --- pkg/strategy/grid2/strategy.go | 84 ++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e490980d9a..a56efd3fa6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -857,6 +857,7 @@ func (s *Strategy) OpenGrid(ctx context.Context) error { return s.openGrid(ctx, s.session) } +// TODO: make sure the context here is the trading context or the shutdown context? func (s *Strategy) cancelAll(ctx context.Context) error { var werr error @@ -866,7 +867,6 @@ func (s *Strategy) cancelAll(ctx context.Context) error { } service, support := session.Exchange.(advancedOrderCancelApi) - if s.UseCancelAllOrdersApiWhenClose && !support { s.logger.Warnf("advancedOrderCancelApi interface is not implemented, fallback to default graceful cancel, exchange %T", session) s.UseCancelAllOrdersApiWhenClose = false @@ -875,48 +875,48 @@ func (s *Strategy) cancelAll(ctx context.Context) error { if s.UseCancelAllOrdersApiWhenClose { s.logger.Infof("useCancelAllOrdersApiWhenClose is set, using advanced order cancel api for canceling...") - if s.OrderGroupID > 0 { - s.logger.Infof("found OrderGroupID (%d), using group ID for canceling orders...", s.OrderGroupID) + for { + s.logger.Infof("checking %s open orders...", s.Symbol) - op := func() error { - _, cancelErr := service.CancelOrdersByGroupID(ctx, s.OrderGroupID) - return cancelErr - } - err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { - werr = multierr.Append(werr, err) - } - } else { - s.logger.Infof("canceling all orders...") - op := func() error { - _, cancelErr := service.CancelAllOrders(ctx) - return cancelErr + return err } - err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) - if err != nil { - werr = multierr.Append(werr, err) + + if len(openOrders) == 0 { + break } - } - time.Sleep(5 * time.Second) + s.logger.Infof("found %d open orders left, using cancel all orders api", len(openOrders)) - s.logger.Infof("checking %s open orders...", s.Symbol) + if s.OrderGroupID > 0 { + s.logger.Infof("found OrderGroupID (%d), using group ID for canceling grid orders...", s.OrderGroupID) - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) - if err != nil { - return err - } + op := func() error { + _, cancelErr := service.CancelOrdersByGroupID(ctx, s.OrderGroupID) + return cancelErr + } + err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) + if err != nil { + s.logger.WithError(err).Errorf("CancelOrdersByGroupID api call error") + werr = multierr.Append(werr, err) + } - if len(openOrders) > 0 { - s.logger.Infof("found %d open orders left, using cancel all orders api", len(openOrders)) + } else { + s.logger.Infof("using cancal all orders api for canceling grid orders...") + op := func() error { + _, cancelErr := service.CancelAllOrders(ctx) + return cancelErr + } - op := func() error { - _, cancelErr := service.CancelOrdersBySymbol(ctx, s.Symbol) - return cancelErr - } - if err2 := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)); err2 != nil { - werr = multierr.Append(werr, err2) + err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) + if err != nil { + s.logger.WithError(err).Errorf("CancelAllOrders api call error") + werr = multierr.Append(werr, err) + } } + + time.Sleep(1 * time.Second) } } else { if err := s.orderExecutor.GracefulCancel(ctx); err != nil { @@ -1743,22 +1743,26 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. session.UserDataStream.OnStart(func() { s.logger.Infof("user data stream started, initializing grid...") - // avoid blocking the user data stream - // callbacks are blocking operation - go func() { + if s.RecoverOrdersWhenStart { + // avoid blocking the user data stream + // callbacks are blocking operation // do recover only when triggerPrice is not set. - if s.RecoverOrdersWhenStart { + go func() { s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") if err := s.recoverGrid(ctx, session); err != nil { log.WithError(err).Errorf("recover error") } - } - + if err := s.openGrid(ctx, session); err != nil { + s.logger.WithError(err).Errorf("failed to setup grid orders") + } + }() + } else { + // avoid using goroutine here for back-test if err := s.openGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("failed to setup grid orders") } - }() + } }) } From fa395b0d0a4b7bc6a8a5b58aaac6e987ac263f1a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 18:22:31 +0800 Subject: [PATCH 0490/1392] grid2: improve the onStart handler --- pkg/strategy/grid2/strategy.go | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index a56efd3fa6..4341fe0e3b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1740,29 +1740,26 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. // if TriggerPrice is zero, that means we need to open the grid when start up if s.TriggerPrice.IsZero() { + // must call the openGrid method inside the OnStart callback because + // it needs to receive the trades from the user data stream + // + // should try to avoid blocking the user data stream + // callbacks are blocking operation session.UserDataStream.OnStart(func() { s.logger.Infof("user data stream started, initializing grid...") - if s.RecoverOrdersWhenStart { - // avoid blocking the user data stream - // callbacks are blocking operation - // do recover only when triggerPrice is not set. - go func() { - s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") - if err := s.recoverGrid(ctx, session); err != nil { - log.WithError(err).Errorf("recover error") - } - - if err := s.openGrid(ctx, session); err != nil { - s.logger.WithError(err).Errorf("failed to setup grid orders") - } - }() - } else { - // avoid using goroutine here for back-test - if err := s.openGrid(ctx, session); err != nil { - s.logger.WithError(err).Errorf("failed to setup grid orders") + if s.RecoverOrdersWhenStart && !bbgo.IsBackTesting { + // do recover only when triggerPrice is not set and not in the back-test mode + s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") + if err := s.recoverGrid(ctx, session); err != nil { + log.WithError(err).Errorf("recover error") } } + + // avoid using goroutine here for back-test + if err := s.openGrid(ctx, session); err != nil { + s.logger.WithError(err).Errorf("failed to setup grid orders") + } }) } From 3f560b22309aae7e48ff97805ea6b214e4ee18e9 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 18:55:15 +0800 Subject: [PATCH 0491/1392] grid2: backoff retry open orders api --- pkg/strategy/grid2/strategy.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 4341fe0e3b..ca685f5b70 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -878,9 +878,14 @@ func (s *Strategy) cancelAll(ctx context.Context) error { for { s.logger.Infof("checking %s open orders...", s.Symbol) - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) - if err != nil { + var openOrders []types.Order + if err := backoff.Retry(func() error { + var err error + openOrders, err = session.Exchange.QueryOpenOrders(ctx, s.Symbol) return err + }, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)); err != nil { + s.logger.WithError(err).Errorf("CancelOrdersByGroupID api call error") + werr = multierr.Append(werr, err) } if len(openOrders) == 0 { From 1a109c118d920bb14ae9e1bb4737c64ae3230d26 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 18:42:08 +0800 Subject: [PATCH 0492/1392] grid2: use write context for submitting orders --- pkg/strategy/grid2/strategy.go | 38 ++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index ca685f5b70..58ba1ef746 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -181,6 +181,9 @@ type Strategy struct { // mu is used for locking the grid object field, avoid double grid opening mu sync.Mutex + + tradingCtx, writeCtx context.Context + cancelWrite context.CancelFunc } func (s *Strategy) ID() string { @@ -477,7 +480,8 @@ func (s *Strategy) processFilledOrder(o types.Order) { s.logger.Infof("SUBMIT GRID REVERSE ORDER: %s", orderForm.String()) - createdOrders, err := s.orderExecutor.SubmitOrders(context.Background(), orderForm) + writeCtx := s.getWriteContext() + createdOrders, err := s.orderExecutor.SubmitOrders(writeCtx, orderForm) if err != nil { s.logger.WithError(err).Errorf("GRID REVERSE ORDER SUBMISSION ERROR: order: %s", orderForm.String()) return @@ -1047,9 +1051,11 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) s.debugGridOrders(submitOrders, lastPrice) + writeCtx := s.getWriteContext(ctx) + var createdOrders []types.Order for _, submitOrder := range submitOrders { - ret, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrder) + ret, err2 := s.orderExecutor.SubmitOrders(writeCtx, submitOrder) if err2 != nil { return err2 } @@ -1604,9 +1610,33 @@ func (s *Strategy) CleanUp(ctx context.Context) error { return s.cancelAll(ctx) } +func (s *Strategy) getWriteContext(fallbackCtxList ...context.Context) context.Context { + if s.writeCtx != nil { + return s.writeCtx + } + + // fallback to context background + for _, c := range fallbackCtxList { + if c != nil { + return c + } + } + + if s.tradingCtx != nil { + return s.tradingCtx + } + + // final fallback to context background + return context.Background() +} + func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() + // allocate a context for write operation (submitting orders) + s.tradingCtx = ctx + s.writeCtx, s.cancelWrite = context.WithCancel(ctx) + s.session = session if service, ok := session.Exchange.(types.ExchangeOrderQueryService); ok { @@ -1704,6 +1734,10 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. return } + if s.cancelWrite != nil { + s.cancelWrite() + } + if err := s.CloseGrid(ctx); err != nil { s.logger.WithError(err).Errorf("grid graceful order cancel error") } From e2435f1fc070159a93540379323520fcfad6957e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 18:43:47 +0800 Subject: [PATCH 0493/1392] grid2: pass submit orders in one call since we have solved the order store issue --- pkg/strategy/grid2/strategy.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 58ba1ef746..a00d4a1e72 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1053,13 +1053,9 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) writeCtx := s.getWriteContext(ctx) - var createdOrders []types.Order - for _, submitOrder := range submitOrders { - ret, err2 := s.orderExecutor.SubmitOrders(writeCtx, submitOrder) - if err2 != nil { - return err2 - } - createdOrders = append(createdOrders, ret...) + createdOrders, err2 := s.orderExecutor.SubmitOrders(writeCtx, submitOrders...) + if err2 != nil { + return err2 } // try to always emit grid ready From 9d1da7c84776acf04e3b60eac80b8e96bafc5571 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 19:21:23 +0800 Subject: [PATCH 0494/1392] grid2: remove outdated comment --- pkg/strategy/grid2/strategy.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index a00d4a1e72..f18b5121ea 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1611,7 +1611,6 @@ func (s *Strategy) getWriteContext(fallbackCtxList ...context.Context) context.C return s.writeCtx } - // fallback to context background for _, c := range fallbackCtxList { if c != nil { return c From 94b946a9935c798ecd05b597a8dcc499f6e1c724 Mon Sep 17 00:00:00 2001 From: narumi Date: Fri, 3 Mar 2023 23:14:30 +0800 Subject: [PATCH 0495/1392] add orderType parameter --- config/marketcap.yaml | 3 ++- pkg/strategy/marketcap/strategy.go | 11 ++++++++--- pkg/strategy/rebalance/strategy.go | 4 ++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/config/marketcap.yaml b/config/marketcap.yaml index d1f8b41bf2..7f4caa615e 100644 --- a/config/marketcap.yaml +++ b/config/marketcap.yaml @@ -22,5 +22,6 @@ exchangeStrategies: threshold: 1% # max amount to buy or sell per order maxAmount: 1_000 - dryRun: true queryInterval: 1h + orderType: LIMIT_MAKER # LIMIT_MAKER, LIMIT, MARKET + dryRun: true diff --git a/pkg/strategy/marketcap/strategy.go b/pkg/strategy/marketcap/strategy.go index 83800f573b..14f6b7d513 100644 --- a/pkg/strategy/marketcap/strategy.go +++ b/pkg/strategy/marketcap/strategy.go @@ -31,11 +31,12 @@ type Strategy struct { QuoteCurrencyWeight fixedpoint.Value `json:"quoteCurrencyWeight"` BaseCurrencies []string `json:"baseCurrencies"` Threshold fixedpoint.Value `json:"threshold"` - DryRun bool `json:"dryRun"` // max amount to buy or sell per order MaxAmount fixedpoint.Value `json:"maxAmount"` // interval to query marketcap data from coinmarketcap - QueryInterval types.Interval `json:"queryInterval"` + QueryInterval types.Interval `json:"queryInterval"` + OrderType types.OrderType `json:"orderType"` + DryRun bool `json:"dryRun"` subscribeSymbol string activeOrderBook *bbgo.ActiveOrderBook @@ -77,6 +78,10 @@ func (s *Strategy) Validate() error { return fmt.Errorf("maxAmount shoud not less than 0") } + if s.OrderType == "" { + s.OrderType = types.OrderTypeLimitMaker + } + return nil } @@ -178,7 +183,7 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context, session *bbgo.Excha order := types.SubmitOrder{ Symbol: symbol, Side: side, - Type: types.OrderTypeLimit, + Type: s.OrderType, Quantity: quantity, Price: currentPrice, } diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index 41eef02dcd..d4ba35ace3 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -62,6 +62,10 @@ func (s *Strategy) Validate() error { return fmt.Errorf("maxAmount shoud not less than 0") } + if s.OrderType == "" { + s.OrderType = types.OrderTypeLimitMaker + } + return nil } From ec0d438f9dac2ffeab92eb9a477fe47e7ce340ad Mon Sep 17 00:00:00 2001 From: gx578007 Date: Sun, 5 Mar 2023 14:29:31 +0800 Subject: [PATCH 0496/1392] FIX: [grid2] fix active orderbook at recovering --- pkg/strategy/grid2/strategy.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f18b5121ea..515b743747 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1341,8 +1341,11 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService missingPrices := scanMissingPinPrices(orderBook, grid.Pins) if numMissing := len(missingPrices); numMissing <= 1 { s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") + s.addOrdersToActiveOrderBook(gridOrders) s.setGrid(grid) s.EmitGridReady() + s.updateGridNumOfOrdersMetrics() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } else { s.logger.Infof("GRID RECOVER: found missing prices: %v", missingPrices) @@ -1384,8 +1387,11 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService // if all orders on the order book are active orders, we don't need to recover. if isCompleteGridOrderBook(orderBook, s.GridNum) { s.logger.Infof("GRID RECOVER: all orders are active orders, do not need recover") + s.addOrdersToActiveOrderBook(gridOrders) s.setGrid(grid) s.EmitGridReady() + s.updateGridNumOfOrdersMetrics() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } @@ -1407,11 +1413,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService // before we re-play the orders, // we need to add these open orders to the active order book - activeOrderBook := s.orderExecutor.ActiveMakerOrders() - for _, gridOrder := range gridOrders { - // put the order back to the active order book so that we can receive order update - activeOrderBook.Add(gridOrder) - } + s.addOrdersToActiveOrderBook(gridOrders) s.setGrid(grid) s.EmitGridReady() @@ -1435,6 +1437,14 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService return nil } +func (s *Strategy) addOrdersToActiveOrderBook(gridOrders []types.Order) { + activeOrderBook := s.orderExecutor.ActiveMakerOrders() + for _, gridOrder := range gridOrders { + // put the order back to the active order book so that we can receive order update + activeOrderBook.Add(gridOrder) + } +} + func (s *Strategy) setGrid(grid *Grid) { s.mu.Lock() s.grid = grid @@ -1857,4 +1867,4 @@ func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (f } return v.Round(prec, fixedpoint.Up), prec -} +} \ No newline at end of file From 0f307bba7d1381bd8b10b55f2881004de46b5552 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 5 Mar 2023 17:07:01 +0800 Subject: [PATCH 0497/1392] grid2: pull out start process to a function --- pkg/strategy/grid2/strategy.go | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 515b743747..e48d2f873c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1792,17 +1792,10 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. session.UserDataStream.OnStart(func() { s.logger.Infof("user data stream started, initializing grid...") - if s.RecoverOrdersWhenStart && !bbgo.IsBackTesting { - // do recover only when triggerPrice is not set and not in the back-test mode - s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") - if err := s.recoverGrid(ctx, session); err != nil { - log.WithError(err).Errorf("recover error") - } - } - - // avoid using goroutine here for back-test - if err := s.openGrid(ctx, session); err != nil { - s.logger.WithError(err).Errorf("failed to setup grid orders") + if !bbgo.IsBackTesting { + go s.startProcess(ctx, session) + } else { + s.startProcess(ctx, session) } }) } @@ -1810,6 +1803,21 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. return nil } +func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSession) { + if s.RecoverOrdersWhenStart { + // do recover only when triggerPrice is not set and not in the back-test mode + s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") + if err := s.recoverGrid(ctx, session); err != nil { + s.logger.WithError(err).Errorf("recover error") + } + } + + // avoid using goroutine here for back-test + if err := s.openGrid(ctx, session); err != nil { + s.logger.WithError(err).Errorf("failed to setup grid orders") + } +} + func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { @@ -1867,4 +1875,4 @@ func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (f } return v.Round(prec, fixedpoint.Up), prec -} \ No newline at end of file +} From 5805f0c7f05c82875f41654d04fa0a9e74896631 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 5 Mar 2023 17:10:11 +0800 Subject: [PATCH 0498/1392] grid2: call cancelWrite before everything --- pkg/strategy/grid2/strategy.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e48d2f873c..5eb2c28a42 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1734,15 +1734,15 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() + if s.cancelWrite != nil { + s.cancelWrite() + } + if s.KeepOrdersWhenShutdown { s.logger.Infof("keepOrdersWhenShutdown is set, will keep the orders on the exchange") return } - if s.cancelWrite != nil { - s.cancelWrite() - } - if err := s.CloseGrid(ctx); err != nil { s.logger.WithError(err).Errorf("grid graceful order cancel error") } From a01888dcddc0ab9a4583974f4cda4d88885d0376 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 5 Mar 2023 17:21:29 +0800 Subject: [PATCH 0499/1392] bbgo: fix logger usage in BatchRetryPlaceOrder --- pkg/bbgo/order_execution.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index d5f8fc2a06..02c172c9ec 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -377,7 +377,7 @@ batchRetryOrder: // can allocate permanent error backoff.Permanent(err) to stop backoff createdOrder, err2 := exchange.SubmitOrder(timeoutCtx, submitOrder) if err2 != nil { - log.WithError(err2).Errorf("submit order error") + logger.WithError(err2).Errorf("submit order error") } if err2 == nil && createdOrder != nil { From 07f2de43004f3cfd39aa84779422851891ae2aae Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 5 Mar 2023 17:23:06 +0800 Subject: [PATCH 0500/1392] bbgo: print submit order in the message --- pkg/bbgo/order_execution.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 02c172c9ec..761dc74e17 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -377,7 +377,7 @@ batchRetryOrder: // can allocate permanent error backoff.Permanent(err) to stop backoff createdOrder, err2 := exchange.SubmitOrder(timeoutCtx, submitOrder) if err2 != nil { - logger.WithError(err2).Errorf("submit order error") + logger.WithError(err2).Errorf("submit order error: %s", submitOrder.String()) } if err2 == nil && createdOrder != nil { From 4927dd7f98717ec2913dc4bdf35cb250c41c7941 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 5 Mar 2023 17:34:50 +0800 Subject: [PATCH 0501/1392] grid2: add more logs --- pkg/strategy/grid2/strategy.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5eb2c28a42..2ad136f4f8 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1320,9 +1320,11 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService // for MAX exchange we need the order ID to query the closed order history if s.GridProfitStats != nil && s.GridProfitStats.InitialOrderID > 0 { lastOrderID = s.GridProfitStats.InitialOrderID + s.logger.Infof("found initial order id #%d from grid stats", lastOrderID) } else { if oid, ok := findEarliestOrderID(openOrders); ok { lastOrderID = oid + s.logger.Infof("found earliest order id #%d from open orders", lastOrderID) } } @@ -1826,6 +1828,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio // do recover only when openOrders > 0 if len(openOrders) == 0 { + s.logger.Warn("0 open orders, skip recovery process") return nil } @@ -1838,7 +1841,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio } if err := s.recoverGridWithOpenOrders(ctx, historyService, openOrders); err != nil { - return errors.Wrap(err, "recover grid error") + return errors.Wrap(err, "grid recover error") } return nil From 584fae1a534842f5b6564220c45585c962dc9f51 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 5 Mar 2023 17:41:05 +0800 Subject: [PATCH 0502/1392] grid2: fix recover order filtering --- pkg/strategy/grid2/strategy.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2ad136f4f8..8230a6e0be 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1384,6 +1384,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService debugGrid(s.logger, grid, orderBook) + // note that the tmpOrders contains FILLED and NEW orders tmpOrders := orderBook.Orders() // if all orders on the order book are active orders, we don't need to recover. @@ -1400,14 +1401,15 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService // for reverse order recovering, we need the orders to be sort by update time ascending-ly types.SortOrdersUpdateTimeAscending(tmpOrders) - if len(tmpOrders) > 1 && len(tmpOrders) == int(s.GridNum) { - // remove the latest updated order because it's near the empty slot - tmpOrders = tmpOrders[1:] - } - // we will only submit reverse orders for filled orders filledOrders := types.OrdersFilled(tmpOrders) + // if the number of FILLED orders and NEW orders equals to GridNum, then we need to remove an extra filled order for the replay events + if len(tmpOrders) == int(s.GridNum) && len(filledOrders) > 1 { + // remove the latest updated order because it's near the empty slot + filledOrders = filledOrders[1:] + } + s.logger.Infof("GRID RECOVER: found %d filled grid orders, will re-replay the order event in the following order:", len(filledOrders)) for i, o := range filledOrders { s.logger.Infof("%d) %s", i+1, o.String()) From dfba758e88a5638b8847d0c267e344739ad5b780 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 5 Mar 2023 17:55:04 +0800 Subject: [PATCH 0503/1392] grid2: add one more log --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 8230a6e0be..136ae53e3b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1410,7 +1410,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService filledOrders = filledOrders[1:] } - s.logger.Infof("GRID RECOVER: found %d filled grid orders, will re-replay the order event in the following order:", len(filledOrders)) + s.logger.Infof("GRID RECOVER: found %d/%d filled grid orders, gridNumber=%d, will re-replay the order event in the following order:", len(filledOrders), len(tmpOrders), int(s.GridNum)) for i, o := range filledOrders { s.logger.Infof("%d) %s", i+1, o.String()) } From a5e35b47119feb5331aa8e19aedcf7caf66ed161 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Sun, 5 Mar 2023 22:20:14 +0800 Subject: [PATCH 0504/1392] FIX: add mutex in memory store --- pkg/service/memory.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/service/memory.go b/pkg/service/memory.go index 92ee9f6cdc..978b3d2caa 100644 --- a/pkg/service/memory.go +++ b/pkg/service/memory.go @@ -3,6 +3,7 @@ package service import ( "reflect" "strings" + "sync" ) type MemoryService struct { @@ -26,14 +27,21 @@ func (s *MemoryService) NewStore(id string, subIDs ...string) Store { type MemoryStore struct { Key string memory *MemoryService + mu sync.Mutex } func (store *MemoryStore) Save(val interface{}) error { + store.mu.Lock() + defer store.mu.Unlock() + store.memory.Slots[store.Key] = val return nil } func (store *MemoryStore) Load(val interface{}) error { + store.mu.Lock() + defer store.mu.Unlock() + v := reflect.ValueOf(val) if data, ok := store.memory.Slots[store.Key]; ok { dataRV := reflect.ValueOf(data) @@ -46,6 +54,9 @@ func (store *MemoryStore) Load(val interface{}) error { } func (store *MemoryStore) Reset() error { + store.mu.Lock() + defer store.mu.Unlock() + delete(store.memory.Slots, store.Key) return nil -} +} \ No newline at end of file From 773b0557112194454cf7359c9078a38253ec4ab6 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 5 Mar 2023 23:20:17 +0800 Subject: [PATCH 0505/1392] grid2: fix length check --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 136ae53e3b..731a63c5d5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1405,7 +1405,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService filledOrders := types.OrdersFilled(tmpOrders) // if the number of FILLED orders and NEW orders equals to GridNum, then we need to remove an extra filled order for the replay events - if len(tmpOrders) == int(s.GridNum) && len(filledOrders) > 1 { + if len(tmpOrders) == int(s.GridNum) && len(filledOrders) > 0 { // remove the latest updated order because it's near the empty slot filledOrders = filledOrders[1:] } From 9f29fbd6453fe4b3fcd014f3be82e9b18b2a5ed7 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 5 Mar 2023 23:21:28 +0800 Subject: [PATCH 0506/1392] grid2: add order group id to the submitOrder forms --- pkg/strategy/grid2/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 731a63c5d5..7946c423ba 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -476,6 +476,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { TimeInForce: types.TimeInForceGTC, Quantity: newQuantity, Tag: orderTag, + GroupID: s.OrderGroupID, } s.logger.Infof("SUBMIT GRID REVERSE ORDER: %s", orderForm.String()) @@ -1176,6 +1177,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin Market: s.Market, TimeInForce: types.TimeInForceGTC, Tag: orderTag, + GroupID: s.OrderGroupID, }) usedBase = usedBase.Add(quantity) } else { @@ -1192,6 +1194,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin Market: s.Market, TimeInForce: types.TimeInForceGTC, Tag: orderTag, + GroupID: s.OrderGroupID, }) quoteQuantity := quantity.Mul(nextPrice) usedQuote = usedQuote.Add(quoteQuantity) @@ -1224,6 +1227,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin Market: s.Market, TimeInForce: types.TimeInForceGTC, Tag: orderTag, + GroupID: s.OrderGroupID, }) usedQuote = usedQuote.Add(quoteQuantity) } From 1dd6f9ef3e7d650b54946a81145d96c162756f5d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 6 Mar 2023 10:37:34 +0800 Subject: [PATCH 0507/1392] grid2: remove order group cancel --- pkg/strategy/grid2/strategy.go | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7946c423ba..6b0000e964 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -899,31 +899,16 @@ func (s *Strategy) cancelAll(ctx context.Context) error { s.logger.Infof("found %d open orders left, using cancel all orders api", len(openOrders)) - if s.OrderGroupID > 0 { - s.logger.Infof("found OrderGroupID (%d), using group ID for canceling grid orders...", s.OrderGroupID) - - op := func() error { - _, cancelErr := service.CancelOrdersByGroupID(ctx, s.OrderGroupID) - return cancelErr - } - err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) - if err != nil { - s.logger.WithError(err).Errorf("CancelOrdersByGroupID api call error") - werr = multierr.Append(werr, err) - } - - } else { - s.logger.Infof("using cancal all orders api for canceling grid orders...") - op := func() error { - _, cancelErr := service.CancelAllOrders(ctx) - return cancelErr - } + s.logger.Infof("using cancal all orders api for canceling grid orders...") + op := func() error { + _, cancelErr := service.CancelAllOrders(ctx) + return cancelErr + } - err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) - if err != nil { - s.logger.WithError(err).Errorf("CancelAllOrders api call error") - werr = multierr.Append(werr, err) - } + err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) + if err != nil { + s.logger.WithError(err).Errorf("CancelAllOrders api call error") + werr = multierr.Append(werr, err) } time.Sleep(1 * time.Second) From d466a63d22186e39071ef03d1580deaf6388e3bb Mon Sep 17 00:00:00 2001 From: chiahung Date: Mon, 6 Mar 2023 15:58:18 +0800 Subject: [PATCH 0508/1392] FIX: add group id on submit order API --- pkg/exchange/max/exchange.go | 4 +++- pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go | 2 +- .../maxapi/v3/cancel_wallet_order_all_request_requestgen.go | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 0e4800ee9a..01f6081777 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -480,7 +480,9 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (cr Side(toLocalSideType(o.Side)). Volume(quantityString). OrderType(orderType). - ClientOrderID(clientOrderID) + ClientOrderID(clientOrderID). + // TODO: make sure MAX API support uint32 group id + GroupID(fmt.Sprintf("%d", o.GroupID)) switch o.Type { case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker: diff --git a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go index dffd263579..f8608e79bc 100644 --- a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go +++ b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go @@ -22,5 +22,5 @@ type CancelWalletOrderAllRequest struct { walletType WalletType `param:"walletType,slug,required"` side *string `param:"side"` market *string `param:"market"` - groupID *uint32 `param:"groupID"` + groupID *uint32 `param:"group_id"` } diff --git a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go index 7184372a6d..29b0db9311 100644 --- a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go @@ -64,12 +64,12 @@ func (c *CancelWalletOrderAllRequest) GetParameters() (map[string]interface{}, e params["market"] = market } else { } - // check groupID field -> json key groupID + // check groupID field -> json key group_id if c.groupID != nil { groupID := *c.groupID // assign parameter of groupID - params["groupID"] = groupID + params["group_id"] = groupID } else { } From 83d9977a574ca5f3600d08ad4dc685344d1f4d8e Mon Sep 17 00:00:00 2001 From: chiahung Date: Mon, 6 Mar 2023 16:32:36 +0800 Subject: [PATCH 0509/1392] make sure group id is > 0 --- pkg/exchange/max/exchange.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 01f6081777..abdcab6086 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -480,9 +480,12 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (cr Side(toLocalSideType(o.Side)). Volume(quantityString). OrderType(orderType). - ClientOrderID(clientOrderID). - // TODO: make sure MAX API support uint32 group id - GroupID(fmt.Sprintf("%d", o.GroupID)) + ClientOrderID(clientOrderID) + + if o.GroupID > 0 { + // TODO: MAX API only support 0 ~ 2^31-1 (2147483647) + req.GroupID(strconv.FormatUint(uint64(o.GroupID), 10)) + } switch o.Type { case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker: From d4912ed3cd5fe5abe3688fe786bd684f96cbd124 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Mon, 6 Mar 2023 16:56:40 +0800 Subject: [PATCH 0510/1392] FIX: [grid2] avoid initializing metrics twice --- pkg/strategy/grid2/metrics.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/metrics.go b/pkg/strategy/grid2/metrics.go index a73a795f7b..17938f9643 100644 --- a/pkg/strategy/grid2/metrics.go +++ b/pkg/strategy/grid2/metrics.go @@ -1,6 +1,8 @@ package grid2 -import "github.com/prometheus/client_golang/prometheus" +import ( + "github.com/prometheus/client_golang/prometheus" +) var ( metricsGridNum *prometheus.GaugeVec @@ -32,6 +34,10 @@ func mergeLabels(a, b prometheus.Labels) prometheus.Labels { } func initMetrics(extendedLabels []string) { + if metricsGridNum != nil { + return + } + metricsGridNum = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "bbgo_grid2_num", @@ -110,4 +116,4 @@ func registerMetrics() { metricsGridOrderPrices, ) metricsRegistered = true -} +} \ No newline at end of file From cd500e6e7391d230f6de61d1823f0b4df13e3a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= <4680567+narumiruna@users.noreply.github.com> Date: Mon, 6 Mar 2023 12:33:14 +0000 Subject: [PATCH 0511/1392] set order type default value in Defaults method --- pkg/strategy/marketcap/strategy.go | 11 ++++++----- pkg/strategy/rebalance/strategy.go | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/marketcap/strategy.go b/pkg/strategy/marketcap/strategy.go index 14f6b7d513..81e2a3b8c2 100644 --- a/pkg/strategy/marketcap/strategy.go +++ b/pkg/strategy/marketcap/strategy.go @@ -43,6 +43,12 @@ type Strategy struct { targetWeights types.ValueMap } +func (s *Strategy) Defaults() error { + if s.OrderType == "" { + s.OrderType = types.OrderTypeLimitMaker + } +} + func (s *Strategy) Initialize() error { apiKey := os.Getenv("COINMARKETCAP_API_KEY") s.datasource = coinmarketcap.New(apiKey) @@ -77,11 +83,6 @@ func (s *Strategy) Validate() error { if s.MaxAmount.Sign() < 0 { return fmt.Errorf("maxAmount shoud not less than 0") } - - if s.OrderType == "" { - s.OrderType = types.OrderTypeLimitMaker - } - return nil } diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index d4ba35ace3..7b6a9d6bbe 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -31,6 +31,12 @@ type Strategy struct { activeOrderBook *bbgo.ActiveOrderBook } +func (s *Strategy) Defaults() error { + if s.OrderType == "" { + s.OrderType = types.OrderTypeLimitMaker + } +} + func (s *Strategy) Initialize() error { return nil } @@ -61,11 +67,6 @@ func (s *Strategy) Validate() error { if s.MaxAmount.Sign() < 0 { return fmt.Errorf("maxAmount shoud not less than 0") } - - if s.OrderType == "" { - s.OrderType = types.OrderTypeLimitMaker - } - return nil } From 00e022dbdc6c08abd54614b28e6153f775b94c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= <4680567+narumiruna@users.noreply.github.com> Date: Mon, 6 Mar 2023 13:37:03 +0000 Subject: [PATCH 0512/1392] fixup! set order type default value in Defaults method --- pkg/strategy/marketcap/strategy.go | 1 + pkg/strategy/rebalance/strategy.go | 1 + 2 files changed, 2 insertions(+) diff --git a/pkg/strategy/marketcap/strategy.go b/pkg/strategy/marketcap/strategy.go index 81e2a3b8c2..05aa8d020e 100644 --- a/pkg/strategy/marketcap/strategy.go +++ b/pkg/strategy/marketcap/strategy.go @@ -47,6 +47,7 @@ func (s *Strategy) Defaults() error { if s.OrderType == "" { s.OrderType = types.OrderTypeLimitMaker } + return nil } func (s *Strategy) Initialize() error { diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index 7b6a9d6bbe..681c6022ce 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -35,6 +35,7 @@ func (s *Strategy) Defaults() error { if s.OrderType == "" { s.OrderType = types.OrderTypeLimitMaker } + return nil } func (s *Strategy) Initialize() error { From f8054459c4be27b6259c58cf6779b6f2b110f6f8 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Tue, 7 Mar 2023 11:54:45 +0800 Subject: [PATCH 0513/1392] FIX: [grid2] group id should be bound by MaxInt32 --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6b0000e964..3ec98b6721 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1642,7 +1642,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } if s.OrderGroupID == 0 { - s.OrderGroupID = util.FNV32(instanceID) + s.OrderGroupID = util.FNV32(instanceID) % math.MaxInt32 } if s.AutoRange != nil { @@ -1869,4 +1869,4 @@ func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (f } return v.Round(prec, fixedpoint.Up), prec -} +} \ No newline at end of file From 62eed9605d4d1d892a4ca291290ff92edcbc4b54 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 7 Mar 2023 13:36:33 +0800 Subject: [PATCH 0514/1392] grid2: round down quoteQuantity/baseQuantity after the fee reduction --- pkg/strategy/grid2/strategy.go | 45 +++++++++++++++-------------- pkg/strategy/grid2/strategy_test.go | 4 +-- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3ec98b6721..fe70e6d2c1 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -401,25 +401,14 @@ func (s *Strategy) processFilledOrder(o types.Order) { // will be used for calculating quantity orderExecutedQuoteAmount := o.Quantity.Mul(executedPrice) - // collect trades - feeQuantityReduction := fixedpoint.Zero - feeCurrency := "" - feePrec := 2 - - // feeQuantityReduction calculation is used to reduce the order quantity + // collect trades for fee + // fee calculation is used to reduce the order quantity // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC // if we don't reduce the sell quantity, than we might fail to place the sell order - feeQuantityReduction, feeCurrency = s.aggregateOrderFee(o) + fee, feeCurrency := s.aggregateOrderFee(o) s.logger.Infof("GRID ORDER #%d %s FEE: %s %s", o.OrderID, o.Side, - feeQuantityReduction.String(), feeCurrency) - - feeQuantityReduction, feePrec = roundUpMarketQuantity(s.Market, feeQuantityReduction, feeCurrency) - s.logger.Infof("GRID ORDER #%d %s FEE (rounding precision %d): %s %s", - o.OrderID, o.Side, - feePrec, - feeQuantityReduction.String(), - feeCurrency) + fee.String(), feeCurrency) switch o.Side { case types.SideTypeSell: @@ -437,9 +426,15 @@ func (s *Strategy) processFilledOrder(o types.Order) { if s.Compound || s.EarnBase { // if it's not using the platform fee currency, reduce the quote quantity for the buy order if feeCurrency == s.Market.QuoteCurrency { - orderExecutedQuoteAmount = orderExecutedQuoteAmount.Sub(feeQuantityReduction) + orderExecutedQuoteAmount = orderExecutedQuoteAmount.Sub(fee) } + // for quote amount, always round down with price precision to prevent the quote currency fund locking rounding issue + origQuoteAmount := orderExecutedQuoteAmount + orderExecutedQuoteAmount = orderExecutedQuoteAmount.Round(s.Market.PricePrecision, fixedpoint.Down) + + s.logger.Infof("round down buy order quote quantity %s to %s by quote quantity precision %d", origQuoteAmount.String(), orderExecutedQuoteAmount.String(), s.Market.PricePrecision) + newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice), s.Market.MinQuantity) } else if s.QuantityOrAmount.Quantity.Sign() > 0 { newQuantity = s.QuantityOrAmount.Quantity @@ -449,10 +444,6 @@ func (s *Strategy) processFilledOrder(o types.Order) { profit = s.calculateProfit(o, newPrice, newQuantity) case types.SideTypeBuy: - if feeCurrency == s.Market.BaseCurrency { - newQuantity = newQuantity.Sub(feeQuantityReduction) - } - newSide = types.SideTypeSell if !s.ProfitSpread.IsZero() { newPrice = newPrice.Add(s.ProfitSpread) @@ -462,9 +453,19 @@ func (s *Strategy) processFilledOrder(o types.Order) { } } + if feeCurrency == s.Market.BaseCurrency { + newQuantity = newQuantity.Sub(fee) + } + + // if EarnBase is enabled, we should sell less to get the same quote amount back if s.EarnBase { - newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice).Sub(feeQuantityReduction), s.Market.MinQuantity) + newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice).Sub(fee), s.Market.MinQuantity) } + + // always round down the base quantity for placing sell order to avoid the base currency fund locking issue + origQuantity := newQuantity + newQuantity = newQuantity.Round(s.Market.VolumePrecision, fixedpoint.Down) + s.logger.Infof("round down sell order quantity %s to %s by base quantity precision %d", origQuantity.String(), newQuantity.String(), s.Market.VolumePrecision) } orderForm := types.SubmitOrder{ @@ -1869,4 +1870,4 @@ func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (f } return v.Round(prec, fixedpoint.Up), prec -} \ No newline at end of file +} diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index a1f65d4185..e1f6773d7b 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -706,7 +706,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Type: types.OrderTypeLimit, Side: types.SideTypeBuy, Price: number(11_000.0), - Quantity: number(0.09999999), + Quantity: number(0.09999909), TimeInForce: types.TimeInForceGTC, Market: s.Market, Tag: orderTag, @@ -797,7 +797,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Symbol: "BTCUSDT", Type: types.OrderTypeLimit, Price: number(12_000.0), - Quantity: number(0.09998999), + Quantity: number(0.09999), Side: types.SideTypeSell, TimeInForce: types.TimeInForceGTC, Market: s.Market, From 756a3bb43fcc01f93f08aa557dbf472629c1d9c1 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 7 Mar 2023 18:37:45 +0800 Subject: [PATCH 0515/1392] grid2: add base round down for buy order --- pkg/strategy/grid2/strategy.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index fe70e6d2c1..396f9317c7 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -432,10 +432,15 @@ func (s *Strategy) processFilledOrder(o types.Order) { // for quote amount, always round down with price precision to prevent the quote currency fund locking rounding issue origQuoteAmount := orderExecutedQuoteAmount orderExecutedQuoteAmount = orderExecutedQuoteAmount.Round(s.Market.PricePrecision, fixedpoint.Down) + s.logger.Infof("round down %s %s order quote quantity %s to %s by quote precision %d", s.Symbol, newSide, origQuoteAmount.String(), orderExecutedQuoteAmount.String(), s.Market.PricePrecision) - s.logger.Infof("round down buy order quote quantity %s to %s by quote quantity precision %d", origQuoteAmount.String(), orderExecutedQuoteAmount.String(), s.Market.PricePrecision) + newQuantity = orderExecutedQuoteAmount.Div(newPrice) - newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice), s.Market.MinQuantity) + origQuantity := newQuantity + newQuantity = newQuantity.Round(s.Market.VolumePrecision, fixedpoint.Down) + s.logger.Infof("round down %s %s order base quantity %s to %s by base precision %d", s.Symbol, newSide, origQuantity.String(), newQuantity.String(), s.Market.VolumePrecision) + + newQuantity = fixedpoint.Max(newQuantity, s.Market.MinQuantity) } else if s.QuantityOrAmount.Quantity.Sign() > 0 { newQuantity = s.QuantityOrAmount.Quantity } From db119a22186e43df9c9f1a6128cbc9a3ea525017 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 7 Mar 2023 20:01:27 +0800 Subject: [PATCH 0516/1392] grid2: update metrics before we re-play orders --- pkg/strategy/grid2/strategy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 396f9317c7..9ebbd2ccdf 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1413,9 +1413,10 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService // before we re-play the orders, // we need to add these open orders to the active order book s.addOrdersToActiveOrderBook(gridOrders) - s.setGrid(grid) s.EmitGridReady() + s.updateGridNumOfOrdersMetrics() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) for i := range filledOrders { // avoid using the iterator From 72b6f73cb6d5430546fc7e00bab55d394f3829f0 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 7 Mar 2023 21:41:16 +0800 Subject: [PATCH 0517/1392] grid2: fix complete grid order book condition --- pkg/strategy/grid2/strategy.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 9ebbd2ccdf..301e2659e0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1526,12 +1526,7 @@ func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook func isCompleteGridOrderBook(orderBook *bbgo.ActiveOrderBook, gridNum int64) bool { tmpOrders := orderBook.Orders() - - if len(tmpOrders) == int(gridNum) && types.OrdersAll(tmpOrders, types.IsActiveOrder) { - return true - } - - return false + return len(tmpOrders) == int(gridNum)-1 && types.OrdersAll(tmpOrders, types.IsActiveOrder) } func findEarliestOrderID(orders []types.Order) (uint64, bool) { From a75bc2e590debf6553bb85bf0b95972adb59a735 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 7 Mar 2023 21:42:53 +0800 Subject: [PATCH 0518/1392] grid2: add isCompleteGridOrderBook doc comment --- pkg/strategy/grid2/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 301e2659e0..9c2c5fc0e3 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1524,6 +1524,7 @@ func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook return nil } +// isCompleteGridOrderBook checks if the number of open orders == gridNum - 1 and all orders are active order func isCompleteGridOrderBook(orderBook *bbgo.ActiveOrderBook, gridNum int64) bool { tmpOrders := orderBook.Orders() return len(tmpOrders) == int(gridNum)-1 && types.OrdersAll(tmpOrders, types.IsActiveOrder) From 2970f735426fc2ace94baf8e313ecfd776208eea Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 8 Mar 2023 15:35:44 +0800 Subject: [PATCH 0519/1392] improve/exit: show symbol in trailing stop triggered message --- pkg/bbgo/exit_trailing_stop.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/exit_trailing_stop.go b/pkg/bbgo/exit_trailing_stop.go index 6ba12ca34f..5a4c9ef394 100644 --- a/pkg/bbgo/exit_trailing_stop.go +++ b/pkg/bbgo/exit_trailing_stop.go @@ -100,7 +100,7 @@ func (s *TrailingStop2) checkStopPrice(price fixedpoint.Value, position *types.P // check if we have the minimal profit roi := position.ROI(price) if roi.Compare(s.MinProfit) >= 0 { - Notify("[trailingStop] activated: ROI %f > minimal profit ratio %f", roi.Float64(), s.MinProfit.Float64()) + Notify("[trailingStop] activated: %s ROI %f > minimal profit ratio %f", s.Symbol, roi.Float64(), s.MinProfit.Float64()) s.activated = true } } else if !s.ActivationRatio.IsZero() { @@ -174,7 +174,7 @@ func (s *TrailingStop2) triggerStop(price fixedpoint.Value) error { s.activated = false s.latestHigh = fixedpoint.Zero }() - Notify("[TrailingStop] %s stop loss triggered. price: %f callback rate: %f", s.Symbol, price.Float64(), s.CallbackRate.Float64()) + Notify("[TrailingStop] %s %s stop loss triggered. price: %f callback rate: %f", s.Symbol, s, price.Float64(), s.CallbackRate.Float64()) ctx := context.Background() p := fixedpoint.One if !s.ClosePosition.IsZero() { From c860e45c348f353fc3eee3c76f3540530249a99c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 8 Mar 2023 16:02:31 +0800 Subject: [PATCH 0520/1392] grid2: simplify isCompleteGridOrderBook --- pkg/strategy/grid2/strategy.go | 3 ++- pkg/types/order.go | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 9c2c5fc0e3..2289fe3b40 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1527,7 +1527,8 @@ func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook // isCompleteGridOrderBook checks if the number of open orders == gridNum - 1 and all orders are active order func isCompleteGridOrderBook(orderBook *bbgo.ActiveOrderBook, gridNum int64) bool { tmpOrders := orderBook.Orders() - return len(tmpOrders) == int(gridNum)-1 && types.OrdersAll(tmpOrders, types.IsActiveOrder) + activeOrders := types.OrdersActive(tmpOrders) + return len(activeOrders) == int(gridNum)-1 } func findEarliestOrderID(orders []types.Order) (uint64, bool) { diff --git a/pkg/types/order.go b/pkg/types/order.go index 82456caf94..b103fb3a55 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -395,17 +395,25 @@ func (o Order) SlackAttachment() slack.Attachment { } } -func OrdersFilled(in []Order) (out []Order) { +func OrdersFilter(in []Order, f func(o Order) bool) (out []Order) { for _, o := range in { - switch o.Status { - case OrderStatusFilled: - o2 := o - out = append(out, o2) + if f(o) { + out = append(out, o) } } return out } +func OrdersActive(in []Order) []Order { + return OrdersFilter(in, IsActiveOrder) +} + +func OrdersFilled(in []Order) (out []Order) { + return OrdersFilter(in, func(o Order) bool { + return o.Status == OrderStatusFilled + }) +} + func OrdersAll(orders []Order, f func(o Order) bool) bool { for _, o := range orders { if !f(o) { From 9068ed7ae395c50686b8e9de69692a9d5b0d5560 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 8 Mar 2023 15:10:34 +0800 Subject: [PATCH 0521/1392] fix/scale: fix LinearScale calculation --- pkg/bbgo/scale.go | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/pkg/bbgo/scale.go b/pkg/bbgo/scale.go index ec4064b3e1..062559895a 100644 --- a/pkg/bbgo/scale.go +++ b/pkg/bbgo/scale.go @@ -134,33 +134,27 @@ type LinearScale struct { Domain [2]float64 `json:"domain"` Range [2]float64 `json:"range"` - a, b float64 + // a is the ratio for Range to Domain + a float64 } func (s *LinearScale) Solve() error { xs := s.Domain ys := s.Range - // y1 = a * x1 + b - // y2 = a * x2 + b - // y2 - y1 = (a * x2 + b) - (a * x1 + b) - // y2 - y1 = (a * x2) - (a * x1) - // y2 - y1 = a * (x2 - x1) - - // a = (y2 - y1) / (x2 - x1) - // b = y1 - (a * x1) + s.a = (ys[1] - ys[0]) / (xs[1] - xs[0]) - s.b = ys[0] - (s.a * xs[0]) + return nil } func (s *LinearScale) Call(x float64) (y float64) { - if x < s.Domain[0] { - x = s.Domain[0] - } else if x > s.Domain[1] { - x = s.Domain[1] + if x <= s.Domain[0] { + return s.Range[0] + } else if x >= s.Domain[1] { + return s.Range[0] } - y = s.a*x + s.b + y = s.Range[0] + (x-s.Domain[0])*s.a return y } @@ -169,11 +163,11 @@ func (s *LinearScale) String() string { } func (s *LinearScale) Formula() string { - return fmt.Sprintf("f(x) = %f * x + %f", s.a, s.b) + return fmt.Sprintf("f(x) = %f + (x - %f) * %f", s.Range[0], s.Domain[0], s.a) } func (s *LinearScale) FormulaOf(x float64) string { - return fmt.Sprintf("f(%f) = %f * %f + %f", x, s.a, x, s.b) + return fmt.Sprintf("f(%f) = %f + (%f - %f) * %f", s.Range[0], x, s.Domain[0], s.a) } // see also: http://www.vb-helper.com/howto_find_quadratic_curve.html From 9516340303d760a9f9bdc58507bfab5020f19703 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 8 Mar 2023 17:09:58 +0800 Subject: [PATCH 0522/1392] fix/scale: update test case --- pkg/bbgo/scale_test.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/scale_test.go b/pkg/bbgo/scale_test.go index ab7ac2cf83..3b46c2c7a1 100644 --- a/pkg/bbgo/scale_test.go +++ b/pkg/bbgo/scale_test.go @@ -73,7 +73,7 @@ func TestLinearScale(t *testing.T) { err := scale.Solve() assert.NoError(t, err) - assert.Equal(t, "f(x) = 0.007000 * x + -4.000000", scale.String()) + assert.Equal(t, "f(x) = 3.000000 + (x - 1000.000000) * 0.007000", scale.String()) assert.InDelta(t, 3, scale.Call(1000), delta) assert.InDelta(t, 10, scale.Call(2000), delta) for x := 1000; x <= 2000; x += 100 { @@ -90,11 +90,24 @@ func TestLinearScale2(t *testing.T) { err := scale.Solve() assert.NoError(t, err) - assert.Equal(t, "f(x) = 0.150000 * x + -0.050000", scale.String()) + assert.Equal(t, "f(x) = 0.100000 + (x - 1.000000) * 0.150000", scale.String()) assert.InDelta(t, 0.1, scale.Call(1), delta) assert.InDelta(t, 0.4, scale.Call(3), delta) } +func TestLinearScaleNegative(t *testing.T) { + scale := LinearScale{ + Domain: [2]float64{-1, 3}, + Range: [2]float64{0.1, 0.4}, + } + + err := scale.Solve() + assert.NoError(t, err) + assert.Equal(t, "f(x) = 0.100000 + (x - -1.000000) * 0.075000", scale.String()) + assert.InDelta(t, 0.1, scale.Call(-1), delta) + assert.InDelta(t, 0.4, scale.Call(3), delta) +} + func TestQuadraticScale(t *testing.T) { // see https://www.desmos.com/calculator/vfqntrxzpr scale := QuadraticScale{ From 58b2678ae8dc8d3f1b4dca461299b4ad777256e5 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 8 Mar 2023 17:12:41 +0800 Subject: [PATCH 0523/1392] improve/exit: use roi.Percentage() instead of roi.Float64() --- pkg/bbgo/exit_trailing_stop.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/exit_trailing_stop.go b/pkg/bbgo/exit_trailing_stop.go index 5a4c9ef394..ba8d4d2d51 100644 --- a/pkg/bbgo/exit_trailing_stop.go +++ b/pkg/bbgo/exit_trailing_stop.go @@ -100,7 +100,7 @@ func (s *TrailingStop2) checkStopPrice(price fixedpoint.Value, position *types.P // check if we have the minimal profit roi := position.ROI(price) if roi.Compare(s.MinProfit) >= 0 { - Notify("[trailingStop] activated: %s ROI %f > minimal profit ratio %f", s.Symbol, roi.Float64(), s.MinProfit.Float64()) + Notify("[trailingStop] activated: %s ROI %f > minimal profit ratio %f", s.Symbol, roi.Percentage(), s.MinProfit.Float64()) s.activated = true } } else if !s.ActivationRatio.IsZero() { From f9f634646831891aa4b86f86dc8681574fcf1a59 Mon Sep 17 00:00:00 2001 From: chiahung Date: Wed, 8 Mar 2023 17:18:18 +0800 Subject: [PATCH 0524/1392] FEATURE: split self trades when use MAX RESTful API to query trades --- pkg/exchange/max/convert.go | 48 +++++++- pkg/exchange/max/convert_test.go | 116 ++++++++++++++++++ pkg/exchange/max/exchange.go | 8 +- .../v3/get_order_trades_request_requestgen.go | 5 +- .../get_wallet_trades_request_requestgen.go | 4 +- pkg/exchange/max/maxapi/v3/order.go | 1 - pkg/exchange/max/maxapi/v3/trade.go | 33 +++++ 7 files changed, 203 insertions(+), 12 deletions(-) create mode 100644 pkg/exchange/max/convert_test.go create mode 100644 pkg/exchange/max/maxapi/v3/trade.go diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 80e01bab8f..c8c406b4ab 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -5,7 +5,8 @@ import ( "strings" "time" - "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + max "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + v3 "github.com/c9s/bbgo/pkg/exchange/max/maxapi/v3" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -193,7 +194,50 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { }, nil } -func toGlobalTrade(t max.Trade) (*types.Trade, error) { +func toGlobalTradeV3(t v3.Trade) ([]types.Trade, error) { + var trades []types.Trade + isMargin := t.WalletType == max.WalletTypeMargin + side := toGlobalSideType(t.Side) + + trade := types.Trade{ + ID: t.ID, + OrderID: t.OrderID, + Price: t.Price, + Symbol: toGlobalSymbol(t.Market), + Exchange: types.ExchangeMax, + Quantity: t.Volume, + Side: side, + IsBuyer: t.IsBuyer(), + IsMaker: t.IsMaker(), + Fee: t.Fee, + FeeCurrency: toGlobalCurrency(t.FeeCurrency), + QuoteQuantity: t.Funds, + Time: types.Time(t.CreatedAt), + IsMargin: isMargin, + IsIsolated: false, + IsFutures: false, + } + + if t.Side == "self-trade" { + trade.Side = types.SideTypeSell + + // create trade for bid + bidTrade := trade + bidTrade.Side = types.SideTypeBuy + bidTrade.OrderID = t.SelfTradeBidOrderID + bidTrade.Fee = t.SelfTradeBidFee + bidTrade.FeeCurrency = t.SelfTradeBidFeeCurrency + bidTrade.IsBuyer = !trade.IsBuyer + bidTrade.IsMaker = !trade.IsMaker + trades = append(trades, bidTrade) + } + + trades = append(trades, trade) + + return trades, nil +} + +func toGlobalTradeV2(t max.Trade) (*types.Trade, error) { isMargin := t.WalletType == max.WalletTypeMargin side := toGlobalSideType(t.Side) return &types.Trade{ diff --git a/pkg/exchange/max/convert_test.go b/pkg/exchange/max/convert_test.go new file mode 100644 index 0000000000..d6a3b3c4b5 --- /dev/null +++ b/pkg/exchange/max/convert_test.go @@ -0,0 +1,116 @@ +package max + +import ( + "encoding/json" + "testing" + + v3 "github.com/c9s/bbgo/pkg/exchange/max/maxapi/v3" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +func Test_toGlobalTradeV3(t *testing.T) { + assert := assert.New(t) + + t.Run("ask trade", func(t *testing.T) { + str := ` + { + "id": 68444, + "order_id": 87, + "wallet_type": "spot", + "price": "21499.0", + "volume": "0.2658", + "funds": "5714.4", + "market": "ethtwd", + "market_name": "ETH/TWD", + "side": "bid", + "fee": "0.00001", + "fee_currency": "usdt", + "self_trade_bid_fee": "0.00001", + "self_trade_bid_fee_currency": "eth", + "self_trade_bid_order_id": 86, + "liquidity": "maker", + "created_at": 1521726960357 + } + ` + + var trade v3.Trade + assert.NoError(json.Unmarshal([]byte(str), &trade)) + + trades, err := toGlobalTradeV3(trade) + assert.NoError(err) + assert.Len(trades, 1) + + assert.Equal(uint64(87), trades[0].OrderID) + assert.Equal(types.SideTypeBuy, trades[0].Side) + }) + + t.Run("bid trade", func(t *testing.T) { + str := ` + { + "id": 68444, + "order_id": 87, + "wallet_type": "spot", + "price": "21499.0", + "volume": "0.2658", + "funds": "5714.4", + "market": "ethtwd", + "market_name": "ETH/TWD", + "side": "ask", + "fee": "0.00001", + "fee_currency": "usdt", + "self_trade_bid_fee": "0.00001", + "self_trade_bid_fee_currency": "eth", + "self_trade_bid_order_id": 86, + "liquidity": "maker", + "created_at": 1521726960357 + } + ` + + var trade v3.Trade + assert.NoError(json.Unmarshal([]byte(str), &trade)) + + trades, err := toGlobalTradeV3(trade) + assert.NoError(err) + assert.Len(trades, 1) + + assert.Equal(uint64(87), trades[0].OrderID) + assert.Equal(types.SideTypeSell, trades[0].Side) + }) + + t.Run("self trade", func(t *testing.T) { + str := ` + { + "id": 68444, + "order_id": 87, + "wallet_type": "spot", + "price": "21499.0", + "volume": "0.2658", + "funds": "5714.4", + "market": "ethtwd", + "market_name": "ETH/TWD", + "side": "self-trade", + "fee": "0.00001", + "fee_currency": "usdt", + "self_trade_bid_fee": "0.00001", + "self_trade_bid_fee_currency": "eth", + "self_trade_bid_order_id": 86, + "liquidity": "maker", + "created_at": 1521726960357 + } + ` + + var trade v3.Trade + assert.NoError(json.Unmarshal([]byte(str), &trade)) + + trades, err := toGlobalTradeV3(trade) + assert.NoError(err) + assert.Len(trades, 2) + + assert.Equal(uint64(86), trades[0].OrderID) + assert.Equal(types.SideTypeBuy, trades[0].Side) + + assert.Equal(uint64(87), trades[1].OrderID) + assert.Equal(types.SideTypeSell, trades[1].Side) + }) +} diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index abdcab6086..41a7b02017 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -186,13 +186,13 @@ func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([] var trades []types.Trade for _, t := range maxTrades { - localTrade, err := toGlobalTrade(t) + localTrades, err := toGlobalTradeV3(t) if err != nil { log.WithError(err).Errorf("can not convert trade: %+v", t) continue } - trades = append(trades, *localTrade) + trades = append(trades, localTrades...) } // ensure everything is sorted ascending @@ -806,13 +806,13 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type } for _, t := range maxTrades { - localTrade, err := toGlobalTrade(t) + localTrades, err := toGlobalTradeV3(t) if err != nil { log.WithError(err).Errorf("can not convert trade: %+v", t) continue } - trades = append(trades, *localTrade) + trades = append(trades, localTrades...) } // ensure everything is sorted ascending diff --git a/pkg/exchange/max/maxapi/v3/get_order_trades_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_order_trades_request_requestgen.go index 10bd1cd448..e739f3396f 100644 --- a/pkg/exchange/max/maxapi/v3/get_order_trades_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_order_trades_request_requestgen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/c9s/bbgo/pkg/exchange/max/maxapi" "net/url" "reflect" "regexp" @@ -136,7 +135,7 @@ func (g *GetOrderTradesRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } -func (g *GetOrderTradesRequest) Do(ctx context.Context) ([]max.Trade, error) { +func (g *GetOrderTradesRequest) Do(ctx context.Context) ([]Trade, error) { // empty params for GET operation var params interface{} @@ -157,7 +156,7 @@ func (g *GetOrderTradesRequest) Do(ctx context.Context) ([]max.Trade, error) { return nil, err } - var apiResponse []max.Trade + var apiResponse []Trade if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go index 2fdf94c400..647916103c 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go @@ -198,7 +198,7 @@ func (g *GetWalletTradesRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } -func (g *GetWalletTradesRequest) Do(ctx context.Context) ([]max.Trade, error) { +func (g *GetWalletTradesRequest) Do(ctx context.Context) ([]Trade, error) { // empty params for GET operation var params interface{} @@ -225,7 +225,7 @@ func (g *GetWalletTradesRequest) Do(ctx context.Context) ([]max.Trade, error) { return nil, err } - var apiResponse []max.Trade + var apiResponse []Trade if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } diff --git a/pkg/exchange/max/maxapi/v3/order.go b/pkg/exchange/max/maxapi/v3/order.go index df3a63a606..22486c112e 100644 --- a/pkg/exchange/max/maxapi/v3/order.go +++ b/pkg/exchange/max/maxapi/v3/order.go @@ -15,7 +15,6 @@ type WalletType = maxapi.WalletType type OrderType = maxapi.OrderType type Order = maxapi.Order -type Trade = maxapi.Trade type Account = maxapi.Account // OrderService manages the Order endpoint. diff --git a/pkg/exchange/max/maxapi/v3/trade.go b/pkg/exchange/max/maxapi/v3/trade.go new file mode 100644 index 0000000000..c9e975bb0b --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/trade.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type Trade struct { + ID uint64 `json:"id" db:"exchange_id"` + WalletType WalletType `json:"wallet_type,omitempty"` + Price fixedpoint.Value `json:"price"` + Volume fixedpoint.Value `json:"volume"` + Funds fixedpoint.Value `json:"funds"` + Market string `json:"market"` + MarketName string `json:"market_name"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + Side string `json:"side"` + OrderID uint64 `json:"order_id"` + Fee fixedpoint.Value `json:"fee"` // float number as string + FeeCurrency string `json:"fee_currency"` + Liquidity string `json:"liquidity"` + SelfTradeBidFee fixedpoint.Value `json:"self_trade_bid_fee"` + SelfTradeBidFeeCurrency string `json:"self_trade_bid_fee_currency"` + SelfTradeBidOrderID uint64 `json:"self_trade_bid_order_id"` +} + +func (t Trade) IsBuyer() bool { + return t.Side == "bid" +} + +func (t Trade) IsMaker() bool { + return t.Liquidity == "maker" +} From f92bcda51d8a12cb82128e0235dd1ad646a023cf Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 8 Mar 2023 19:31:47 +0800 Subject: [PATCH 0525/1392] improve/exit: fix typo --- pkg/bbgo/scale.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/scale.go b/pkg/bbgo/scale.go index 062559895a..e4d36133d0 100644 --- a/pkg/bbgo/scale.go +++ b/pkg/bbgo/scale.go @@ -167,7 +167,7 @@ func (s *LinearScale) Formula() string { } func (s *LinearScale) FormulaOf(x float64) string { - return fmt.Sprintf("f(%f) = %f + (%f - %f) * %f", s.Range[0], x, s.Domain[0], s.a) + return fmt.Sprintf("f(%f) = %f + (%f - %f) * %f", x, s.Range[0], x, s.Domain[0], s.a) } // see also: http://www.vb-helper.com/howto_find_quadratic_curve.html From 40e2296492f2069e8a03ef89cb868b27f661f487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= <4680567+narumiruna@users.noreply.github.com> Date: Wed, 8 Mar 2023 13:54:36 +0000 Subject: [PATCH 0526/1392] add positions and profit stats --- pkg/strategy/rebalance/strategy.go | 122 ++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 27 deletions(-) diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index 681c6022ce..9de03764b0 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -20,6 +20,8 @@ func init() { } type Strategy struct { + Environment *bbgo.Environment + Interval types.Interval `json:"interval"` QuoteCurrency string `json:"quoteCurrency"` TargetWeights types.ValueMap `json:"targetWeights"` @@ -28,7 +30,12 @@ type Strategy struct { OrderType types.OrderType `json:"orderType"` DryRun bool `json:"dryRun"` - activeOrderBook *bbgo.ActiveOrderBook + PositionMap map[string]*types.Position `persistence:"positionMap"` + ProfitStatsMap map[string]*types.ProfitStats `persistence:"profitStatsMap"` + + session *bbgo.ExchangeSession + orderExecutorMap map[string]*bbgo.GeneralOrderExecutor + activeOrderBook *bbgo.ActiveOrderBook } func (s *Strategy) Defaults() error { @@ -46,6 +53,10 @@ func (s *Strategy) ID() string { return ID } +func (s *Strategy) InstanceID(symbol string) string { + return fmt.Sprintf("%s:%s", ID, symbol) +} + func (s *Strategy) Validate() error { if len(s.TargetWeights) == 0 { return fmt.Errorf("targetWeights should not be empty") @@ -77,31 +88,75 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } -func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - s.activeOrderBook = bbgo.NewActiveOrderBook("") - s.activeOrderBook.BindStream(session.UserDataStream) +func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.session = session - markets := session.Markets() - for _, symbol := range s.symbols() { - if _, ok := markets[symbol]; !ok { - return fmt.Errorf("exchange: %s does not supoort matket: %s", session.Exchange.Name(), symbol) - } + markets, err := s.markets() + if err != nil { + return err + } + + if s.PositionMap == nil { + s.initPositionMapFromMarkets(markets) + } + + if s.ProfitStatsMap == nil { + s.initProfitStatsMapFromMarkets(markets) } - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - s.rebalance(ctx, orderExecutor, session) + s.initOrderExecutorMapFromMarkets(ctx, markets) + + s.activeOrderBook = bbgo.NewActiveOrderBook("") + s.activeOrderBook.BindStream(s.session.UserDataStream) + + s.session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + s.rebalance(ctx) }) return nil } -func (s *Strategy) rebalance(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) { +func (s *Strategy) initPositionMapFromMarkets(markets []types.Market) { + s.PositionMap = make(map[string]*types.Position) + for _, market := range markets { + position := types.NewPositionFromMarket(market) + position.Strategy = s.ID() + position.StrategyInstanceID = s.InstanceID(market.Symbol) + s.PositionMap[market.Symbol] = position + } +} + +func (s *Strategy) initProfitStatsMapFromMarkets(markets []types.Market) { + s.ProfitStatsMap = make(map[string]*types.ProfitStats) + for _, market := range markets { + s.ProfitStatsMap[market.Symbol] = types.NewProfitStats(market) + } +} + +func (s *Strategy) initOrderExecutorMapFromMarkets(ctx context.Context, markets []types.Market) { + s.orderExecutorMap = make(map[string]*bbgo.GeneralOrderExecutor) + for _, market := range markets { + symbol := market.Symbol + + orderExecutor := bbgo.NewGeneralOrderExecutor(s.session, symbol, ID, s.InstanceID(symbol), s.PositionMap[symbol]) + orderExecutor.BindEnvironment(s.Environment) + orderExecutor.BindProfitStats(s.ProfitStatsMap[symbol]) + orderExecutor.Bind() + orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + + s.orderExecutorMap[market.Symbol] = orderExecutor + } +} + +func (s *Strategy) rebalance(ctx context.Context) { // cancel active orders before rebalance - if err := session.Exchange.CancelOrders(ctx, s.activeOrderBook.Orders()...); err != nil { + if err := s.session.Exchange.CancelOrders(ctx, s.activeOrderBook.Orders()...); err != nil { log.WithError(err).Errorf("failed to cancel orders") } - submitOrders := s.generateSubmitOrders(ctx, session) + submitOrders := s.generateSubmitOrders(ctx) for _, order := range submitOrders { log.Infof("generated submit order: %s", order.String()) } @@ -110,16 +165,17 @@ func (s *Strategy) rebalance(ctx context.Context, orderExecutor bbgo.OrderExecut return } - createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrders...) - if err != nil { - log.WithError(err).Error("failed to submit orders") - return + for _, submitOrder := range submitOrders { + createdOrders, err := s.orderExecutorMap[submitOrder.Symbol].SubmitOrders(ctx, submitOrder) + if err != nil { + log.WithError(err).Error("failed to submit orders") + return + } + s.activeOrderBook.Add(createdOrders...) } - - s.activeOrderBook.Add(createdOrders...) } -func (s *Strategy) prices(ctx context.Context, session *bbgo.ExchangeSession) types.ValueMap { +func (s *Strategy) prices(ctx context.Context) types.ValueMap { m := make(types.ValueMap) for currency := range s.TargetWeights { if currency == s.QuoteCurrency { @@ -127,7 +183,7 @@ func (s *Strategy) prices(ctx context.Context, session *bbgo.ExchangeSession) ty continue } - ticker, err := session.Exchange.QueryTicker(ctx, currency+s.QuoteCurrency) + ticker, err := s.session.Exchange.QueryTicker(ctx, currency+s.QuoteCurrency) if err != nil { log.WithError(err).Error("failed to query tickers") return nil @@ -138,10 +194,10 @@ func (s *Strategy) prices(ctx context.Context, session *bbgo.ExchangeSession) ty return m } -func (s *Strategy) quantities(session *bbgo.ExchangeSession) types.ValueMap { +func (s *Strategy) quantities() types.ValueMap { m := make(types.ValueMap) - balances := session.GetAccount().Balances() + balances := s.session.GetAccount().Balances() for currency := range s.TargetWeights { m[currency] = balances[currency].Total() } @@ -149,9 +205,9 @@ func (s *Strategy) quantities(session *bbgo.ExchangeSession) types.ValueMap { return m } -func (s *Strategy) generateSubmitOrders(ctx context.Context, session *bbgo.ExchangeSession) (submitOrders []types.SubmitOrder) { - prices := s.prices(ctx, session) - marketValues := prices.Mul(s.quantities(session)) +func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []types.SubmitOrder) { + prices := s.prices(ctx) + marketValues := prices.Mul(s.quantities()) currentWeights := marketValues.Normalize() for currency, targetWeight := range s.TargetWeights { @@ -225,3 +281,15 @@ func (s *Strategy) symbols() (symbols []string) { } return symbols } + +func (s *Strategy) markets() ([]types.Market, error) { + markets := []types.Market{} + for _, symbol := range s.symbols() { + market, ok := s.session.Market(symbol) + if !ok { + return nil, fmt.Errorf("market %s not found", symbol) + } + markets = append(markets, market) + } + return markets, nil +} From 5988567d09ccfb28b4b711601dbd7d5d5e4cb6ec Mon Sep 17 00:00:00 2001 From: gx578007 Date: Wed, 8 Mar 2023 21:36:19 +0800 Subject: [PATCH 0527/1392] FEATURE: [grid2] add more metrics and fix metric-related issues --- pkg/strategy/grid2/metrics.go | 106 +++++++++++++++++++++++++++++++-- pkg/strategy/grid2/strategy.go | 61 +++++++++++++++---- 2 files changed, 150 insertions(+), 17 deletions(-) diff --git a/pkg/strategy/grid2/metrics.go b/pkg/strategy/grid2/metrics.go index 17938f9643..56e9638acc 100644 --- a/pkg/strategy/grid2/metrics.go +++ b/pkg/strategy/grid2/metrics.go @@ -5,11 +5,20 @@ import ( ) var ( - metricsGridNum *prometheus.GaugeVec - metricsGridNumOfOrders *prometheus.GaugeVec - metricsGridNumOfMissingOrders *prometheus.GaugeVec - metricsGridOrderPrices *prometheus.GaugeVec - metricsGridProfit *prometheus.GaugeVec + metricsGridNum *prometheus.GaugeVec + metricsGridNumOfOrders *prometheus.GaugeVec + metricsGridNumOfOrdersWithCorrectPrice *prometheus.GaugeVec + metricsGridNumOfMissingOrders *prometheus.GaugeVec + metricsGridNumOfMissingOrdersWithCorrectPrice *prometheus.GaugeVec + metricsGridOrderPrices *prometheus.GaugeVec + metricsGridProfit *prometheus.GaugeVec + + metricsGridUpperPrice *prometheus.GaugeVec + metricsGridLowerPrice *prometheus.GaugeVec + metricsGridQuoteInvestment *prometheus.GaugeVec + metricsGridBaseInvestment *prometheus.GaugeVec + + metricsGridFilledOrderPrice *prometheus.GaugeVec ) func labelKeys(labels prometheus.Labels) []string { @@ -60,6 +69,17 @@ func initMetrics(extendedLabels []string) { }, extendedLabels...), ) + metricsGridNumOfOrdersWithCorrectPrice = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_num_of_correct_price_orders", + Help: "number of orders with correct grid prices", + }, + append([]string{ + "exchange", // exchange name + "symbol", // symbol of the market + }, extendedLabels...), + ) + metricsGridNumOfMissingOrders = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "bbgo_grid2_num_of_missing_orders", @@ -71,6 +91,17 @@ func initMetrics(extendedLabels []string) { }, extendedLabels...), ) + metricsGridNumOfMissingOrdersWithCorrectPrice = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_num_of_missing_correct_price_orders", + Help: "number of missing orders with correct prices", + }, + append([]string{ + "exchange", // exchange name + "symbol", // symbol of the market + }, extendedLabels...), + ) + metricsGridOrderPrices = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "bbgo_grid2_order_prices", @@ -86,7 +117,7 @@ func initMetrics(extendedLabels []string) { metricsGridProfit = prometheus.NewGaugeVec( prometheus.GaugeOpts{ - Name: "bbgo_grid2_grid_profit", + Name: "bbgo_grid2_profit", Help: "realized grid profit", }, append([]string{ @@ -94,6 +125,62 @@ func initMetrics(extendedLabels []string) { "symbol", // symbol of the market }, extendedLabels...), ) + + metricsGridUpperPrice = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_upper_price", + Help: "the upper price of grid", + }, + append([]string{ + "exchange", // exchange name + "symbol", // symbol of the market + }, extendedLabels...), + ) + + metricsGridLowerPrice = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_lower_price", + Help: "the lower price of grid", + }, + append([]string{ + "exchange", // exchange name + "symbol", // symbol of the market + }, extendedLabels...), + ) + + metricsGridQuoteInvestment = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_quote_investment", + Help: "the quote investment of grid", + }, + append([]string{ + "exchange", // exchange name + "symbol", // symbol of the market + }, extendedLabels...), + ) + + metricsGridBaseInvestment = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_base_investment", + Help: "the base investment of grid", + }, + append([]string{ + "exchange", // exchange name + "symbol", // symbol of the market + }, extendedLabels...), + ) + + metricsGridFilledOrderPrice = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_grid2_filled_order_price", + Help: "the price of filled grid order", + }, + append([]string{ + "exchange", // exchange name + "symbol", // symbol of the market + "side", + }, extendedLabels...), + ) } var metricsRegistered = false @@ -111,9 +198,16 @@ func registerMetrics() { prometheus.MustRegister( metricsGridNum, metricsGridNumOfOrders, + metricsGridNumOfOrdersWithCorrectPrice, metricsGridNumOfMissingOrders, + metricsGridNumOfMissingOrdersWithCorrectPrice, metricsGridProfit, metricsGridOrderPrices, + metricsGridLowerPrice, + metricsGridUpperPrice, + metricsGridQuoteInvestment, + metricsGridBaseInvestment, + metricsGridFilledOrderPrice, ) metricsRegistered = true } \ No newline at end of file diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2289fe3b40..36246b6041 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -512,6 +512,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } s.logger.Infof("GRID ORDER FILLED: %s", o.String()) + s.updateFilledOrderMetrics(o) s.processFilledOrder(o) } @@ -807,7 +808,7 @@ func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.Exch // sync the profits to redis bbgo.Sync(ctx, s) - s.updateGridNumOfOrdersMetrics() + s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) } } @@ -943,7 +944,7 @@ func (s *Strategy) CloseGrid(ctx context.Context) error { // free the grid object s.setGrid(nil) - s.updateGridNumOfOrdersMetrics() + s.updateGridNumOfOrdersMetricsWithLock() return err } @@ -959,7 +960,10 @@ func (s *Strategy) newGrid() *Grid { func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) error { // grid object guard s.mu.Lock() - defer s.mu.Unlock() + defer func() { + s.mu.Unlock() + s.updateGridNumOfOrdersMetricsWithLock() + }() if s.grid != nil { return nil @@ -1080,23 +1084,44 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return nil } -func (s *Strategy) updateGridNumOfOrdersMetrics() { +func (s *Strategy) updateFilledOrderMetrics(order types.Order) { + labels := s.newPrometheusLabels() + labels["side"] = order.Side.String() + metricsGridFilledOrderPrice.With(labels).Set(order.Price.Float64()) +} + +func (s *Strategy) updateGridNumOfOrdersMetricsWithLock() { baseLabels := s.newPrometheusLabels() - numOfOrders := s.orderExecutor.ActiveMakerOrders().NumOfOrders() + makerOrders := s.orderExecutor.ActiveMakerOrders() + numOfOrders := makerOrders.NumOfOrders() metricsGridNumOfOrders.With(baseLabels).Set(float64(numOfOrders)) + metricsGridLowerPrice.With(baseLabels).Set(s.LowerPrice.Float64()) + metricsGridUpperPrice.With(baseLabels).Set(s.UpperPrice.Float64()) + metricsGridQuoteInvestment.With(baseLabels).Set(s.QuoteInvestment.Float64()) + metricsGridBaseInvestment.With(baseLabels).Set(s.BaseInvestment.Float64()) if grid := s.getGrid(); grid != nil { gridNum := grid.Size.Int() metricsGridNum.With(baseLabels).Set(float64(gridNum)) numOfMissingOrders := gridNum - 1 - numOfOrders metricsGridNumOfMissingOrders.With(baseLabels).Set(float64(numOfMissingOrders)) + + var numOfOrdersWithCorrectPrice int + for _, order := range makerOrders.Orders() { + if grid.HasPin(Pin(order.Price)) { + numOfOrdersWithCorrectPrice++ + } + } + numOfMissingOrdersWithCorrectPrice := gridNum - 1 - numOfOrdersWithCorrectPrice + metricsGridNumOfOrdersWithCorrectPrice.With(baseLabels).Set(float64(numOfOrdersWithCorrectPrice)) + metricsGridNumOfMissingOrdersWithCorrectPrice.With(baseLabels).Set(float64(numOfMissingOrdersWithCorrectPrice)) } } func (s *Strategy) updateOpenOrderPricesMetrics(orders []types.Order) { orders = sortOrdersByPriceAscending(orders) num := len(orders) - metricsGridOrderPrices.Reset() + s.deleteOpenOrderPricesMetrics() for idx, order := range orders { labels := s.newPrometheusLabels() labels["side"] = order.Side.String() @@ -1105,6 +1130,20 @@ func (s *Strategy) updateOpenOrderPricesMetrics(orders []types.Order) { } } +func (s *Strategy) deleteOpenOrderPricesMetrics() { + for i := 1; i <= int(s.GridNum); i++ { + ithStr := strconv.Itoa(i) + labels := s.newPrometheusLabels() + labels["side"] = "BUY" + labels["ith"] = ithStr + metricsGridOrderPrices.Delete(labels) + labels = s.newPrometheusLabels() + labels["side"] = "SELL" + labels["ith"] = ithStr + metricsGridOrderPrices.Delete(labels) + } +} + func sortOrdersByPriceAscending(orders []types.Order) []types.Order { sort.Slice(orders, func(i, j int) bool { a := orders[i] @@ -1341,7 +1380,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.addOrdersToActiveOrderBook(gridOrders) s.setGrid(grid) s.EmitGridReady() - s.updateGridNumOfOrdersMetrics() + s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } else { @@ -1388,7 +1427,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.addOrdersToActiveOrderBook(gridOrders) s.setGrid(grid) s.EmitGridReady() - s.updateGridNumOfOrdersMetrics() + s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } @@ -1415,7 +1454,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.addOrdersToActiveOrderBook(gridOrders) s.setGrid(grid) s.EmitGridReady() - s.updateGridNumOfOrdersMetrics() + s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) for i := range filledOrders { @@ -1432,7 +1471,7 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) - s.updateGridNumOfOrdersMetrics() + s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } @@ -1873,4 +1912,4 @@ func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (f } return v.Round(prec, fixedpoint.Up), prec -} +} \ No newline at end of file From 045c8de2a612ae6d95e2de31c948e1245175f8cd Mon Sep 17 00:00:00 2001 From: gx578007 Date: Thu, 9 Mar 2023 11:26:02 +0800 Subject: [PATCH 0528/1392] refactor metric function to be separated in terms of lock --- pkg/strategy/grid2/strategy.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 36246b6041..3d51573140 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -960,16 +960,14 @@ func (s *Strategy) newGrid() *Grid { func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) error { // grid object guard s.mu.Lock() - defer func() { - s.mu.Unlock() - s.updateGridNumOfOrdersMetricsWithLock() - }() + defer s.mu.Unlock() if s.grid != nil { return nil } - s.grid = s.newGrid() + grid := s.newGrid() + s.grid = grid s.logger.Info("OPENING GRID: ", s.grid.String()) lastPrice, err := s.getLastTradePrice(ctx, session) @@ -1080,6 +1078,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) s.logger.Infof("ALL GRID ORDERS SUBMITTED") + s.updateGridNumOfOrdersMetrics(grid) s.updateOpenOrderPricesMetrics(createdOrders) return nil } @@ -1091,6 +1090,10 @@ func (s *Strategy) updateFilledOrderMetrics(order types.Order) { } func (s *Strategy) updateGridNumOfOrdersMetricsWithLock() { + s.updateGridNumOfOrdersMetrics(s.getGrid()) +} + +func (s *Strategy) updateGridNumOfOrdersMetrics(grid *Grid) { baseLabels := s.newPrometheusLabels() makerOrders := s.orderExecutor.ActiveMakerOrders() numOfOrders := makerOrders.NumOfOrders() @@ -1100,7 +1103,7 @@ func (s *Strategy) updateGridNumOfOrdersMetricsWithLock() { metricsGridQuoteInvestment.With(baseLabels).Set(s.QuoteInvestment.Float64()) metricsGridBaseInvestment.With(baseLabels).Set(s.BaseInvestment.Float64()) - if grid := s.getGrid(); grid != nil { + if grid != nil { gridNum := grid.Size.Int() metricsGridNum.With(baseLabels).Set(float64(gridNum)) numOfMissingOrders := gridNum - 1 - numOfOrders From d29c3fa05c02de2644f7e598530e398f9a222375 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 9 Mar 2023 11:35:35 +0800 Subject: [PATCH 0529/1392] FIX: use updated_at instead of created_at to convert MAX order to types.Order --- pkg/exchange/max/convert.go | 2 +- pkg/exchange/max/maxapi/order.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index c8c406b4ab..fc169fac44 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -188,7 +188,7 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume), ExecutedQuantity: executedVolume, CreationTime: types.Time(maxOrder.CreatedAt.Time()), - UpdateTime: types.Time(maxOrder.CreatedAt.Time()), + UpdateTime: types.Time(maxOrder.UpdatedAt.Time()), IsMargin: isMargin, IsIsolated: false, // isolated margin is not supported }, nil diff --git a/pkg/exchange/max/maxapi/order.go b/pkg/exchange/max/maxapi/order.go index fa76096b7b..be3fdb1fe9 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -91,4 +91,5 @@ type Order struct { GroupID uint32 `json:"group_id,omitempty"` ClientOID string `json:"client_oid,omitempty"` CreatedAt types.MillisecondTimestamp `json:"created_at"` + UpdatedAt types.MillisecondTimestamp `json:"updated_at"` } From ead5486b521540a1906906259dacd75ed1fbdbaa Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 9 Mar 2023 16:15:38 +0800 Subject: [PATCH 0530/1392] FIX: filter wrong order id from self-trade trades --- pkg/exchange/max/exchange.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 41a7b02017..7aa223f1ac 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -192,7 +192,13 @@ func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([] continue } - trades = append(trades, localTrades...) + // because self-trades will contains ask and bid orders in its struct + // we need to make sure the trade's order is what we want + for _, localTrade := range localTrades { + if localTrade.OrderID == uint64(orderID) { + trades = append(trades, localTrade) + } + } } // ensure everything is sorted ascending From 4288c82e25cb48470e7c016b6b221cce48a78c01 Mon Sep 17 00:00:00 2001 From: chiahung Date: Tue, 7 Mar 2023 20:58:05 +0800 Subject: [PATCH 0531/1392] FEATURE: recover grids with open orders by querying trades process and its buildPinOrderMap method --- pkg/strategy/grid2/strategy.go | 107 +++++++++++++++++++++++++- pkg/strategy/grid2/strategy_test.go | 115 ++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3d51573140..f61750a696 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1339,6 +1339,70 @@ func (s *Strategy) checkMinimalQuoteInvestment(grid *Grid) error { return nil } +func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrdersOnGrid []types.Order) error { + if s.orderQueryService == nil { + return fmt.Errorf("orderQueryService is nil, it can't get orders by trade") + } + + // set grid + grid := s.newGrid() + s.setGrid(grid) + + // add open orders to active order book + s.addOrdersToActiveOrderBook(openOrdersOnGrid) + + expectedOrderNums := s.GridNum - 1 + openOrdersOnGridNums := int64(len(openOrdersOnGrid)) + s.logger.Infof("[DEBUG] open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) + if expectedOrderNums == openOrdersOnGridNums { + // no need to recover + return nil + } + + // 1. build pin-order map + // 2. fill the pin-order map by querying trades + // 3. get the filled orders from pin-order map + // 4. emit the filled orders + + // 5. emit grid ready + s.EmitGridReady() + + // 6. debug and send metrics + debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) + s.updateGridNumOfOrdersMetrics() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) + + return nil +} + +// buildPinOrderMap build the pin-order map with grid and open orders. +// The keys of this map contains all required pins of this grid. +// If the Order of the pin is empty types.Order (OrderID == 0), it means there is no open orders at this pin. +func (s *Strategy) buildPinOrderMap(grid *Grid, openOrders []types.Order) (map[string]types.Order, error) { + pinOrderMap := make(map[string]types.Order) + + for _, pin := range grid.Pins { + priceStr := s.FormatPrice(fixedpoint.Value(pin)) + pinOrderMap[priceStr] = types.Order{} + } + + for _, openOrder := range openOrders { + priceStr := s.FormatPrice(openOrder.Price) + v, exist := pinOrderMap[priceStr] + if !exist { + return nil, fmt.Errorf("the price of the order (id: %d) is not in pins", openOrder.OrderID) + } + + if v.OrderID != 0 { + return nil, fmt.Errorf("there are duplicated open orders at the same pin") + } + + pinOrderMap[priceStr] = openOrder + } + + return pinOrderMap, nil +} + func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { grid := s.newGrid() @@ -1846,7 +1910,7 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi if s.RecoverOrdersWhenStart { // do recover only when triggerPrice is not set and not in the back-test mode s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") - if err := s.recoverGrid(ctx, session); err != nil { + if err := s.recoverGridByScanningOrders(ctx, session); err != nil { s.logger.WithError(err).Errorf("recover error") } } @@ -1857,7 +1921,7 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi } } -func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { +func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbgo.ExchangeSession) error { openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { return err @@ -1884,6 +1948,45 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio return nil } +func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { + // no initial order id means we don't need to recover + if s.GridProfitStats.InitialOrderID == 0 { + s.logger.Info("[DEBUG] new strategy, no need to recover") + return nil + } + + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } + + s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) + + s.logger.Infof("[DEBUG] recover grid with group id: %d", s.OrderGroupID) + // filter out the order with the group id belongs to this grid + var openOrdersOnGrid []types.Order + for _, order := range openOrders { + s.logger.Infof("[DEBUG] order (%d) group id: %d", order.OrderID, order.GroupID) + if order.GroupID == s.OrderGroupID { + openOrdersOnGrid = append(openOrdersOnGrid, order) + } + } + + s.logger.Infof("found %d open orders belong to this grid on the %s order book", len(openOrdersOnGrid), s.Symbol) + + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) + if !implemented { + s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") + return nil + } + + if err := s.recoverGridWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { + return errors.Wrap(err, "grid recover error") + } + + return nil +} + // openOrdersMismatches verifies if the open orders are on the grid pins // return true if mismatches func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.ExchangeSession) (bool, error) { diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index e1f6773d7b..c70a9eaecf 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -981,3 +981,118 @@ func Test_roundUpMarketQuantity(t *testing.T) { assert.Equal(t, "0.00000003", q3.String(), "rounding prec 8") assert.Equal(t, 8, prec) } + +func Test_buildPinOrderMap(t *testing.T) { + assert := assert.New(t) + s := newTestStrategy() + s.UpperPrice = number(2000.0) + s.LowerPrice = number(1000.0) + s.GridNum = 11 + s.grid = s.newGrid() + + t.Run("successful case", func(t *testing.T) { + openOrders := []types.Order{ + types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(1.0), + Price: number(1000.0), + AveragePrice: number(0), + StopPrice: number(0), + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "max", + GID: 1, + OrderID: 1, + Status: types.OrderStatusNew, + ExecutedQuantity: number(0.0), + IsWorking: false, + }, + } + m, err := s.buildPinOrderMap(s.grid, openOrders) + assert.NoError(err) + assert.Len(m, 11) + + for pin, order := range m { + if pin == s.FormatPrice(openOrders[0].Price) { + assert.Equal(openOrders[0].OrderID, order.OrderID) + } else { + assert.Equal(uint64(0), order.OrderID) + } + } + }) + + t.Run("there is one order with non-pin price in openOrders", func(t *testing.T) { + openOrders := []types.Order{ + types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(1.0), + Price: number(1111.0), + AveragePrice: number(0), + StopPrice: number(0), + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "max", + GID: 1, + OrderID: 1, + Status: types.OrderStatusNew, + ExecutedQuantity: number(0.0), + IsWorking: false, + }, + } + _, err := s.buildPinOrderMap(s.grid, openOrders) + assert.Error(err) + }) + + t.Run("there are duplicated open orders at same pin", func(t *testing.T) { + openOrders := []types.Order{ + types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(1.0), + Price: number(1000.0), + AveragePrice: number(0), + StopPrice: number(0), + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "max", + GID: 1, + OrderID: 1, + Status: types.OrderStatusNew, + ExecutedQuantity: number(0.0), + IsWorking: false, + }, + types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(1.0), + Price: number(1000.0), + AveragePrice: number(0), + StopPrice: number(0), + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "max", + GID: 2, + OrderID: 2, + Status: types.OrderStatusNew, + ExecutedQuantity: number(0.0), + IsWorking: false, + }, + } + _, err := s.buildPinOrderMap(s.grid, openOrders) + assert.Error(err) + }) +} From 67001fcbb79b49e36e44791c62cd2694b062ab74 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 9 Mar 2023 17:42:37 +0800 Subject: [PATCH 0532/1392] new config 'recoverGridByScanningTrades' --- pkg/strategy/grid2/strategy.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f61750a696..91cc533161 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -157,7 +157,8 @@ type Strategy struct { // it makes sure that your grid configuration is profitable. FeeRate fixedpoint.Value `json:"feeRate"` - SkipSpreadCheck bool `json:"skipSpreadCheck"` + SkipSpreadCheck bool `json:"skipSpreadCheck"` + RecoverGridByScanningTrades bool `json:"recoverGridByScanningTrades"` GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"` Position *types.Position `persistence:"position"` @@ -1369,7 +1370,7 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context // 6. debug and send metrics debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) - s.updateGridNumOfOrdersMetrics() + s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil @@ -1910,7 +1911,7 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi if s.RecoverOrdersWhenStart { // do recover only when triggerPrice is not set and not in the back-test mode s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") - if err := s.recoverGridByScanningOrders(ctx, session); err != nil { + if err := s.recoverGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("recover error") } } @@ -1921,6 +1922,14 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi } } +func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { + if s.RecoverGridByScanningTrades { + return s.recoverGridByScanningTrades(ctx, session) + } + + return s.recoverGridByScanningOrders(ctx, session) +} + func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbgo.ExchangeSession) error { openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { @@ -2018,4 +2027,4 @@ func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (f } return v.Round(prec, fixedpoint.Up), prec -} \ No newline at end of file +} From ccf567fdab73507397d4f9a3e0897850ace98c0c Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 10 Mar 2023 00:46:12 +0800 Subject: [PATCH 0533/1392] grid2: add ClearDuplicatedPriceOpenOrders option --- pkg/strategy/grid2/strategy.go | 92 ++++++++++++++++++++++++++--- pkg/strategy/grid2/strategy_test.go | 73 ++++++++++++++++++----- 2 files changed, 142 insertions(+), 23 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 91cc533161..45ee953723 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -6,6 +6,7 @@ import ( "math" "sort" "strconv" + "strings" "sync" "time" @@ -134,6 +135,8 @@ type Strategy struct { ClearOpenOrdersIfMismatch bool `json:"clearOpenOrdersIfMismatch"` + ClearDuplicatedPriceOpenOrders bool `json:"clearDuplicatedPriceOpenOrders"` + // UseCancelAllOrdersApiWhenClose uses a different API to cancel all the orders on the market when closing a grid UseCancelAllOrdersApiWhenClose bool `json:"useCancelAllOrdersApiWhenClose"` @@ -1158,15 +1161,35 @@ func sortOrdersByPriceAscending(orders []types.Order) []types.Order { } func (s *Strategy) debugGridOrders(submitOrders []types.SubmitOrder, lastPrice fixedpoint.Value) { - s.logger.Infof("GRID ORDERS: [") + var sb strings.Builder + + sb.WriteString("GRID ORDERS [") for i, order := range submitOrders { if i > 0 && lastPrice.Compare(order.Price) >= 0 && lastPrice.Compare(submitOrders[i-1].Price) <= 0 { - s.logger.Infof(" - LAST PRICE: %f", lastPrice.Float64()) + sb.WriteString(fmt.Sprintf(" - LAST PRICE: %f", lastPrice.Float64())) } - s.logger.Info(" - ", order.String()) + sb.WriteString(" - " + order.String()) + } + sb.WriteString("] END OF GRID ORDERS") + + s.logger.Infof(sb.String()) +} + +func (s *Strategy) debugOrders(desc string, orders []types.Order) { + var sb strings.Builder + + if desc == "" { + desc = "ORDERS" } - s.logger.Infof("] END OF GRID ORDERS") + + sb.WriteString(desc + " [") + for i, order := range orders { + sb.WriteString(fmt.Sprintf(" - %d) %s", i, order.String())) + } + sb.WriteString("]") + + s.logger.Infof(sb.String()) } func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoint.Value) ([]types.SubmitOrder, error) { @@ -1884,6 +1907,13 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } } } + + if s.ClearDuplicatedPriceOpenOrders { + s.logger.Infof("clearDuplicatedPriceOpenOrders is set, finding duplicated open orders...") + if err := s.cancelDuplicatedPriceOpenOrders(ctx, session); err != nil { + s.logger.WithError(err).Errorf("cancelDuplicatedPriceOpenOrders error") + } + } }) // if TriggerPrice is zero, that means we need to open the grid when start up @@ -2020,11 +2050,55 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha return false, nil } -func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (fixedpoint.Value, int) { - prec := market.VolumePrecision - if c == market.QuoteCurrency { - prec = market.PricePrecision +func (s *Strategy) cancelDuplicatedPriceOpenOrders(ctx context.Context, session *bbgo.ExchangeSession) error { + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } + + if len(openOrders) == 0 { + return nil + } + + dupOrders := s.findDuplicatedPriceOpenOrders(openOrders) + + if len(dupOrders) > 0 { + s.debugOrders("DUPLICATED ORDERS", dupOrders) + return session.Exchange.CancelOrders(ctx, dupOrders...) + } + + s.logger.Infof("no duplicated order found") + return nil +} + +func (s *Strategy) findDuplicatedPriceOpenOrders(openOrders []types.Order) (dupOrders []types.Order) { + orderBook := bbgo.NewActiveOrderBook(s.Symbol) + for _, openOrder := range openOrders { + existingOrder := orderBook.Lookup(func(o types.Order) bool { + return o.Price.Compare(openOrder.Price) == 0 + }) + + if existingOrder != nil { + // found duplicated order + // compare creation time and remove the latest created order + // if the creation time equals, then we can just cancel one of them + s.debugOrders( + fmt.Sprintf("found duplicated order at price %s, comparing orders", openOrder.Price.String()), + []types.Order{*existingOrder, openOrder}) + + dupOrder := *existingOrder + if openOrder.CreationTime.After(existingOrder.CreationTime.Time()) { + dupOrder = openOrder + } else if openOrder.CreationTime.Before(existingOrder.CreationTime.Time()) { + // override the existing order and take the existing order as a duplicated one + orderBook.Add(openOrder) + } + + dupOrders = append(dupOrders, dupOrder) + } else { + orderBook.Add(openOrder) + } } - return v.Round(prec, fixedpoint.Up), prec + return dupOrders } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index c70a9eaecf..8d42273e69 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -355,11 +355,10 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { assert.NoError(t, err) assert.InDelta(t, 0.099992, quantity.Float64(), 0.0001) }) - } -func newTestStrategy() *Strategy { - market := types.Market{ +func newTestMarket() types.Market { + return types.Market{ BaseCurrency: "BTC", QuoteCurrency: "USDT", TickSize: number(0.01), @@ -368,6 +367,36 @@ func newTestStrategy() *Strategy { MinNotional: number(10.0), MinQuantity: number(0.001), } +} + +var testOrderID = uint64(0) + +func newTestOrder(price, quantity fixedpoint.Value, side types.SideType) types.Order { + market := newTestMarket() + testOrderID++ + return types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: side, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + AveragePrice: fixedpoint.Zero, + StopPrice: fixedpoint.Zero, + Market: market, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "binance", + GID: testOrderID, + OrderID: testOrderID, + Status: types.OrderStatusNew, + ExecutedQuantity: fixedpoint.Zero, + IsWorking: true, + } +} + +func newTestStrategy() *Strategy { + market := newTestMarket() s := &Strategy{ logger: logrus.NewEntry(logrus.New()), @@ -500,6 +529,33 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) { assert.Equal(t, "0.01", baseFee.String()) } +func TestStrategy_findDuplicatedPriceOpenOrders(t *testing.T) { + t.Run("no duplicated open orders", func(t *testing.T) { + s := newTestStrategy() + s.grid = s.newGrid() + + dupOrders := s.findDuplicatedPriceOpenOrders([]types.Order{ + newTestOrder(number(1900.0), number(0.1), types.SideTypeSell), + newTestOrder(number(1800.0), number(0.1), types.SideTypeSell), + newTestOrder(number(1700.0), number(0.1), types.SideTypeSell), + }) + assert.Empty(t, dupOrders) + assert.Len(t, dupOrders, 0) + }) + + t.Run("1 duplicated open order SELL", func(t *testing.T) { + s := newTestStrategy() + s.grid = s.newGrid() + + dupOrders := s.findDuplicatedPriceOpenOrders([]types.Order{ + newTestOrder(number(1900.0), number(0.1), types.SideTypeSell), + newTestOrder(number(1900.0), number(0.1), types.SideTypeSell), + newTestOrder(number(1800.0), number(0.1), types.SideTypeSell), + }) + assert.Len(t, dupOrders, 1) + }) +} + func TestStrategy_handleOrderFilled(t *testing.T) { ctx := context.Background() @@ -971,17 +1027,6 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { }) } -func Test_roundUpMarketQuantity(t *testing.T) { - q := number("0.00000003") - assert.Equal(t, "0.00000003", q.String()) - - q3, prec := roundUpMarketQuantity(types.Market{ - VolumePrecision: 8, - }, q, "BTC") - assert.Equal(t, "0.00000003", q3.String(), "rounding prec 8") - assert.Equal(t, 8, prec) -} - func Test_buildPinOrderMap(t *testing.T) { assert := assert.New(t) s := newTestStrategy() From 64e0a169e9c7b914a092f2af637d9e17323d7350 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 10 Mar 2023 00:50:25 +0800 Subject: [PATCH 0534/1392] grid2: add debug option --- pkg/strategy/grid2/strategy.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 45ee953723..27837a2c00 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -163,6 +163,9 @@ type Strategy struct { SkipSpreadCheck bool `json:"skipSpreadCheck"` RecoverGridByScanningTrades bool `json:"recoverGridByScanningTrades"` + // Debug enables the debug mode + Debug bool `json:"debug"` + GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"` Position *types.Position `persistence:"position"` @@ -1161,6 +1164,10 @@ func sortOrdersByPriceAscending(orders []types.Order) []types.Order { } func (s *Strategy) debugGridOrders(submitOrders []types.SubmitOrder, lastPrice fixedpoint.Value) { + if !s.Debug { + return + } + var sb strings.Builder sb.WriteString("GRID ORDERS [") @@ -1177,6 +1184,10 @@ func (s *Strategy) debugGridOrders(submitOrders []types.SubmitOrder, lastPrice f } func (s *Strategy) debugOrders(desc string, orders []types.Order) { + if !s.Debug { + return + } + var sb strings.Builder if desc == "" { From f093c73457200986560f067db58627d6dab6941c Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 10 Mar 2023 13:00:13 +0800 Subject: [PATCH 0535/1392] grid2: add queryOpenOrdersUntilSuccessful func --- pkg/strategy/grid2/strategy.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 27837a2c00..849804f60e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -2062,7 +2062,7 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha } func (s *Strategy) cancelDuplicatedPriceOpenOrders(ctx context.Context, session *bbgo.ExchangeSession) error { - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + openOrders, err := queryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) if err != nil { return err } @@ -2113,3 +2113,12 @@ func (s *Strategy) findDuplicatedPriceOpenOrders(openOrders []types.Order) (dupO return dupOrders } + +func queryOpenOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, symbol string) (openOrders []types.Order, err error) { + var op = func() (err2 error) { + openOrders, err2 = ex.QueryOpenOrders(ctx, symbol) + return err2 + } + err = backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) + return openOrders, err +} From 89abbeb2d1c2da9e015bbd10cacf1410742eb1f1 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 10 Mar 2023 13:01:19 +0800 Subject: [PATCH 0536/1392] grid2: add context to backoffs --- pkg/strategy/grid2/strategy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 849804f60e..60bd1058e7 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -2119,6 +2119,10 @@ func queryOpenOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, symb openOrders, err2 = ex.QueryOpenOrders(ctx, symbol) return err2 } - err = backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) + err = backoff.Retry(op, backoff.WithContext( + backoff.WithMaxRetries( + backoff.NewExponentialBackOff(), + 101), + ctx)) return openOrders, err } From df6e58d6542b2a56514bcf1bdd50fd1cea95cf13 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 10 Mar 2023 13:08:29 +0800 Subject: [PATCH 0537/1392] grid2: replace all openOrders query to queryOpenOrdersUntilSuccessful --- pkg/strategy/grid2/strategy.go | 47 +++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 60bd1058e7..fb697b6ff4 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -897,12 +897,8 @@ func (s *Strategy) cancelAll(ctx context.Context) error { for { s.logger.Infof("checking %s open orders...", s.Symbol) - var openOrders []types.Order - if err := backoff.Retry(func() error { - var err error - openOrders, err = session.Exchange.QueryOpenOrders(ctx, s.Symbol) - return err - }, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)); err != nil { + openOrders, err := queryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) + if err != nil { s.logger.WithError(err).Errorf("CancelOrdersByGroupID api call error") werr = multierr.Append(werr, err) } @@ -919,8 +915,7 @@ func (s *Strategy) cancelAll(ctx context.Context) error { return cancelErr } - err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)) - if err != nil { + if err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)); err != nil { s.logger.WithError(err).Errorf("CancelAllOrders api call error") werr = multierr.Append(werr, err) } @@ -1306,17 +1301,12 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin func (s *Strategy) clearOpenOrders(ctx context.Context, session *bbgo.ExchangeSession) error { // clear open orders when start - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) - if err != nil { - return err - } - - err = session.Exchange.CancelOrders(ctx, openOrders...) + openOrders, err := queryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) if err != nil { return err } - return nil + return cancelOrdersUntilSuccessful(ctx, session.Exchange, openOrders...) } func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.ExchangeSession) (fixedpoint.Value, error) { @@ -1972,7 +1962,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio } func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbgo.ExchangeSession) error { - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + openOrders, err := queryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) if err != nil { return err } @@ -2114,15 +2104,30 @@ func (s *Strategy) findDuplicatedPriceOpenOrders(openOrders []types.Order) (dupO return dupOrders } -func queryOpenOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, symbol string) (openOrders []types.Order, err error) { - var op = func() (err2 error) { - openOrders, err2 = ex.QueryOpenOrders(ctx, symbol) - return err2 - } +func generalBackoff(ctx context.Context, op backoff.Operation) (err error) { err = backoff.Retry(op, backoff.WithContext( backoff.WithMaxRetries( backoff.NewExponentialBackOff(), 101), ctx)) + return err +} + +func cancelOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, orders ...types.Order) error { + var op = func() (err2 error) { + err2 = ex.CancelOrders(ctx, orders...) + return err2 + } + + return generalBackoff(ctx, op) +} + +func queryOpenOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, symbol string) (openOrders []types.Order, err error) { + var op = func() (err2 error) { + openOrders, err2 = ex.QueryOpenOrders(ctx, symbol) + return err2 + } + + err = generalBackoff(ctx, op) return openOrders, err } From d51a802315e626ed0c0b05adec5731abf9982c04 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 10 Mar 2023 13:51:29 +0800 Subject: [PATCH 0538/1392] fix/scale: fix typo and add some more tests --- pkg/bbgo/scale.go | 2 +- pkg/bbgo/scale_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/scale.go b/pkg/bbgo/scale.go index e4d36133d0..da5a5bc52a 100644 --- a/pkg/bbgo/scale.go +++ b/pkg/bbgo/scale.go @@ -151,7 +151,7 @@ func (s *LinearScale) Call(x float64) (y float64) { if x <= s.Domain[0] { return s.Range[0] } else if x >= s.Domain[1] { - return s.Range[0] + return s.Range[1] } y = s.Range[0] + (x-s.Domain[0])*s.a diff --git a/pkg/bbgo/scale_test.go b/pkg/bbgo/scale_test.go index 3b46c2c7a1..1ad86a0c4d 100644 --- a/pkg/bbgo/scale_test.go +++ b/pkg/bbgo/scale_test.go @@ -75,6 +75,7 @@ func TestLinearScale(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "f(x) = 3.000000 + (x - 1000.000000) * 0.007000", scale.String()) assert.InDelta(t, 3, scale.Call(1000), delta) + assert.InDelta(t, 6.5, scale.Call(1500), delta) assert.InDelta(t, 10, scale.Call(2000), delta) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) @@ -92,6 +93,7 @@ func TestLinearScale2(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "f(x) = 0.100000 + (x - 1.000000) * 0.150000", scale.String()) assert.InDelta(t, 0.1, scale.Call(1), delta) + assert.InDelta(t, 0.25, scale.Call(2), delta) assert.InDelta(t, 0.4, scale.Call(3), delta) } @@ -105,6 +107,7 @@ func TestLinearScaleNegative(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "f(x) = 0.100000 + (x - -1.000000) * 0.075000", scale.String()) assert.InDelta(t, 0.1, scale.Call(-1), delta) + assert.InDelta(t, 0.25, scale.Call(1), delta) assert.InDelta(t, 0.4, scale.Call(3), delta) } From 36f48bc604b55b34eb18b05effdd1e03dd65a1eb Mon Sep 17 00:00:00 2001 From: chiahung Date: Fri, 10 Mar 2023 15:27:50 +0800 Subject: [PATCH 0539/1392] FIX: fix format string float point issue --- pkg/fixedpoint/convert.go | 47 +++++++++++++++++++++++++-- pkg/fixedpoint/convert_test.go | 59 ++++++++++++++++++++++++++++++++++ pkg/types/market.go | 2 +- 3 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 pkg/fixedpoint/convert_test.go diff --git a/pkg/fixedpoint/convert.go b/pkg/fixedpoint/convert.go index b4023c555f..89bf5bd271 100644 --- a/pkg/fixedpoint/convert.go +++ b/pkg/fixedpoint/convert.go @@ -115,9 +115,50 @@ func (v Value) FormatString(prec int) string { } else if v == NegInf { return "-inf" } - pow := math.Pow10(prec) - return strconv.FormatFloat( - math.Trunc(float64(v)/DefaultPow*pow)/pow, 'f', prec, 64) + + u := int64(v) + + // trunc precision + precDiff := DefaultPrecision - prec + if precDiff > 0 { + powDiff := int64(math.Pow10(precDiff)) + u = int64(v) / powDiff * powDiff + } + + // check sign + sign := Value(u).Sign() + + basePow := int64(DefaultPow) + a := u / basePow + b := u % basePow + + if a < 0 { + a = -a + } + + if b < 0 { + b = -b + } + + str := strconv.FormatInt(a, 10) + if prec > 0 { + bStr := fmt.Sprintf(".%08d", b) + if prec <= DefaultPrecision { + bStr = bStr[0 : prec+1] + } else { + for i := prec - DefaultPrecision; i > 0; i-- { + bStr += "0" + } + } + + str += bStr + } + + if sign < 0 { + str = "-" + str + } + + return str } func (v Value) Percentage() string { diff --git a/pkg/fixedpoint/convert_test.go b/pkg/fixedpoint/convert_test.go new file mode 100644 index 0000000000..06a9d58486 --- /dev/null +++ b/pkg/fixedpoint/convert_test.go @@ -0,0 +1,59 @@ +package fixedpoint + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_FormatString(t *testing.T) { + assert := assert.New(t) + + t.Run("Value(57000000) with prec = 5, expected 0.57", func(t *testing.T) { + v := Value(57000000) + s := v.FormatString(5) + assert.Equal("0.57000", s) + }) + + t.Run("Value(57123456) with prec = 5, expected 0.57123", func(t *testing.T) { + v := Value(57123456) + s := v.FormatString(5) + assert.Equal("0.57123", s) + }) + + t.Run("Value(123456789) with prec = 9, expected 1.23456789", func(t *testing.T) { + v := Value(123456789) + s := v.FormatString(9) + assert.Equal("1.234567890", s) + }) + + t.Run("Value(102345678) with prec = 9, expected 1.02345678", func(t *testing.T) { + v := Value(102345678) + s := v.FormatString(9) + assert.Equal("1.023456780", s) + }) + + t.Run("Value(-57000000) with prec = 5, expected -0.57", func(t *testing.T) { + v := Value(-57000000) + s := v.FormatString(5) + assert.Equal("-0.57000", s) + }) + + t.Run("Value(-123456789) with prec = 9, expected 1.23456789", func(t *testing.T) { + v := Value(-123456789) + s := v.FormatString(9) + assert.Equal("-1.234567890", s) + }) + + t.Run("Value(1234567890) with prec = -1, expected 10", func(t *testing.T) { + v := Value(1234567890) + s := v.FormatString(-1) + assert.Equal("10", s) + }) + + t.Run("Value(-1234) with prec = 3, expected = 0.000", func(t *testing.T) { + v := Value(-1234) + s := v.FormatString(3) + assert.Equal("0.000", s) + }) +} diff --git a/pkg/types/market.go b/pkg/types/market.go index 6cc9466ff8..b7a44db924 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -113,7 +113,7 @@ func (m Market) FormatPrice(val fixedpoint.Value) string { } func FormatPrice(price fixedpoint.Value, tickSize fixedpoint.Value) string { - prec := int(math.Round(math.Abs(math.Log10(tickSize.Float64())))) + prec := int(math.Round(math.Log10(tickSize.Float64()) * -1.0)) return price.FormatString(prec) } From fd2032b825167d1292ee88ef59a03e1c268c9e69 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Fri, 10 Mar 2023 15:33:19 +0800 Subject: [PATCH 0540/1392] FIX: [grid2] avoid handling one orderID twice --- pkg/strategy/grid2/strategy.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index fb697b6ff4..72c5bb174f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -186,6 +186,9 @@ type Strategy struct { gridClosedCallbacks []func() gridErrorCallbacks []func(err error) + // filledOrderIDMap is used to prevent processing the same order ID twice. + filledOrderIDMap *types.SyncOrderMap + // mu is used for locking the grid object field, avoid double grid opening mu sync.Mutex @@ -240,6 +243,7 @@ func (s *Strategy) Defaults() error { } func (s *Strategy) Initialize() error { + s.filledOrderIDMap = types.NewSyncOrderMap() s.logger = log.WithFields(s.LogFields) return nil } @@ -505,8 +509,8 @@ func (s *Strategy) processFilledOrder(o types.Order) { // we calculate profit only when the order is placed successfully if profit != nil { - s.logger.Infof("GENERATED GRID PROFIT: %+v", profit) s.GridProfitStats.AddProfit(profit) + s.logger.Infof("GENERATED GRID PROFIT: %+v; TOTAL GRID PROFIT BECOMES: %f", profit, s.GridProfitStats.TotalQuoteProfit.Float64()) s.EmitGridProfit(s.GridProfitStats, profit) } } @@ -518,6 +522,12 @@ func (s *Strategy) handleOrderFilled(o types.Order) { return } + if s.filledOrderIDMap.Exists(o.OrderID) { + s.logger.Warn("duplicated id (%d) of filled order detected", o.OrderID) + return + } + s.filledOrderIDMap.Add(o) + s.logger.Infof("GRID ORDER FILLED: %s", o.String()) s.updateFilledOrderMetrics(o) s.processFilledOrder(o) @@ -2130,4 +2140,4 @@ func queryOpenOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, symb err = generalBackoff(ctx, op) return openOrders, err -} +} \ No newline at end of file From a7cfd488ed3f12166093041e0eed8ae26a5d2332 Mon Sep 17 00:00:00 2001 From: narumi Date: Fri, 10 Mar 2023 16:29:49 +0800 Subject: [PATCH 0541/1392] add fixedmaker --- config/fixedmaker.yaml | 21 +++ pkg/strategy/fixedmaker/strategy.go | 214 ++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 config/fixedmaker.yaml create mode 100644 pkg/strategy/fixedmaker/strategy.go diff --git a/config/fixedmaker.yaml b/config/fixedmaker.yaml new file mode 100644 index 0000000000..6869c6086f --- /dev/null +++ b/config/fixedmaker.yaml @@ -0,0 +1,21 @@ +--- +exchangeStrategies: + - on: max + fixedmm: + interval: 5m + symbol: BTCUSDT + halfSpreadRatio: 0.05% + quantity: 0.005 + dryRun: false + + - on: max + rebalance: + interval: 1h + quoteCurrency: USDT + targetWeights: + BTC: 50% + USDT: 50% + threshold: 2% + maxAmount: 200 # max amount to buy or sell per order + orderType: LIMIT_MAKER # LIMIT, LIMIT_MAKER or MARKET + dryRun: false diff --git a/pkg/strategy/fixedmaker/strategy.go b/pkg/strategy/fixedmaker/strategy.go new file mode 100644 index 0000000000..60ca5e82cd --- /dev/null +++ b/pkg/strategy/fixedmaker/strategy.go @@ -0,0 +1,214 @@ +package fixedmaker + +import ( + "context" + "fmt" + "sync" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "fixedmaker" + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +// Fixed spread market making strategy +type Strategy struct { + Environment *bbgo.Environment + StandardIndicatorSet *bbgo.StandardIndicatorSet + Market types.Market + + Interval types.Interval `json:"interval"` + Symbol string `json:"symbol"` + Quantity fixedpoint.Value `json:"quantity"` + halfSpread fixedpoint.Value `json:"halfSpreadRatio"` + DryRun bool `json:"dryRun"` + + // persistence fields + Position *types.Position `json:"position,omitempty" persistence:"position"` + ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + activeOrderBook *bbgo.ActiveOrderBook +} + +func (s *Strategy) Initialize() error { + return nil +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Validate() error { + if s.Quantity.Float64() <= 0 { + return fmt.Errorf("quantity should be positive") + } + + if s.halfSpread.Float64() <= 0 { + return fmt.Errorf("halfSpreadRatio should be positive") + } + return nil +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.session = session + + s.activeOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + s.activeOrderBook.BindStream(session.UserDataStream) + + instanceID := s.InstanceID() + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + // Always update the position fields + s.Position.Strategy = ID + s.Position.StrategyInstanceID = instanceID + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + + s.orderExecutor.BindProfitStats(s.ProfitStats) + + s.orderExecutor.Bind() + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + + session.UserDataStream.OnStart(func() { + // you can place orders here when bbgo is started, this will be called only once. + }) + + s.activeOrderBook.OnFilled(func(order types.Order) { + if s.activeOrderBook.NumOfOrders() == 0 { + log.Infof("no active orders, replenish") + s.replenish(ctx) + } + }) + + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + log.Infof("%+v", kline) + + s.cancelOrders(ctx) + s.replenish(ctx) + }) + + // the shutdown handler, you can cancel all orders + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + return nil +} + +func (s *Strategy) cancelOrders(ctx context.Context) { + if err := s.session.Exchange.CancelOrders(ctx, s.activeOrderBook.Orders()...); err != nil { + log.WithError(err).Errorf("failed to cancel orders") + } +} + +func (s *Strategy) replenish(ctx context.Context) { + submitOrders, err := s.generateSubmitOrders(ctx) + if err != nil { + log.WithError(err).Error("failed to generate submit orders") + return + } + log.Infof("submit orders: %+v", submitOrders) + + if s.DryRun { + log.Infof("dry run, not submitting orders") + return + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, submitOrders...) + if err != nil { + log.WithError(err).Error("failed to submit orders") + return + } + log.Infof("created orders: %+v", createdOrders) + + s.activeOrderBook.Add(createdOrders...) +} + +func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrder, error) { + baseBalance, ok := s.session.GetAccount().Balance(s.Market.BaseCurrency) + if !ok { + return nil, fmt.Errorf("base currency %s balance not found", s.Market.BaseCurrency) + } + log.Infof("base balance: %+v", baseBalance) + + quoteBalance, ok := s.session.GetAccount().Balance(s.Market.QuoteCurrency) + if !ok { + return nil, fmt.Errorf("quote currency %s balance not found", s.Market.QuoteCurrency) + } + log.Infof("quote balance: %+v", quoteBalance) + + ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + return nil, err + } + midPrice := ticker.Buy.Add(ticker.Sell).Div(fixedpoint.NewFromFloat(2.0)) + log.Infof("mid price: %+v", midPrice) + + orders := []types.SubmitOrder{} + + // calculate buy and sell price + // buy price = mid price * (1 - r) + buyPrice := midPrice.Mul(fixedpoint.One.Sub(s.halfSpread)) + log.Infof("buy price: %+v", buyPrice) + // sell price = mid price * (1 + r) + sellPrice := midPrice.Mul(fixedpoint.One.Add(s.halfSpread)) + log.Infof("sell price: %+v", sellPrice) + + // check balance and generate orders + amount := s.Quantity.Mul(buyPrice) + if quoteBalance.Available.Compare(amount) > 0 { + orders = append(orders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Price: buyPrice, + Quantity: s.Quantity, + }) + } else { + log.Infof("not enough quote balance to buy, available: %s, amount: %s", quoteBalance.Available, amount) + } + + if baseBalance.Available.Compare(s.Quantity) > 0 { + orders = append(orders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Price: sellPrice, + Quantity: s.Quantity, + }) + } else { + log.Infof("not enough base balance to sell, available: %s, quantity: %s", baseBalance.Available, s.Quantity) + } + + return orders, nil +} From c6609927f2aabc41f67f42e709b7fc08f7595a93 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 10 Mar 2023 17:00:09 +0800 Subject: [PATCH 0542/1392] grid2: fix Warn by using Warnf --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 72c5bb174f..afdff36e4f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -523,7 +523,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) { } if s.filledOrderIDMap.Exists(o.OrderID) { - s.logger.Warn("duplicated id (%d) of filled order detected", o.OrderID) + s.logger.Warnf("duplicated id (%d) of filled order detected", o.OrderID) return } s.filledOrderIDMap.Add(o) @@ -2140,4 +2140,4 @@ func queryOpenOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, symb err = generalBackoff(ctx, op) return openOrders, err -} \ No newline at end of file +} From 3eae532e1369d25021de1a8c27f009a69faee2d6 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 10 Mar 2023 17:11:51 +0800 Subject: [PATCH 0543/1392] grid2: init filledOrderIDMap for tests --- pkg/strategy/grid2/strategy_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 8d42273e69..1801419ffd 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -408,6 +408,8 @@ func newTestStrategy() *Strategy { GridNum: 11, historicalTrades: bbgo.NewTradeStore(), + filledOrderIDMap: types.NewSyncOrderMap(), + // QuoteInvestment: number(9000.0), } return s From 291a6f273aae9ced23c5ccc9eb9f6f4b2381f0cd Mon Sep 17 00:00:00 2001 From: chiahung Date: Fri, 10 Mar 2023 17:03:39 +0800 Subject: [PATCH 0544/1392] fix test error --- pkg/fixedpoint/convert.go | 2 +- pkg/fixedpoint/convert_test.go | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/fixedpoint/convert.go b/pkg/fixedpoint/convert.go index 89bf5bd271..6600185a6f 100644 --- a/pkg/fixedpoint/convert.go +++ b/pkg/fixedpoint/convert.go @@ -121,7 +121,7 @@ func (v Value) FormatString(prec int) string { // trunc precision precDiff := DefaultPrecision - prec if precDiff > 0 { - powDiff := int64(math.Pow10(precDiff)) + powDiff := int64(math.Round(math.Pow10(precDiff))) u = int64(v) / powDiff * powDiff } diff --git a/pkg/fixedpoint/convert_test.go b/pkg/fixedpoint/convert_test.go index 06a9d58486..be842a6b56 100644 --- a/pkg/fixedpoint/convert_test.go +++ b/pkg/fixedpoint/convert_test.go @@ -10,49 +10,49 @@ func Test_FormatString(t *testing.T) { assert := assert.New(t) t.Run("Value(57000000) with prec = 5, expected 0.57", func(t *testing.T) { - v := Value(57000000) + v := MustNewFromString("0.57") s := v.FormatString(5) assert.Equal("0.57000", s) }) t.Run("Value(57123456) with prec = 5, expected 0.57123", func(t *testing.T) { - v := Value(57123456) + v := MustNewFromString("0.57123456") s := v.FormatString(5) assert.Equal("0.57123", s) }) t.Run("Value(123456789) with prec = 9, expected 1.23456789", func(t *testing.T) { - v := Value(123456789) + v := MustNewFromString("1.23456789") s := v.FormatString(9) assert.Equal("1.234567890", s) }) t.Run("Value(102345678) with prec = 9, expected 1.02345678", func(t *testing.T) { - v := Value(102345678) + v := MustNewFromString("1.02345678") s := v.FormatString(9) assert.Equal("1.023456780", s) }) t.Run("Value(-57000000) with prec = 5, expected -0.57", func(t *testing.T) { - v := Value(-57000000) + v := MustNewFromString("-0.57") s := v.FormatString(5) assert.Equal("-0.57000", s) }) t.Run("Value(-123456789) with prec = 9, expected 1.23456789", func(t *testing.T) { - v := Value(-123456789) + v := MustNewFromString("-1.23456789") s := v.FormatString(9) assert.Equal("-1.234567890", s) }) t.Run("Value(1234567890) with prec = -1, expected 10", func(t *testing.T) { - v := Value(1234567890) + v := MustNewFromString("12.3456789") s := v.FormatString(-1) assert.Equal("10", s) }) t.Run("Value(-1234) with prec = 3, expected = 0.000", func(t *testing.T) { - v := Value(-1234) + v := MustNewFromString("-0.00001234") s := v.FormatString(3) assert.Equal("0.000", s) }) From 8c9ed0538f92ac0b135958b3c3cde44625e80e2c Mon Sep 17 00:00:00 2001 From: chiahung Date: Fri, 10 Mar 2023 17:55:55 +0800 Subject: [PATCH 0545/1392] add more test case --- pkg/fixedpoint/convert_test.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pkg/fixedpoint/convert_test.go b/pkg/fixedpoint/convert_test.go index be842a6b56..634e408da4 100644 --- a/pkg/fixedpoint/convert_test.go +++ b/pkg/fixedpoint/convert_test.go @@ -9,51 +9,57 @@ import ( func Test_FormatString(t *testing.T) { assert := assert.New(t) - t.Run("Value(57000000) with prec = 5, expected 0.57", func(t *testing.T) { + t.Run("0.57 with prec = 5, expected 0.57", func(t *testing.T) { v := MustNewFromString("0.57") s := v.FormatString(5) assert.Equal("0.57000", s) }) - t.Run("Value(57123456) with prec = 5, expected 0.57123", func(t *testing.T) { + t.Run("0.57123456 with prec = 5, expected 0.57123", func(t *testing.T) { v := MustNewFromString("0.57123456") s := v.FormatString(5) assert.Equal("0.57123", s) }) - t.Run("Value(123456789) with prec = 9, expected 1.23456789", func(t *testing.T) { + t.Run("1.23456789 with prec = 9, expected 1.23456789", func(t *testing.T) { v := MustNewFromString("1.23456789") s := v.FormatString(9) assert.Equal("1.234567890", s) }) - t.Run("Value(102345678) with prec = 9, expected 1.02345678", func(t *testing.T) { + t.Run("1.02345678 with prec = 9, expected 1.02345678", func(t *testing.T) { v := MustNewFromString("1.02345678") s := v.FormatString(9) assert.Equal("1.023456780", s) }) - t.Run("Value(-57000000) with prec = 5, expected -0.57", func(t *testing.T) { + t.Run("-0.57 with prec = 5, expected -0.57", func(t *testing.T) { v := MustNewFromString("-0.57") s := v.FormatString(5) assert.Equal("-0.57000", s) }) - t.Run("Value(-123456789) with prec = 9, expected 1.23456789", func(t *testing.T) { + t.Run("-1.23456789 with prec = 9, expected 1.23456789", func(t *testing.T) { v := MustNewFromString("-1.23456789") s := v.FormatString(9) assert.Equal("-1.234567890", s) }) - t.Run("Value(1234567890) with prec = -1, expected 10", func(t *testing.T) { + t.Run("12.3456789 with prec = -1, expected 10", func(t *testing.T) { v := MustNewFromString("12.3456789") s := v.FormatString(-1) assert.Equal("10", s) }) - t.Run("Value(-1234) with prec = 3, expected = 0.000", func(t *testing.T) { + t.Run("-0.00001234 with prec = 3, expected = 0.000", func(t *testing.T) { v := MustNewFromString("-0.00001234") s := v.FormatString(3) assert.Equal("0.000", s) }) + + t.Run("12.3456789 with prec = -3, expected = 0", func(t *testing.T) { + v := MustNewFromString("12.3456789") + s := v.FormatString(-2) + assert.Equal("0", s) + }) } From 16b30960ccfc12561f49ef2e78a0bd818153c620 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Fri, 10 Mar 2023 15:47:42 +0800 Subject: [PATCH 0546/1392] FIX: [grid2] specify client order id explicitly --- pkg/strategy/grid2/strategy.go | 79 ++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index afdff36e4f..e8889ee9f0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -11,6 +11,7 @@ import ( "time" "github.com/cenkalti/backoff/v4" + "github.com/google/uuid" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" @@ -485,15 +486,16 @@ func (s *Strategy) processFilledOrder(o types.Order) { } orderForm := types.SubmitOrder{ - Symbol: s.Symbol, - Market: s.Market, - Type: types.OrderTypeLimit, - Price: newPrice, - Side: newSide, - TimeInForce: types.TimeInForceGTC, - Quantity: newQuantity, - Tag: orderTag, - GroupID: s.OrderGroupID, + Symbol: s.Symbol, + Market: s.Market, + Type: types.OrderTypeLimit, + Price: newPrice, + Side: newSide, + TimeInForce: types.TimeInForceGTC, + Quantity: newQuantity, + Tag: orderTag, + GroupID: s.OrderGroupID, + ClientOrderID: uuid.New().String(), } s.logger.Infof("SUBMIT GRID REVERSE ORDER: %s", orderForm.String()) @@ -1242,15 +1244,16 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin if usedBase.Add(quantity).Compare(totalBase) < 0 { submitOrders = append(submitOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimit, - Side: types.SideTypeSell, - Price: sellPrice, - Quantity: quantity, - Market: s.Market, - TimeInForce: types.TimeInForceGTC, - Tag: orderTag, - GroupID: s.OrderGroupID, + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeSell, + Price: sellPrice, + Quantity: quantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: orderTag, + GroupID: s.OrderGroupID, + ClientOrderID: uuid.New().String(), }) usedBase = usedBase.Add(quantity) } else { @@ -1259,15 +1262,16 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin nextPin := pins[i-1] nextPrice := fixedpoint.Value(nextPin) submitOrders = append(submitOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimit, - Side: types.SideTypeBuy, - Price: nextPrice, - Quantity: quantity, - Market: s.Market, - TimeInForce: types.TimeInForceGTC, - Tag: orderTag, - GroupID: s.OrderGroupID, + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeBuy, + Price: nextPrice, + Quantity: quantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: orderTag, + GroupID: s.OrderGroupID, + ClientOrderID: uuid.New().String(), }) quoteQuantity := quantity.Mul(nextPrice) usedQuote = usedQuote.Add(quoteQuantity) @@ -1292,15 +1296,16 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin } submitOrders = append(submitOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimit, - Side: types.SideTypeBuy, - Price: price, - Quantity: quantity, - Market: s.Market, - TimeInForce: types.TimeInForceGTC, - Tag: orderTag, - GroupID: s.OrderGroupID, + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeBuy, + Price: price, + Quantity: quantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: orderTag, + GroupID: s.OrderGroupID, + ClientOrderID: uuid.New().String(), }) usedQuote = usedQuote.Add(quoteQuantity) } @@ -2140,4 +2145,4 @@ func queryOpenOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, symb err = generalBackoff(ctx, op) return openOrders, err -} +} \ No newline at end of file From 74656e0e49f2827475d205ffe5d1cdcdf4e8cfd5 Mon Sep 17 00:00:00 2001 From: narumi Date: Fri, 10 Mar 2023 17:39:31 +0800 Subject: [PATCH 0547/1392] fix fixedmaker errors --- config/fixedmaker.yaml | 2 +- pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/fixedmaker/strategy.go | 27 +++++++++++++++++---------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/config/fixedmaker.yaml b/config/fixedmaker.yaml index 6869c6086f..cdd474c026 100644 --- a/config/fixedmaker.yaml +++ b/config/fixedmaker.yaml @@ -1,7 +1,7 @@ --- exchangeStrategies: - on: max - fixedmm: + fixedmaker: interval: 5m symbol: BTCUSDT halfSpreadRatio: 0.05% diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 0b4cab10f6..edfdce4751 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -13,6 +13,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/etf" _ "github.com/c9s/bbgo/pkg/strategy/ewoDgtrd" _ "github.com/c9s/bbgo/pkg/strategy/factorzoo" + _ "github.com/c9s/bbgo/pkg/strategy/fixedmaker" _ "github.com/c9s/bbgo/pkg/strategy/flashcrash" _ "github.com/c9s/bbgo/pkg/strategy/fmaker" _ "github.com/c9s/bbgo/pkg/strategy/funding" diff --git a/pkg/strategy/fixedmaker/strategy.go b/pkg/strategy/fixedmaker/strategy.go index 60ca5e82cd..b409721924 100644 --- a/pkg/strategy/fixedmaker/strategy.go +++ b/pkg/strategy/fixedmaker/strategy.go @@ -26,11 +26,12 @@ type Strategy struct { StandardIndicatorSet *bbgo.StandardIndicatorSet Market types.Market - Interval types.Interval `json:"interval"` - Symbol string `json:"symbol"` - Quantity fixedpoint.Value `json:"quantity"` - halfSpread fixedpoint.Value `json:"halfSpreadRatio"` - DryRun bool `json:"dryRun"` + Interval types.Interval `json:"interval"` + Symbol string `json:"symbol"` + Quantity fixedpoint.Value `json:"quantity"` + HalfSpreadRatio fixedpoint.Value `json:"halfSpreadRatio"` + OrderType types.OrderType `json:"orderType"` + DryRun bool `json:"dryRun"` // persistence fields Position *types.Position `json:"position,omitempty" persistence:"position"` @@ -41,6 +42,12 @@ type Strategy struct { activeOrderBook *bbgo.ActiveOrderBook } +func (s *Strategy) Defaults() error { + if s.OrderType == "" { + s.OrderType = types.OrderTypeLimitMaker + } + return nil +} func (s *Strategy) Initialize() error { return nil } @@ -58,7 +65,7 @@ func (s *Strategy) Validate() error { return fmt.Errorf("quantity should be positive") } - if s.halfSpread.Float64() <= 0 { + if s.HalfSpreadRatio.Float64() <= 0 { return fmt.Errorf("halfSpreadRatio should be positive") } return nil @@ -178,10 +185,10 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrde // calculate buy and sell price // buy price = mid price * (1 - r) - buyPrice := midPrice.Mul(fixedpoint.One.Sub(s.halfSpread)) + buyPrice := midPrice.Mul(fixedpoint.One.Sub(s.HalfSpreadRatio)) log.Infof("buy price: %+v", buyPrice) // sell price = mid price * (1 + r) - sellPrice := midPrice.Mul(fixedpoint.One.Add(s.halfSpread)) + sellPrice := midPrice.Mul(fixedpoint.One.Add(s.HalfSpreadRatio)) log.Infof("sell price: %+v", sellPrice) // check balance and generate orders @@ -190,7 +197,7 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrde orders = append(orders, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, - Type: types.OrderTypeLimitMaker, + Type: s.OrderType, Price: buyPrice, Quantity: s.Quantity, }) @@ -202,7 +209,7 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrde orders = append(orders, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, + Type: s.OrderType, Price: sellPrice, Quantity: s.Quantity, }) From b050ae40986ce4585388c6fce2d5efd70054cba6 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 11 Mar 2023 15:31:09 +0800 Subject: [PATCH 0548/1392] grid2: fix log format --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index afdff36e4f..41e9b14294 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1178,10 +1178,10 @@ func (s *Strategy) debugGridOrders(submitOrders []types.SubmitOrder, lastPrice f sb.WriteString("GRID ORDERS [") for i, order := range submitOrders { if i > 0 && lastPrice.Compare(order.Price) >= 0 && lastPrice.Compare(submitOrders[i-1].Price) <= 0 { - sb.WriteString(fmt.Sprintf(" - LAST PRICE: %f", lastPrice.Float64())) + sb.WriteString(fmt.Sprintf(" - LAST PRICE: %f\n", lastPrice.Float64())) } - sb.WriteString(" - " + order.String()) + sb.WriteString(" - " + order.String() + "\n") } sb.WriteString("] END OF GRID ORDERS") @@ -1201,7 +1201,7 @@ func (s *Strategy) debugOrders(desc string, orders []types.Order) { sb.WriteString(desc + " [") for i, order := range orders { - sb.WriteString(fmt.Sprintf(" - %d) %s", i, order.String())) + sb.WriteString(fmt.Sprintf(" - %d) %s\n", i, order.String())) } sb.WriteString("]") From 4559a35f31dff11d7ccd9e2ae84e48e7bc538c2c Mon Sep 17 00:00:00 2001 From: narumi Date: Sun, 12 Mar 2023 23:48:26 +0800 Subject: [PATCH 0549/1392] graceful cancel in rebalance strategy --- pkg/strategy/rebalance/order_executor_map.go | 76 +++++++++++++++++++ pkg/strategy/rebalance/position_map.go | 20 +++++ pkg/strategy/rebalance/profit_stats_map.go | 15 ++++ pkg/strategy/rebalance/strategy.go | 77 +++++++------------- 4 files changed, 137 insertions(+), 51 deletions(-) create mode 100644 pkg/strategy/rebalance/order_executor_map.go create mode 100644 pkg/strategy/rebalance/position_map.go create mode 100644 pkg/strategy/rebalance/profit_stats_map.go diff --git a/pkg/strategy/rebalance/order_executor_map.go b/pkg/strategy/rebalance/order_executor_map.go new file mode 100644 index 0000000000..04dc094520 --- /dev/null +++ b/pkg/strategy/rebalance/order_executor_map.go @@ -0,0 +1,76 @@ +package rebalance + +import ( + "context" + "fmt" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +type GeneralOrderExecutorMap map[string]*bbgo.GeneralOrderExecutor + +func NewGeneralOrderExecutorMap(session *bbgo.ExchangeSession, positionMap PositionMap) GeneralOrderExecutorMap { + m := make(GeneralOrderExecutorMap) + + for symbol, position := range positionMap { + orderExecutor := bbgo.NewGeneralOrderExecutor(session, symbol, ID, instanceID(symbol), position) + m[symbol] = orderExecutor + } + + return m +} + +func (m GeneralOrderExecutorMap) BindEnvironment(environ *bbgo.Environment) { + for _, orderExecutor := range m { + orderExecutor.BindEnvironment(environ) + } +} + +func (m GeneralOrderExecutorMap) BindProfitStats(profitStatsMap ProfitStatsMap) { + for symbol, orderExecutor := range m { + orderExecutor.BindProfitStats(profitStatsMap[symbol]) + } +} + +func (m GeneralOrderExecutorMap) Bind() { + for _, orderExecutor := range m { + orderExecutor.Bind() + } +} + +func (m GeneralOrderExecutorMap) Sync(ctx context.Context, obj interface{}) { + for _, orderExecutor := range m { + orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, obj) + }) + } +} + +func (m GeneralOrderExecutorMap) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { + var allCreatedOrders types.OrderSlice + for _, submitOrder := range submitOrders { + orderExecutor, ok := m[submitOrder.Symbol] + if !ok { + return nil, fmt.Errorf("order executor not found for symbol %s", submitOrder.Symbol) + } + + createdOrders, err := orderExecutor.SubmitOrders(ctx) + if err != nil { + return nil, err + } + allCreatedOrders = append(allCreatedOrders, createdOrders...) + } + + return allCreatedOrders, nil +} + +func (m GeneralOrderExecutorMap) GracefulCancel(ctx context.Context) error { + for _, orderExecutor := range m { + err := orderExecutor.GracefulCancel(ctx) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/strategy/rebalance/position_map.go b/pkg/strategy/rebalance/position_map.go new file mode 100644 index 0000000000..162da96f95 --- /dev/null +++ b/pkg/strategy/rebalance/position_map.go @@ -0,0 +1,20 @@ +package rebalance + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +type PositionMap map[string]*types.Position + +func NewPositionMap(markets []types.Market) PositionMap { + m := make(PositionMap) + + for _, market := range markets { + position := types.NewPositionFromMarket(market) + position.Strategy = ID + position.StrategyInstanceID = instanceID(market.Symbol) + m[market.Symbol] = position + } + + return m +} diff --git a/pkg/strategy/rebalance/profit_stats_map.go b/pkg/strategy/rebalance/profit_stats_map.go new file mode 100644 index 0000000000..eba460be03 --- /dev/null +++ b/pkg/strategy/rebalance/profit_stats_map.go @@ -0,0 +1,15 @@ +package rebalance + +import "github.com/c9s/bbgo/pkg/types" + +type ProfitStatsMap map[string]*types.ProfitStats + +func NewProfitStatsMap(markets []types.Market) ProfitStatsMap { + m := make(ProfitStatsMap) + + for _, market := range markets { + m[market.Symbol] = types.NewProfitStats(market) + } + + return m +} diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index 9de03764b0..faacd160f0 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -3,6 +3,7 @@ package rebalance import ( "context" "fmt" + "sync" "github.com/sirupsen/logrus" @@ -19,6 +20,10 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +func instanceID(symbol string) string { + return fmt.Sprintf("%s:%s", ID, symbol) +} + type Strategy struct { Environment *bbgo.Environment @@ -30,11 +35,11 @@ type Strategy struct { OrderType types.OrderType `json:"orderType"` DryRun bool `json:"dryRun"` - PositionMap map[string]*types.Position `persistence:"positionMap"` - ProfitStatsMap map[string]*types.ProfitStats `persistence:"profitStatsMap"` + PositionMap PositionMap `persistence:"positionMap"` + ProfitStatsMap ProfitStatsMap `persistence:"profitStatsMap"` session *bbgo.ExchangeSession - orderExecutorMap map[string]*bbgo.GeneralOrderExecutor + orderExecutorMap GeneralOrderExecutorMap activeOrderBook *bbgo.ActiveOrderBook } @@ -53,10 +58,6 @@ func (s *Strategy) ID() string { return ID } -func (s *Strategy) InstanceID(symbol string) string { - return fmt.Sprintf("%s:%s", ID, symbol) -} - func (s *Strategy) Validate() error { if len(s.TargetWeights) == 0 { return fmt.Errorf("targetWeights should not be empty") @@ -97,14 +98,18 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } if s.PositionMap == nil { - s.initPositionMapFromMarkets(markets) + s.PositionMap = NewPositionMap(markets) } if s.ProfitStatsMap == nil { - s.initProfitStatsMapFromMarkets(markets) + s.ProfitStatsMap = NewProfitStatsMap(markets) } - s.initOrderExecutorMapFromMarkets(ctx, markets) + s.orderExecutorMap = NewGeneralOrderExecutorMap(session, s.PositionMap) + s.orderExecutorMap.BindEnvironment(s.Environment) + s.orderExecutorMap.BindProfitStats(s.ProfitStatsMap) + s.orderExecutorMap.Bind() + s.orderExecutorMap.Sync(ctx, s) s.activeOrderBook = bbgo.NewActiveOrderBook("") s.activeOrderBook.BindStream(s.session.UserDataStream) @@ -113,41 +118,13 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.rebalance(ctx) }) - return nil -} - -func (s *Strategy) initPositionMapFromMarkets(markets []types.Market) { - s.PositionMap = make(map[string]*types.Position) - for _, market := range markets { - position := types.NewPositionFromMarket(market) - position.Strategy = s.ID() - position.StrategyInstanceID = s.InstanceID(market.Symbol) - s.PositionMap[market.Symbol] = position - } -} - -func (s *Strategy) initProfitStatsMapFromMarkets(markets []types.Market) { - s.ProfitStatsMap = make(map[string]*types.ProfitStats) - for _, market := range markets { - s.ProfitStatsMap[market.Symbol] = types.NewProfitStats(market) - } -} - -func (s *Strategy) initOrderExecutorMapFromMarkets(ctx context.Context, markets []types.Market) { - s.orderExecutorMap = make(map[string]*bbgo.GeneralOrderExecutor) - for _, market := range markets { - symbol := market.Symbol - - orderExecutor := bbgo.NewGeneralOrderExecutor(s.session, symbol, ID, s.InstanceID(symbol), s.PositionMap[symbol]) - orderExecutor.BindEnvironment(s.Environment) - orderExecutor.BindProfitStats(s.ProfitStatsMap[symbol]) - orderExecutor.Bind() - orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { - bbgo.Sync(ctx, s) - }) + // the shutdown handler, you can cancel all orders + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + _ = s.orderExecutorMap.GracefulCancel(ctx) + }) - s.orderExecutorMap[market.Symbol] = orderExecutor - } + return nil } func (s *Strategy) rebalance(ctx context.Context) { @@ -165,14 +142,12 @@ func (s *Strategy) rebalance(ctx context.Context) { return } - for _, submitOrder := range submitOrders { - createdOrders, err := s.orderExecutorMap[submitOrder.Symbol].SubmitOrders(ctx, submitOrder) - if err != nil { - log.WithError(err).Error("failed to submit orders") - return - } - s.activeOrderBook.Add(createdOrders...) + createdOrders, err := s.orderExecutorMap.SubmitOrders(ctx, submitOrders...) + if err != nil { + log.WithError(err).Error("failed to submit orders") + return } + s.activeOrderBook.Add(createdOrders...) } func (s *Strategy) prices(ctx context.Context) types.ValueMap { From 51a52d1c18379d02de55413827b81a74a39c867f Mon Sep 17 00:00:00 2001 From: chiahung Date: Mon, 13 Mar 2023 11:13:26 +0800 Subject: [PATCH 0550/1392] comment out negative precision for dnum --- pkg/fixedpoint/convert_test.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pkg/fixedpoint/convert_test.go b/pkg/fixedpoint/convert_test.go index 634e408da4..c0f5369148 100644 --- a/pkg/fixedpoint/convert_test.go +++ b/pkg/fixedpoint/convert_test.go @@ -45,21 +45,24 @@ func Test_FormatString(t *testing.T) { assert.Equal("-1.234567890", s) }) - t.Run("12.3456789 with prec = -1, expected 10", func(t *testing.T) { - v := MustNewFromString("12.3456789") - s := v.FormatString(-1) - assert.Equal("10", s) - }) - t.Run("-0.00001234 with prec = 3, expected = 0.000", func(t *testing.T) { - v := MustNewFromString("-0.00001234") + v := MustNewFromString("-0.0001234") s := v.FormatString(3) assert.Equal("0.000", s) }) - t.Run("12.3456789 with prec = -3, expected = 0", func(t *testing.T) { - v := MustNewFromString("12.3456789") - s := v.FormatString(-2) - assert.Equal("0", s) - }) + // comment out negative precision for dnum testing + /* + t.Run("12.3456789 with prec = -1, expected 10", func(t *testing.T) { + v := MustNewFromString("12.3456789") + s := v.FormatString(-1) + assert.Equal("10", s) + }) + + t.Run("12.3456789 with prec = -3, expected = 0", func(t *testing.T) { + v := MustNewFromString("12.3456789") + s := v.FormatString(-2) + assert.Equal("0", s) + }) + */ } From 0ea345a18c2ae9f1dc516a75e597026c59d865a4 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 18 Jan 2023 17:09:22 +0800 Subject: [PATCH 0551/1392] improve/linregmaker: fix balance calculation in backtesting --- config/linregmaker.yaml | 2 +- pkg/strategy/linregmaker/strategy.go | 20 ++++++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 57b89ec8a7..0fab52c491 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -19,7 +19,7 @@ backtest: # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp startTime: "2022-05-01" - endTime: "2022-10-31" + endTime: "2022-05-02" symbols: - BTCUSDT accounts: diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index a0d2f27300..7f267e9c71 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -426,22 +426,17 @@ func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] lastPrice, _ := s.session.LastPrice(s.Symbol) - if bbgo.IsBackTesting { - if !hasQuoteBalance { - baseQty = fixedpoint.Zero - quoteQty = fixedpoint.Zero - } else { - baseQty = quoteBalance.Available.Div(lastPrice) - quoteQty = quoteBalance.Available - } - } else if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { + if bbgo.IsBackTesting { // Backtesting + baseQty = s.Position.Base + quoteQty = quoteBalance.Available - fixedpoint.Max(s.Position.Quote.Mul(fixedpoint.Two), fixedpoint.Zero) + } else if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { // Leveraged quoteQ, err := bbgo.CalculateQuoteQuantity(s.ctx, s.session, s.Market.QuoteCurrency, s.Leverage) if err != nil { quoteQ = fixedpoint.Zero } quoteQty = quoteQ baseQty = quoteQ.Div(lastPrice) - } else { + } else { // Spot if !hasBaseBalance { baseQty = fixedpoint.Zero } else { @@ -465,9 +460,9 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f // Check if current position > maxExposurePosition if s.Position.GetBase().Abs().Compare(s.MaxExposurePosition) > 0 { - if s.mainTrendCurrent == types.DirectionUp { + if s.Position.IsLong() { canBuy = false - } else if s.mainTrendCurrent == types.DirectionDown { + } else if s.Position.IsShort() { canSell = false } log.Infof("current position %v larger than max exposure %v, skip increase position", s.Position.GetBase().Abs(), s.MaxExposurePosition) @@ -589,6 +584,7 @@ func (s *Strategy) getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice f } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + log.Debugf("%v", orderExecutor) // Here just to suppress GoLand warning // initial required information s.session = session s.ctx = ctx From a607f230d662658a20d9f284df83d893a086f008 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 6 Jan 2023 12:00:32 +0800 Subject: [PATCH 0552/1392] improve/linregmaker: more log for can buy sell --- pkg/strategy/linregmaker/strategy.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 7f267e9c71..7cc49ebc89 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -473,12 +473,12 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f // Price too high if bidPrice.Float64() > s.neutralBoll.UpBand.Last() { canBuy = false - log.Infof("tradeInBand is set, skip buy when the price is higher than the neutralBB") + log.Infof("tradeInBand is set, skip buy due to the price is higher than the neutralBB") } // Price too low in uptrend if askPrice.Float64() < s.neutralBoll.DownBand.Last() { canSell = false - log.Infof("tradeInBand is set, skip sell when the price is lower than the neutralBB") + log.Infof("tradeInBand is set, skip sell due to the price is lower than the neutralBB") } } @@ -486,8 +486,10 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f if !s.isAllowOppositePosition() { if s.mainTrendCurrent == types.DirectionUp && (s.Position.IsClosed() || s.Position.IsDust(askPrice)) { canSell = false + log.Infof("Skip sell due to the long position is closed") } else if s.mainTrendCurrent == types.DirectionDown && (s.Position.IsClosed() || s.Position.IsDust(bidPrice)) { canBuy = false + log.Infof("Skip buy due to the short position is closed") } } @@ -497,16 +499,20 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f if quoteQty.Compare(fixedpoint.Zero) <= 0 { if s.Position.IsLong() { canBuy = false + log.Infof("Skip buy due to the account has no available balance") } else if s.Position.IsShort() { canSell = false + log.Infof("Skip sell due to the account has no available balance") } } } else { if buyQuantity.Compare(quoteQty.Div(bidPrice)) > 0 { // Spot canBuy = false + log.Infof("Skip buy due to the account has no available balance") } if sellQuantity.Compare(baseQty) > 0 { canSell = false + log.Infof("Skip sell due to the account has no available balance") } } From 3c14382c3c2c76e2d76c6e8c9653f9aa24b411bc Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 16 Jan 2023 14:46:30 +0800 Subject: [PATCH 0553/1392] improve/linregmaker: fix StandardIndicatorSet initialization problem --- pkg/strategy/linregmaker/strategy.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 7cc49ebc89..b251debec7 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -179,8 +179,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: s.ReverseEMA.Interval, }) - // Initialize ReverseEMA - s.ReverseEMA = s.StandardIndicatorSet.EWMA(s.ReverseEMA.IntervalWindow) // Subscribe for ReverseInterval. Use interval of ReverseEMA if ReverseInterval is omitted if s.ReverseInterval == "" { @@ -214,8 +212,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { Interval: s.NeutralBollinger.Interval, }) } - // Initialize BBs - s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) // Setup Exits s.ExitMethods.SetAndSubscribe(session, s) @@ -639,6 +635,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.ExitMethods.Bind(session, s.orderExecutor) + // Indicators initialized by StandardIndicatorSet must be initialized in Run() + // Initialize ReverseEMA + s.ReverseEMA = s.StandardIndicatorSet.EWMA(s.ReverseEMA.IntervalWindow) + // Initialize BBs + s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) + // Default spread if s.Spread == fixedpoint.Zero { s.Spread = fixedpoint.NewFromFloat(0.001) From 6e854f8027811e66f6a96bb0f1a766f45532b8b4 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 18 Jan 2023 14:51:07 +0800 Subject: [PATCH 0554/1392] improve/linregmaker: add MinProfitSpread --- config/linregmaker.yaml | 8 +++++ pkg/strategy/linregmaker/strategy.go | 44 +++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 0fab52c491..0964734c2d 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -163,6 +163,14 @@ exchangeStrategies: domain: [ -0.00005, 0.0001 ] range: [ 0.02, 0 ] + # minProfitSpread is the minimal order price spread from the current average cost. + # For long position, you will only place sell order above the price (= average cost * (1 + minProfitSpread)) + # For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) + minProfitSpread: 0.1% + + # minProfitDisableOn disables MinProfitSpread when position RoI drops below specified percentage + minProfitDisableOn: -10% + exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) - roiStopLoss: diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index b251debec7..42ca4eba05 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -118,6 +118,14 @@ type Strategy struct { // UseDynamicQuantityAsAmount calculates amount instead of quantity UseDynamicQuantityAsAmount bool `json:"useDynamicQuantityAsAmount"` + // MinProfitSpread is the minimal order price spread from the current average cost. + // For long position, you will only place sell order above the price (= average cost * (1 + minProfitSpread)) + // For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) + MinProfitSpread fixedpoint.Value `json:"minProfitSpread"` + + // MinProfitDisableOn disables MinProfitSpread when position RoI drops below specified percentage + MinProfitDisableOn fixedpoint.Value `json:"minProfitDisableOn"` + // ExitMethods are various TP/SL methods ExitMethods bbgo.ExitMethodSet `json:"exits"` @@ -449,7 +457,7 @@ func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { } // getCanBuySell returns the buy sell switches -func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice fixedpoint.Value) (canBuy bool, canSell bool) { +func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice, midPrice fixedpoint.Value) (canBuy bool, canSell bool) { // By default, both buy and sell are on, which means we will place buy and sell orders canBuy = true canSell = true @@ -482,11 +490,31 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f if !s.isAllowOppositePosition() { if s.mainTrendCurrent == types.DirectionUp && (s.Position.IsClosed() || s.Position.IsDust(askPrice)) { canSell = false - log.Infof("Skip sell due to the long position is closed") + log.Infof("skip sell due to the long position is closed") } else if s.mainTrendCurrent == types.DirectionDown && (s.Position.IsClosed() || s.Position.IsDust(bidPrice)) { canBuy = false - log.Infof("Skip buy due to the short position is closed") + log.Infof("skip buy due to the short position is closed") + } + } + + // Min profit + roi := s.Position.ROI(midPrice) + if roi.Compare(s.MinProfitDisableOn) >= 0 { + if s.Position.IsLong() && !s.Position.IsDust(askPrice) { + minProfitPrice := s.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread)) + if askPrice.Compare(minProfitPrice) < 0 { + canSell = false + log.Infof("askPrice %v is less than minProfitPrice %v. skip sell", askPrice, minProfitPrice) + } + } else if s.Position.IsShort() && s.Position.IsDust(bidPrice) { + minProfitPrice := s.Position.AverageCost.Mul(fixedpoint.One.Sub(s.MinProfitSpread)) + if bidPrice.Compare(minProfitPrice) > 0 { + canBuy = false + log.Infof("bidPrice %v is greater than minProfitPrice %v. skip buy", bidPrice, minProfitPrice) + } } + } else { + log.Infof("position RoI %v is less than MinProfitDisableOn %v. disable min profit protection", roi, s.MinProfitDisableOn) } // Check against account balance @@ -495,20 +523,20 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice f if quoteQty.Compare(fixedpoint.Zero) <= 0 { if s.Position.IsLong() { canBuy = false - log.Infof("Skip buy due to the account has no available balance") + log.Infof("skip buy due to the account has no available balance") } else if s.Position.IsShort() { canSell = false - log.Infof("Skip sell due to the account has no available balance") + log.Infof("skip sell due to the account has no available balance") } } } else { if buyQuantity.Compare(quoteQty.Div(bidPrice)) > 0 { // Spot canBuy = false - log.Infof("Skip buy due to the account has no available balance") + log.Infof("skip buy due to the account has no available balance") } if sellQuantity.Compare(baseQty) > 0 { canSell = false - log.Infof("Skip sell due to the account has no available balance") + log.Infof("skip sell due to the account has no available balance") } } @@ -762,7 +790,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se buyOrder, sellOrder := s.getOrderForms(buyQuantity, bidPrice, sellQuantity, askPrice) - canBuy, canSell := s.getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice) + canBuy, canSell := s.getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice, midPrice) // Submit orders var submitOrders []types.SubmitOrder From dd87d716cec8f988784c46c73d0a3d5330d7cf62 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 6 Jan 2023 12:00:32 +0800 Subject: [PATCH 0555/1392] improve/linregmaker: fix end date of linregmaker config --- config/linregmaker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 0964734c2d..cf82c5d628 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -19,7 +19,7 @@ backtest: # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp startTime: "2022-05-01" - endTime: "2022-05-02" + endTime: "2022-10-31" symbols: - BTCUSDT accounts: From 5fc459d404f1a525753c846ea8872b394d2885d3 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 19 Jan 2023 12:09:32 +0800 Subject: [PATCH 0556/1392] improve/linregmaker: rename MinProfitDisableOn to MinProfitActivationRate --- config/linregmaker.yaml | 4 ++-- pkg/strategy/linregmaker/strategy.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index cf82c5d628..49e765e2ec 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -168,8 +168,8 @@ exchangeStrategies: # For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) minProfitSpread: 0.1% - # minProfitDisableOn disables MinProfitSpread when position RoI drops below specified percentage - minProfitDisableOn: -10% + # minProfitActivationRate activates MinProfitSpread when position RoI higher than the specified percentage + minProfitActivationRate: -10% exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 42ca4eba05..55a4fa13a9 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -123,8 +123,8 @@ type Strategy struct { // For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) MinProfitSpread fixedpoint.Value `json:"minProfitSpread"` - // MinProfitDisableOn disables MinProfitSpread when position RoI drops below specified percentage - MinProfitDisableOn fixedpoint.Value `json:"minProfitDisableOn"` + // MinProfitActivationRate activates MinProfitSpread when position RoI higher than the specified percentage + MinProfitActivationRate fixedpoint.Value `json:"minProfitActivationRate"` // ExitMethods are various TP/SL methods ExitMethods bbgo.ExitMethodSet `json:"exits"` @@ -499,7 +499,7 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice, // Min profit roi := s.Position.ROI(midPrice) - if roi.Compare(s.MinProfitDisableOn) >= 0 { + if roi.Compare(s.MinProfitActivationRate) >= 0 { if s.Position.IsLong() && !s.Position.IsDust(askPrice) { minProfitPrice := s.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread)) if askPrice.Compare(minProfitPrice) < 0 { @@ -514,7 +514,7 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice, } } } else { - log.Infof("position RoI %v is less than MinProfitDisableOn %v. disable min profit protection", roi, s.MinProfitDisableOn) + log.Infof("position RoI %v is less than minProfitActivationRate %v. min profit protection is not active", roi, s.MinProfitActivationRate) } // Check against account balance From cb412dc13fcb667e088c247b637d6fdbc207f0e4 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 19 Jan 2023 12:15:20 +0800 Subject: [PATCH 0557/1392] improve/bollmaker: add MinProfitActivationRate --- config/bollmaker.yaml | 3 +++ pkg/strategy/bollmaker/strategy.go | 34 ++++++++++++++++++------------ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/config/bollmaker.yaml b/config/bollmaker.yaml index 732c23a7fd..0cc53894c0 100644 --- a/config/bollmaker.yaml +++ b/config/bollmaker.yaml @@ -59,6 +59,9 @@ exchangeStrategies: # For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) minProfitSpread: 0.1% + # minProfitActivationRate activates MinProfitSpread when position RoI higher than the specified percentage + minProfitActivationRate: -10% + # trendEMA detects the trend by a given EMA # when EMA goes up (the last > the previous), allow buy and sell # when EMA goes down (the last < the previous), disable buy, allow sell diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 959bdff734..71003d345e 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -82,6 +82,9 @@ type Strategy struct { // For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) MinProfitSpread fixedpoint.Value `json:"minProfitSpread"` + // MinProfitActivationRate activates MinProfitSpread when position RoI higher than the specified percentage + MinProfitActivationRate fixedpoint.Value `json:"minProfitActivationRate"` + // UseTickerPrice use the ticker api to get the mid price instead of the closed kline price. // The back-test engine is kline-based, so the ticker price api is not supported. // Turn this on if you want to do real trading. @@ -381,22 +384,25 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k isLongPosition := s.Position.IsLong() isShortPosition := s.Position.IsShort() - minProfitPrice := s.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread)) - if isShortPosition { - minProfitPrice = s.Position.AverageCost.Mul(fixedpoint.One.Sub(s.MinProfitSpread)) - } - if isLongPosition { - // for long position if the current price is lower than the minimal profitable price then we should stop sell - // this avoid loss trade - if midPrice.Compare(minProfitPrice) < 0 { - canSell = false + if s.Position.ROI(midPrice).Compare(s.MinProfitActivationRate) >= 0 { + minProfitPrice := s.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread)) + if isShortPosition { + minProfitPrice = s.Position.AverageCost.Mul(fixedpoint.One.Sub(s.MinProfitSpread)) } - } else if isShortPosition { - // for short position if the current price is higher than the minimal profitable price then we should stop buy - // this avoid loss trade - if midPrice.Compare(minProfitPrice) > 0 { - canBuy = false + + if isLongPosition { + // for long position if the current price is lower than the minimal profitable price then we should stop sell + // this avoids loss trade + if midPrice.Compare(minProfitPrice) < 0 { + canSell = false + } + } else if isShortPosition { + // for short position if the current price is higher than the minimal profitable price then we should stop buy + // this avoids loss trade + if midPrice.Compare(minProfitPrice) > 0 { + canBuy = false + } } } From 360173ac2b7d4227ff2df51064d9c4f00c3aa68d Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 7 Feb 2023 10:43:29 +0800 Subject: [PATCH 0558/1392] fix/linregmaker: fix syntax error --- pkg/strategy/linregmaker/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 55a4fa13a9..34ff4fb2d8 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -432,7 +432,7 @@ func (s *Strategy) getAllowedBalance() (baseQty, quoteQty fixedpoint.Value) { if bbgo.IsBackTesting { // Backtesting baseQty = s.Position.Base - quoteQty = quoteBalance.Available - fixedpoint.Max(s.Position.Quote.Mul(fixedpoint.Two), fixedpoint.Zero) + quoteQty = quoteBalance.Available.Sub(fixedpoint.Max(s.Position.Quote.Mul(fixedpoint.Two), fixedpoint.Zero)) } else if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { // Leveraged quoteQ, err := bbgo.CalculateQuoteQuantity(s.ctx, s.session, s.Market.QuoteCurrency, s.Leverage) if err != nil { From 83ba32bf2f913e495ddceac77e5ee6b16400d768 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Mon, 13 Mar 2023 18:43:52 +0800 Subject: [PATCH 0559/1392] mock SubmitOrders by DoAndReturn --- pkg/strategy/grid2/strategy_test.go | 73 +++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 1801419ffd..599a4bf1df 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -22,6 +22,23 @@ func init() { registerMetrics() } +func equalOrdersIgnoreClientOrderID(a, b types.SubmitOrder) bool { + return a.Symbol == b.Symbol && + a.Side == b.Side && + a.Type == b.Type && + a.Quantity == b.Quantity && + a.Price == b.Price && + a.AveragePrice == b.AveragePrice && + a.StopPrice == b.StopPrice && + a.Market == b.Market && + a.TimeInForce == b.TimeInForce && + a.GroupID == b.GroupID && + a.MarginSideEffect == b.MarginSideEffect && + a.ReduceOnly == b.ReduceOnly && + a.ClosePosition == b.ClosePosition && + a.Tag == b.Tag +} + func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { s := &Strategy{ logger: logrus.NewEntry(logrus.New()), @@ -605,9 +622,12 @@ func TestStrategy_handleOrderFilled(t *testing.T) { } orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) - orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{ - {SubmitOrder: expectedSubmitOrder}, - }, nil) + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder) + return []types.Order{ + {SubmitOrder: expectedSubmitOrder}, + }, nil + }) s.orderExecutor = orderExecutor s.handleOrderFilled(types.Order{ @@ -670,9 +690,12 @@ func TestStrategy_handleOrderFilled(t *testing.T) { } orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) - orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{ - {SubmitOrder: expectedSubmitOrder}, - }, nil) + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder) + return []types.Order{ + {SubmitOrder: expectedSubmitOrder}, + }, nil + }) s.orderExecutor = orderExecutor s.handleOrderFilled(types.Order{ @@ -755,9 +778,12 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Market: s.Market, Tag: orderTag, } - orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{ - {SubmitOrder: expectedSubmitOrder}, - }, nil) + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder) + return []types.Order{ + {SubmitOrder: expectedSubmitOrder}, + }, nil + }) expectedSubmitOrder2 := types.SubmitOrder{ Symbol: "BTCUSDT", @@ -769,9 +795,12 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Market: s.Market, Tag: orderTag, } - orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder2).Return([]types.Order{ - {SubmitOrder: expectedSubmitOrder2}, - }, nil) + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder2, order), "%+v is not equal to %+v", order, expectedSubmitOrder2) + return []types.Order{ + {SubmitOrder: expectedSubmitOrder2}, + }, nil + }) s.orderExecutor = orderExecutor @@ -863,9 +892,12 @@ func TestStrategy_handleOrderFilled(t *testing.T) { } orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) - orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{ - {SubmitOrder: expectedSubmitOrder}, - }, nil) + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder) + return []types.Order{ + {SubmitOrder: expectedSubmitOrder}, + }, nil + }) expectedSubmitOrder2 := types.SubmitOrder{ Symbol: "BTCUSDT", @@ -878,9 +910,12 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Tag: orderTag, } - orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder2).Return([]types.Order{ - {SubmitOrder: expectedSubmitOrder2}, - }, nil) + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder2, order), "%+v is not equal to %+v", order, expectedSubmitOrder2) + return []types.Order{ + {SubmitOrder: expectedSubmitOrder2}, + }, nil + }) s.orderExecutor = orderExecutor s.handleOrderFilled(types.Order{ @@ -1142,4 +1177,4 @@ func Test_buildPinOrderMap(t *testing.T) { _, err := s.buildPinOrderMap(s.grid, openOrders) assert.Error(err) }) -} +} \ No newline at end of file From 35ceda84084718831c41b5db22f24eb7494dc0b8 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 13 Mar 2023 21:27:13 +0800 Subject: [PATCH 0560/1392] grid2: use newClientOrderID only for max --- pkg/strategy/grid2/strategy.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 00e100ed97..82b78cebed 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -495,7 +495,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { Quantity: newQuantity, Tag: orderTag, GroupID: s.OrderGroupID, - ClientOrderID: uuid.New().String(), + ClientOrderID: s.newClientOrderID(), } s.logger.Infof("SUBMIT GRID REVERSE ORDER: %s", orderForm.String()) @@ -1253,7 +1253,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin TimeInForce: types.TimeInForceGTC, Tag: orderTag, GroupID: s.OrderGroupID, - ClientOrderID: uuid.New().String(), + ClientOrderID: s.newClientOrderID(), }) usedBase = usedBase.Add(quantity) } else { @@ -1271,7 +1271,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin TimeInForce: types.TimeInForceGTC, Tag: orderTag, GroupID: s.OrderGroupID, - ClientOrderID: uuid.New().String(), + ClientOrderID: s.newClientOrderID(), }) quoteQuantity := quantity.Mul(nextPrice) usedQuote = usedQuote.Add(quoteQuantity) @@ -1305,7 +1305,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin TimeInForce: types.TimeInForceGTC, Tag: orderTag, GroupID: s.OrderGroupID, - ClientOrderID: uuid.New().String(), + ClientOrderID: s.newClientOrderID(), }) usedQuote = usedQuote.Add(quoteQuantity) } @@ -2119,6 +2119,13 @@ func (s *Strategy) findDuplicatedPriceOpenOrders(openOrders []types.Order) (dupO return dupOrders } +func (s *Strategy) newClientOrderID() string { + if s.session != nil && s.session.ExchangeName == types.ExchangeMax { + return uuid.New().String() + } + return "" +} + func generalBackoff(ctx context.Context, op backoff.Operation) (err error) { err = backoff.Retry(op, backoff.WithContext( backoff.WithMaxRetries( @@ -2145,4 +2152,4 @@ func queryOpenOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, symb err = generalBackoff(ctx, op) return openOrders, err -} \ No newline at end of file +} From 18b401e10084d480d1a23008ad188faf344618db Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 13 Mar 2023 22:04:23 +0800 Subject: [PATCH 0561/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index 7162e69342..ea110d2a0f 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -60,4 +60,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index cfec28f9d4..b1173b9824 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -43,4 +43,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 4211995532..e35345e166 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -52,4 +52,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index d5819a29e3..c22fff8b54 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -42,4 +42,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index efd3c28d9c..b6c5608ce3 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -41,4 +41,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index 1cb49a406b..4314f684eb 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -51,4 +51,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index 79407138a4..1b0e09363a 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -43,4 +43,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 27ec167b5b..2918dfe8af 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -50,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 4ea39bbbcf..8158237c42 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -44,4 +44,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index 9426fc9313..3566527530 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -47,4 +47,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 1a71a372a6..5ccd16c2f0 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -44,4 +44,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index 4b510f8ecf..aff25a102b 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -43,4 +43,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index 7e540c5850..91722cc1b8 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -40,4 +40,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index ea4ecf6d50..4d8edfd95f 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -43,4 +43,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 260cf70867..1f982537cc 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -43,4 +43,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index d4aa174cc6..376377e89d 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -43,4 +43,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index 4eba889e9e..beffa4c021 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -42,4 +42,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index 8bb818dfa9..bb5712a6fa 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -46,4 +46,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index 4733cd0f36..fca07896b1 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -44,4 +44,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index bee12f5038..1e958cf703 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -42,4 +42,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index f7ee450567..5aeb284a5c 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -51,4 +51,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 90db070e5d..006037c555 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -53,4 +53,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index c53957181b..332491d7d2 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -48,4 +48,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index ef8947eea5..a4002afd2c 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -44,4 +44,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index e040da5144..73f9463abd 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -44,4 +44,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 1ee6f977aa..1e6b18a1a9 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -42,4 +42,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 33ad27aa4d..7427b776e9 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -44,4 +44,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index c0c567216d..7fb0cd60c7 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -42,4 +42,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index 62e44424b7..bf764e3f32 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -41,4 +41,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Dec-2022 +###### Auto generated by spf13/cobra on 13-Mar-2023 From b58dcaba79332e4daefaaeb6258ccf54ee2035db Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 13 Mar 2023 22:04:23 +0800 Subject: [PATCH 0562/1392] bump version to v1.44.0 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index eb15e9ef31..d98e155dfa 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.43.1-b7f3d4f1-dev" +const Version = "v1.44.0-4b3f00fe-dev" -const VersionGitRef = "b7f3d4f1" +const VersionGitRef = "4b3f00fe" diff --git a/pkg/version/version.go b/pkg/version/version.go index 699cdef19b..bd31cbf394 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.43.1-b7f3d4f1" +const Version = "v1.44.0-4b3f00fe" -const VersionGitRef = "b7f3d4f1" +const VersionGitRef = "4b3f00fe" From a5641f6c736d3188b358fdbda0f61dda780c96f7 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 13 Mar 2023 22:04:23 +0800 Subject: [PATCH 0563/1392] add v1.44.0 release note --- doc/release/v1.44.0.md | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 doc/release/v1.44.0.md diff --git a/doc/release/v1.44.0.md b/doc/release/v1.44.0.md new file mode 100644 index 0000000000..3fa0d4f787 --- /dev/null +++ b/doc/release/v1.44.0.md @@ -0,0 +1,61 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.43.1...main) + + - [#1105](https://github.com/c9s/bbgo/pull/1105): IMPROVE: [grid2]: use newClientOrderID only for max + - [#1052](https://github.com/c9s/bbgo/pull/1052): IMPROVE: strategy: linregmaker minprofit + - [#1100](https://github.com/c9s/bbgo/pull/1100): FIX: [grid2] specify client order id explicitly + - [#1098](https://github.com/c9s/bbgo/pull/1098): FIX: fix format string float point issue + - [#1103](https://github.com/c9s/bbgo/pull/1103): strategy: rebalance: graceful cancel + - [#1102](https://github.com/c9s/bbgo/pull/1102): FIX: strategy: fix fixedmaker + - [#1101](https://github.com/c9s/bbgo/pull/1101): strategy: add fixedmaker + - [#1099](https://github.com/c9s/bbgo/pull/1099): FIX: [grid2] avoid handling one orderID twice + - [#1090](https://github.com/c9s/bbgo/pull/1090): fix/scale: fix LinearScale calculation + - [#1096](https://github.com/c9s/bbgo/pull/1096): FEATURE: [grid2] add ClearDuplicatedPriceOpenOrders option + - [#1087](https://github.com/c9s/bbgo/pull/1087): FEATURE: recover grids with open orders by querying trades process an… + - [#1095](https://github.com/c9s/bbgo/pull/1095): FIX: filter wrong order id from self-trade trades + - [#1094](https://github.com/c9s/bbgo/pull/1094): FIX: use `updated_at` instead of `created_at` to convert MAX order to typ… + - [#1093](https://github.com/c9s/bbgo/pull/1093): strategy: rebalance: add positions and profit stats + - [#1092](https://github.com/c9s/bbgo/pull/1092): FEATURE: [grid2] add more metrics and fix metric-related issues + - [#1091](https://github.com/c9s/bbgo/pull/1091): FEATURE: split self trades when use MAX RESTful API to query trades + - [#1089](https://github.com/c9s/bbgo/pull/1089): IMPROVE: exit: show symbol in trailing stop triggered message + - [#1088](https://github.com/c9s/bbgo/pull/1088): FIX: [grid2] fix isCompleteGrid condition + - [#1086](https://github.com/c9s/bbgo/pull/1086): FIX: [grid2] round down quoteQuantity/baseQuantity after the fee reduction + - [#1085](https://github.com/c9s/bbgo/pull/1085): FIX: [grid2] group id should be bound by MaxInt32 + - [#1080](https://github.com/c9s/bbgo/pull/1080): strategy: marketcap: add orderType parameter + - [#1083](https://github.com/c9s/bbgo/pull/1083): FIX: add group id on submit order API + - [#1084](https://github.com/c9s/bbgo/pull/1084): FIX: [grid2] avoid initializing metrics twice + - [#1082](https://github.com/c9s/bbgo/pull/1082): FIX: add mutex in memory store + - [#1081](https://github.com/c9s/bbgo/pull/1081): FIX: [grid2] fix active orderbook at recovering + - [#1079](https://github.com/c9s/bbgo/pull/1079): FIX: [grid2]: add write context for submitting orders + - [#1078](https://github.com/c9s/bbgo/pull/1078): FIX: [grid2]: improve the onStart callback and the cancel loop + - [#1077](https://github.com/c9s/bbgo/pull/1077): FEATURE: save expiring data to redis + - [#1076](https://github.com/c9s/bbgo/pull/1076): FIX: [grid2]: quantity fee reduction for quote currency + - [#1069](https://github.com/c9s/bbgo/pull/1069): DOC: add private strategy demo trading-gpt + - [#1075](https://github.com/c9s/bbgo/pull/1075): FEATURE: add persistence service to environment + - [#1074](https://github.com/c9s/bbgo/pull/1074): strategy: rebalance: add order type parameter + - [#1073](https://github.com/c9s/bbgo/pull/1073): FIX: fix fixedpoint rounding + - [#1072](https://github.com/c9s/bbgo/pull/1072): FIX: [grid2]: calculate grid profit only when the reverse order is placed + - [#1070](https://github.com/c9s/bbgo/pull/1070): FIX: add context, exponential backoff and max retry limit + - [#1071](https://github.com/c9s/bbgo/pull/1071): FEATURE: [grid2]: use UseCancelAllOrdersApiWhenClose + - [#1066](https://github.com/c9s/bbgo/pull/1066): FIX: [grid2]: fix fee reduction by rounding + - [#1065](https://github.com/c9s/bbgo/pull/1065): FEATURE: submit order backoff + - [#1064](https://github.com/c9s/bbgo/pull/1064): FIX: emit order update handler from the pending maker order + - [#1063](https://github.com/c9s/bbgo/pull/1063): IMPROVE: [grid2]: improve logging + - [#1062](https://github.com/c9s/bbgo/pull/1062): FIX: [grid2]: fix recover sorting + - [#1061](https://github.com/c9s/bbgo/pull/1061): FIX: [grid2] fix quote accumulation + - [#1060](https://github.com/c9s/bbgo/pull/1060): FIX: process pending order update for active order book + - [#1059](https://github.com/c9s/bbgo/pull/1059): FIX: [grid2]: emit grid ready once the grid is recovered + - [#1058](https://github.com/c9s/bbgo/pull/1058): FIX: [grid2]: fix grid order recover + - [#1057](https://github.com/c9s/bbgo/pull/1057): FIX: [grid2]: fix HasPrice + - [#1056](https://github.com/c9s/bbgo/pull/1056): FIX: [grid2]: fix upper price error + - [#1053](https://github.com/c9s/bbgo/pull/1053): FEATURE: get historical public trades from binance + - [#1023](https://github.com/c9s/bbgo/pull/1023): implement indicators from phemex + - [#1050](https://github.com/c9s/bbgo/pull/1050): FEATURE: New indicators + - [#1051](https://github.com/c9s/bbgo/pull/1051): FIX: [grid2]: fix grid num calculation + - [#1049](https://github.com/c9s/bbgo/pull/1049): FEATURE: [grid2]: make OpenGrid, CloseGrid api public + - [#1048](https://github.com/c9s/bbgo/pull/1048): FEATURE: [grid2]: integrate prometheus metrics + - [#1047](https://github.com/c9s/bbgo/pull/1047): FEATURE: add RSI to StandardIndicatorSet + - [#1043](https://github.com/c9s/bbgo/pull/1043): FEATURE: service: add redis namespace support + - [#1036](https://github.com/c9s/bbgo/pull/1036): FIX: create log dir to avoid error + - [#1035](https://github.com/c9s/bbgo/pull/1035): FEATURE: strategy: [grid2] add grid callbacks + - [#1034](https://github.com/c9s/bbgo/pull/1034): FEATURE: strategy: [grid2]: use initial order ID to query closed order history to recover grid + - [#1033](https://github.com/c9s/bbgo/pull/1033): FEATURE: strategy: [grid2]: improve recovering process [part 3] From 0b7f42c38204e11a88d2beaa757b6376d74b226f Mon Sep 17 00:00:00 2001 From: narumi Date: Mon, 13 Mar 2023 16:46:18 +0800 Subject: [PATCH 0564/1392] adjust max amount by balance --- pkg/strategy/rebalance/strategy.go | 88 ++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index faacd160f0..04e95f0d82 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -133,7 +133,11 @@ func (s *Strategy) rebalance(ctx context.Context) { log.WithError(err).Errorf("failed to cancel orders") } - submitOrders := s.generateSubmitOrders(ctx) + submitOrders, err := s.generateSubmitOrders(ctx) + if err != nil { + log.WithError(err).Error("failed to generate submit orders") + return + } for _, order := range submitOrders { log.Infof("generated submit order: %s", order.String()) } @@ -150,7 +154,7 @@ func (s *Strategy) rebalance(ctx context.Context) { s.activeOrderBook.Add(createdOrders...) } -func (s *Strategy) prices(ctx context.Context) types.ValueMap { +func (s *Strategy) prices(ctx context.Context) (types.ValueMap, error) { m := make(types.ValueMap) for currency := range s.TargetWeights { if currency == s.QuoteCurrency { @@ -160,29 +164,37 @@ func (s *Strategy) prices(ctx context.Context) types.ValueMap { ticker, err := s.session.Exchange.QueryTicker(ctx, currency+s.QuoteCurrency) if err != nil { - log.WithError(err).Error("failed to query tickers") - return nil + return nil, err } m[currency] = ticker.Last } - return m + return m, nil } -func (s *Strategy) quantities() types.ValueMap { - m := make(types.ValueMap) - +func (s *Strategy) balances() (types.BalanceMap, error) { + m := make(types.BalanceMap) balances := s.session.GetAccount().Balances() for currency := range s.TargetWeights { - m[currency] = balances[currency].Total() + balance, ok := balances[currency] + if !ok { + return nil, fmt.Errorf("no balance for %s", currency) + } + m[currency] = balance } - - return m + return m, nil } -func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []types.SubmitOrder) { - prices := s.prices(ctx) - marketValues := prices.Mul(s.quantities()) +func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []types.SubmitOrder, err error) { + prices, err := s.prices(ctx) + if err != nil { + return nil, err + } + balances, err := s.balances() + if err != nil { + return nil, err + } + marketValues := prices.Mul(balanceToTotal(balances)) currentWeights := marketValues.Normalize() for currency, targetWeight := range s.TargetWeights { @@ -221,8 +233,9 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []typ quantity = quantity.Abs() } - if s.MaxAmount.Sign() > 0 { - quantity = bbgo.AdjustQuantityByMaxAmount(quantity, currentPrice, s.MaxAmount) + maxAmount := s.adjustMaxAmountByBalance(side, currency, currentPrice, balances) + if maxAmount.Sign() > 0 { + quantity = bbgo.AdjustQuantityByMaxAmount(quantity, currentPrice, maxAmount) log.Infof("adjust the quantity %v (%s %s @ %v) by max amount %v", quantity, symbol, @@ -244,7 +257,7 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []typ submitOrders = append(submitOrders, order) } - return submitOrders + return submitOrders, err } func (s *Strategy) symbols() (symbols []string) { @@ -268,3 +281,44 @@ func (s *Strategy) markets() ([]types.Market, error) { } return markets, nil } + +func (s *Strategy) adjustMaxAmountByBalance(side types.SideType, currency string, currentPrice fixedpoint.Value, balances types.BalanceMap) fixedpoint.Value { + var maxAmount fixedpoint.Value + + switch side { + case types.SideTypeBuy: + maxAmount = balances[s.QuoteCurrency].Available + case types.SideTypeSell: + maxAmount = balances[currency].Available.Mul(currentPrice) + default: + log.Errorf("unknown side type: %s", side) + return fixedpoint.Zero + } + + if s.MaxAmount.Sign() > 0 { + maxAmount = fixedpoint.Min(s.MaxAmount, maxAmount) + } + + return maxAmount +} + +func (s *Strategy) checkMinimalOrderQuantity(order types.SubmitOrder) bool { + if order.Quantity.Compare(order.Market.MinQuantity) < 0 { + log.Infof("order quantity is too small: %f < %f", order.Quantity.Float64(), order.Market.MinQuantity.Float64()) + return false + } + + if order.Quantity.Mul(order.Price).Compare(order.Market.MinNotional) < 0 { + log.Infof("order min notional is too small: %f < %f", order.Quantity.Mul(order.Price).Float64(), order.Market.MinNotional.Float64()) + return false + } + return true +} + +func balanceToTotal(balances types.BalanceMap) types.ValueMap { + m := make(types.ValueMap) + for _, b := range balances { + m[b.Currency] = b.Total() + } + return m +} From c9f69957017e038f22d7f2316116ca7af7184a95 Mon Sep 17 00:00:00 2001 From: narumi Date: Mon, 13 Mar 2023 16:58:19 +0800 Subject: [PATCH 0565/1392] fix OrderExecutorMap's SumbitOrders --- pkg/strategy/rebalance/order_executor_map.go | 2 +- pkg/strategy/rebalance/strategy.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/rebalance/order_executor_map.go b/pkg/strategy/rebalance/order_executor_map.go index 04dc094520..45fd3272e3 100644 --- a/pkg/strategy/rebalance/order_executor_map.go +++ b/pkg/strategy/rebalance/order_executor_map.go @@ -55,7 +55,7 @@ func (m GeneralOrderExecutorMap) SubmitOrders(ctx context.Context, submitOrders return nil, fmt.Errorf("order executor not found for symbol %s", submitOrder.Symbol) } - createdOrders, err := orderExecutor.SubmitOrders(ctx) + createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrder) if err != nil { return nil, err } diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index 04e95f0d82..7d6a201905 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -143,6 +143,7 @@ func (s *Strategy) rebalance(ctx context.Context) { } if s.DryRun { + log.Infof("dry run, not submitting orders") return } From 640001ffa1c6257b73372181d13b3ce2fb18af96 Mon Sep 17 00:00:00 2001 From: narumi Date: Mon, 13 Mar 2023 18:24:46 +0800 Subject: [PATCH 0566/1392] check minimal order quantity --- pkg/strategy/rebalance/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index 7d6a201905..3a482080a1 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -255,7 +255,9 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []typ Price: currentPrice, } - submitOrders = append(submitOrders, order) + if ok := s.checkMinimalOrderQuantity(order); ok { + submitOrders = append(submitOrders, order) + } } return submitOrders, err From 0690518dc7d796313cee5a2f2f3d7297355696d6 Mon Sep 17 00:00:00 2001 From: narumi Date: Mon, 13 Mar 2023 22:43:42 +0800 Subject: [PATCH 0567/1392] add option to rebalance on start --- config/rebalance.yaml | 1 + pkg/strategy/rebalance/strategy.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/config/rebalance.yaml b/config/rebalance.yaml index e9a01e94b7..8a6f7f8bd2 100644 --- a/config/rebalance.yaml +++ b/config/rebalance.yaml @@ -39,3 +39,4 @@ exchangeStrategies: maxAmount: 1_000 # max amount to buy or sell per order orderType: LIMIT_MAKER # LIMIT, LIMIT_MAKER or MARKET dryRun: false + onStart: false diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index 3a482080a1..6df7c0aae0 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -34,6 +34,7 @@ type Strategy struct { MaxAmount fixedpoint.Value `json:"maxAmount"` // max amount to buy or sell per order OrderType types.OrderType `json:"orderType"` DryRun bool `json:"dryRun"` + OnStart bool `json:"onStart"` // rebalance on start PositionMap PositionMap `persistence:"positionMap"` ProfitStatsMap ProfitStatsMap `persistence:"profitStatsMap"` @@ -114,6 +115,12 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.activeOrderBook = bbgo.NewActiveOrderBook("") s.activeOrderBook.BindStream(s.session.UserDataStream) + session.UserDataStream.OnStart(func() { + if s.OnStart { + s.rebalance(ctx) + } + }) + s.session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { s.rebalance(ctx) }) From add9372eba78cde7541ecef31e5e7f646ea14be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= <4680567+narumiruna@users.noreply.github.com> Date: Mon, 13 Mar 2023 15:30:33 +0000 Subject: [PATCH 0568/1392] use mid price to calculate weight --- pkg/strategy/rebalance/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index 6df7c0aae0..1774fc7d39 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -175,7 +175,7 @@ func (s *Strategy) prices(ctx context.Context) (types.ValueMap, error) { return nil, err } - m[currency] = ticker.Last + m[currency] = ticker.Buy.Add(ticker.Sell).Div(fixedpoint.NewFromFloat(2.0)) } return m, nil } From 60d7d20cedbea330fde46b0fd2c9d027362d95df Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 14 Mar 2023 00:29:13 +0800 Subject: [PATCH 0569/1392] grid2: fix newline for the message format --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 82b78cebed..b5c39ce31e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1177,7 +1177,7 @@ func (s *Strategy) debugGridOrders(submitOrders []types.SubmitOrder, lastPrice f var sb strings.Builder - sb.WriteString("GRID ORDERS [") + sb.WriteString("GRID ORDERS [\n") for i, order := range submitOrders { if i > 0 && lastPrice.Compare(order.Price) >= 0 && lastPrice.Compare(submitOrders[i-1].Price) <= 0 { sb.WriteString(fmt.Sprintf(" - LAST PRICE: %f\n", lastPrice.Float64())) @@ -1201,7 +1201,7 @@ func (s *Strategy) debugOrders(desc string, orders []types.Order) { desc = "ORDERS" } - sb.WriteString(desc + " [") + sb.WriteString(desc + " [\n") for i, order := range orders { sb.WriteString(fmt.Sprintf(" - %d) %s\n", i, order.String())) } From 7af4e3bf8a37166fff9d24fa6f86ccd2c3882fa6 Mon Sep 17 00:00:00 2001 From: chiahung Date: Fri, 10 Mar 2023 13:58:04 +0800 Subject: [PATCH 0570/1392] FEATURE: get filled orders when bbgo down --- pkg/strategy/grid2/strategy.go | 119 +++++++++++++++++++++++++++- pkg/strategy/grid2/strategy_test.go | 33 +++++++- 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b5c39ce31e..af1e6ae284 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1400,9 +1400,36 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context } // 1. build pin-order map - // 2. fill the pin-order map by querying trades + pinOrderMapWithOpenOrders, err := s.buildPinOrderMap(grid, openOrdersOnGrid) + if err != nil { + return errors.Wrapf(err, "failed to build pin order map with open orders") + } + + // 2. build the filled pin-order map by querying trades + pinOrderMapWithFilledOrders, err := s.buildFilledPinOrderMapFromTrades(ctx, historyService, pinOrderMapWithOpenOrders) + if err != nil { + return errors.Wrapf(err, "failed to build filled pin order map") + } + // 3. get the filled orders from pin-order map + filledOrders := getOrdersFromPinOrderMapInAscOrder(pinOrderMapWithFilledOrders) + numsFilledOrders := len(filledOrders) + i := 0 + if numsFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { + // nums of filled order is the same as Size - 1 - num(open orders) + } else if numsFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) { + i++ + } else { + return fmt.Errorf("not reasonable num of filled orders") + } + // 4. emit the filled orders + activeOrderBook := s.orderExecutor.ActiveMakerOrders() + for ; i < numsFilledOrders; i++ { + filledOrder := filledOrders[i] + s.logger.Infof("[DEBUG] emit filled order: %s (%s)", filledOrder.String(), filledOrder.UpdateTime) + activeOrderBook.EmitFilled(filledOrder) + } // 5. emit grid ready s.EmitGridReady() @@ -1443,6 +1470,96 @@ func (s *Strategy) buildPinOrderMap(grid *Grid, openOrders []types.Order) (map[s return pinOrderMap, nil } +// buildFilledPinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map +// It will skip the orders on pins at which open orders are already +func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openPinOrderMap map[string]types.Order) (map[string]types.Order, error) { + filledPinOrderMap := make(map[string]types.Order) + var limit int64 = 1000 + // get the filled orders when bbgo is down in order from trades + // [NOTE] only retrieve from last 24 hours !!! + var fromTradeID uint64 = 0 + for { + trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ + LastTradeID: fromTradeID, + Limit: limit, + }) + + if err != nil { + return nil, errors.Wrapf(err, "failed to query trades to recover the grid with open orders") + } + + s.logger.Infof("[DEBUG] len of trades: %d", len(trades)) + + for _, trade := range trades { + order, err := s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ + OrderID: strconv.FormatUint(trade.OrderID, 10), + }) + + if err != nil { + return nil, errors.Wrapf(err, "failed to query order by trade") + } + + s.logger.Infof("[DEBUG] trade: %s", trade.String()) + s.logger.Infof("[DEBUG] (group_id: %d) order: %s", order.GroupID, order.String()) + + // add 1 to avoid duplicate + fromTradeID = trade.ID + 1 + + // this trade doesn't belong to this grid + if order.GroupID != s.OrderGroupID { + continue + } + + // checked the trade's order is filled order + priceStr := s.FormatPrice(order.Price) + v, exist := openPinOrderMap[priceStr] + if !exist { + return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") + } + + // skip open orders on grid + if v.OrderID != 0 { + continue + } + + // check the order's creation time + if pinOrder, exist := filledPinOrderMap[priceStr]; exist && pinOrder.CreationTime.Time().After(order.CreationTime.Time()) { + // do not replace the pin order if the order's creation time is not after pin order's creation time + // this situation should not happen actually, because the trades is already sorted. + s.logger.Infof("pinOrder's creation time (%s) should not be after order's creation time (%s)", pinOrder.CreationTime, order.CreationTime) + continue + } + filledPinOrderMap[priceStr] = *order + } + + // stop condition + if int64(len(trades)) < limit { + break + } + } + + return filledPinOrderMap, nil +} + +// get the orders from pin order map and sort it in asc order +func getOrdersFromPinOrderMapInAscOrder(pinOrderMap map[string]types.Order) []types.Order { + var orders []types.Order + for _, order := range pinOrderMap { + // skip empty order + if order.OrderID == 0 { + continue + } + + orders = append(orders, order) + } + + sort.Slice(orders, func(i, j int) bool { + return orders[i].UpdateTime.Before(orders[j].UpdateTime.Time()) + }) + + return orders +} + func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { grid := s.newGrid() diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 599a4bf1df..d25bccd149 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -6,6 +6,7 @@ import ( "context" "errors" "testing" + "time" "github.com/golang/mock/gomock" "github.com/sirupsen/logrus" @@ -1177,4 +1178,34 @@ func Test_buildPinOrderMap(t *testing.T) { _, err := s.buildPinOrderMap(s.grid, openOrders) assert.Error(err) }) -} \ No newline at end of file +} + +func Test_getOrdersFromPinOrderMapInAscOrder(t *testing.T) { + assert := assert.New(t) + now := time.Now() + pinOrderMap := map[string]types.Order{ + "1000": types.Order{ + OrderID: 1, + CreationTime: types.Time(now.Add(1 * time.Hour)), + UpdateTime: types.Time(now.Add(5 * time.Hour)), + }, + "1100": types.Order{}, + "1200": types.Order{}, + "1300": types.Order{ + OrderID: 3, + CreationTime: types.Time(now.Add(3 * time.Hour)), + UpdateTime: types.Time(now.Add(6 * time.Hour)), + }, + "1400": types.Order{ + OrderID: 2, + CreationTime: types.Time(now.Add(2 * time.Hour)), + UpdateTime: types.Time(now.Add(4 * time.Hour)), + }, + } + + orders := getOrdersFromPinOrderMapInAscOrder(pinOrderMap) + assert.Len(orders, 3) + assert.Equal(uint64(2), orders[0].OrderID) + assert.Equal(uint64(1), orders[1].OrderID) + assert.Equal(uint64(3), orders[2].OrderID) +} From 4af85231446df7cd0d7eea01c4c3e17cf03a7b1f Mon Sep 17 00:00:00 2001 From: chiahung Date: Mon, 13 Mar 2023 14:45:44 +0800 Subject: [PATCH 0571/1392] new struct PinOrderMap --- pkg/strategy/grid2/strategy.go | 56 +++++++++++++++-------------- pkg/strategy/grid2/strategy_test.go | 10 +++--- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index af1e6ae284..5e77da2e6c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -54,6 +54,28 @@ func (pp PrettyPins) String() string { return fmt.Sprintf("%v", ss) } +// PinOrderMap store the pin-order's relation, we will change key from string to fixedpoint.Value when FormatString fixed +type PinOrderMap map[string]types.Order + +// AscendingOrders get the orders from pin order map and sort it in asc order +func (m PinOrderMap) AscendingOrders() []types.Order { + var orders []types.Order + for _, order := range m { + // skip empty order + if order.OrderID == 0 { + continue + } + + orders = append(orders, order) + } + + sort.Slice(orders, func(i, j int) bool { + return orders[i].UpdateTime.Before(orders[j].UpdateTime.Time()) + }) + + return orders +} + //go:generate mockgen -destination=mocks/order_executor.go -package=mocks . OrderExecutor type OrderExecutor interface { SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) @@ -1400,7 +1422,7 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context } // 1. build pin-order map - pinOrderMapWithOpenOrders, err := s.buildPinOrderMap(grid, openOrdersOnGrid) + pinOrderMapWithOpenOrders, err := s.buildPinOrderMap(grid.Pins, openOrdersOnGrid) if err != nil { return errors.Wrapf(err, "failed to build pin order map with open orders") } @@ -1412,7 +1434,7 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context } // 3. get the filled orders from pin-order map - filledOrders := getOrdersFromPinOrderMapInAscOrder(pinOrderMapWithFilledOrders) + filledOrders := pinOrderMapWithFilledOrders.AscendingOrders() numsFilledOrders := len(filledOrders) i := 0 if numsFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { @@ -1445,10 +1467,10 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context // buildPinOrderMap build the pin-order map with grid and open orders. // The keys of this map contains all required pins of this grid. // If the Order of the pin is empty types.Order (OrderID == 0), it means there is no open orders at this pin. -func (s *Strategy) buildPinOrderMap(grid *Grid, openOrders []types.Order) (map[string]types.Order, error) { - pinOrderMap := make(map[string]types.Order) +func (s *Strategy) buildPinOrderMap(pins []Pin, openOrders []types.Order) (PinOrderMap, error) { + pinOrderMap := make(PinOrderMap) - for _, pin := range grid.Pins { + for _, pin := range pins { priceStr := s.FormatPrice(fixedpoint.Value(pin)) pinOrderMap[priceStr] = types.Order{} } @@ -1472,8 +1494,9 @@ func (s *Strategy) buildPinOrderMap(grid *Grid, openOrders []types.Order) (map[s // buildFilledPinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map // It will skip the orders on pins at which open orders are already -func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openPinOrderMap map[string]types.Order) (map[string]types.Order, error) { - filledPinOrderMap := make(map[string]types.Order) +func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openPinOrderMap PinOrderMap) (PinOrderMap, error) { + filledPinOrderMap := make(PinOrderMap) + var limit int64 = 1000 // get the filled orders when bbgo is down in order from trades // [NOTE] only retrieve from last 24 hours !!! @@ -1541,25 +1564,6 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return filledPinOrderMap, nil } -// get the orders from pin order map and sort it in asc order -func getOrdersFromPinOrderMapInAscOrder(pinOrderMap map[string]types.Order) []types.Order { - var orders []types.Order - for _, order := range pinOrderMap { - // skip empty order - if order.OrderID == 0 { - continue - } - - orders = append(orders, order) - } - - sort.Slice(orders, func(i, j int) bool { - return orders[i].UpdateTime.Before(orders[j].UpdateTime.Time()) - }) - - return orders -} - func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { grid := s.newGrid() diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index d25bccd149..46f90343b9 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -1095,7 +1095,7 @@ func Test_buildPinOrderMap(t *testing.T) { IsWorking: false, }, } - m, err := s.buildPinOrderMap(s.grid, openOrders) + m, err := s.buildPinOrderMap(s.grid.Pins, openOrders) assert.NoError(err) assert.Len(m, 11) @@ -1130,7 +1130,7 @@ func Test_buildPinOrderMap(t *testing.T) { IsWorking: false, }, } - _, err := s.buildPinOrderMap(s.grid, openOrders) + _, err := s.buildPinOrderMap(s.grid.Pins, openOrders) assert.Error(err) }) @@ -1175,7 +1175,7 @@ func Test_buildPinOrderMap(t *testing.T) { IsWorking: false, }, } - _, err := s.buildPinOrderMap(s.grid, openOrders) + _, err := s.buildPinOrderMap(s.grid.Pins, openOrders) assert.Error(err) }) } @@ -1183,7 +1183,7 @@ func Test_buildPinOrderMap(t *testing.T) { func Test_getOrdersFromPinOrderMapInAscOrder(t *testing.T) { assert := assert.New(t) now := time.Now() - pinOrderMap := map[string]types.Order{ + pinOrderMap := PinOrderMap{ "1000": types.Order{ OrderID: 1, CreationTime: types.Time(now.Add(1 * time.Hour)), @@ -1203,7 +1203,7 @@ func Test_getOrdersFromPinOrderMapInAscOrder(t *testing.T) { }, } - orders := getOrdersFromPinOrderMapInAscOrder(pinOrderMap) + orders := pinOrderMap.AscendingOrders() assert.Len(orders, 3) assert.Equal(uint64(2), orders[0].OrderID) assert.Equal(uint64(1), orders[1].OrderID) From 9da8c39d2c90468da4f8e251ef6080f732f22365 Mon Sep 17 00:00:00 2001 From: chiahung Date: Tue, 14 Mar 2023 13:46:46 +0800 Subject: [PATCH 0572/1392] avoid re-query same order --- pkg/strategy/grid2/pin_order_map.go | 34 ++++++++++++++++ pkg/strategy/grid2/strategy.go | 60 +++++++++++++---------------- 2 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 pkg/strategy/grid2/pin_order_map.go diff --git a/pkg/strategy/grid2/pin_order_map.go b/pkg/strategy/grid2/pin_order_map.go new file mode 100644 index 0000000000..9f650971cf --- /dev/null +++ b/pkg/strategy/grid2/pin_order_map.go @@ -0,0 +1,34 @@ +package grid2 + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +// PinOrderMap store the pin-order's relation, we will change key from string to fixedpoint.Value when FormatString fixed +type PinOrderMap map[string]types.Order + +// AscendingOrders get the orders from pin order map and sort it in asc order +func (m PinOrderMap) AscendingOrders() []types.Order { + var orders []types.Order + for _, order := range m { + // skip empty order + if order.OrderID == 0 { + continue + } + + orders = append(orders, order) + } + + types.SortOrdersUpdateTimeAscending(orders) + + return orders +} + +func (m PinOrderMap) buildSyncOrderMap() *types.SyncOrderMap { + orderMap := types.NewSyncOrderMap() + for _, order := range m { + orderMap.Add(order) + } + + return orderMap +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5e77da2e6c..e065def993 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -54,28 +54,6 @@ func (pp PrettyPins) String() string { return fmt.Sprintf("%v", ss) } -// PinOrderMap store the pin-order's relation, we will change key from string to fixedpoint.Value when FormatString fixed -type PinOrderMap map[string]types.Order - -// AscendingOrders get the orders from pin order map and sort it in asc order -func (m PinOrderMap) AscendingOrders() []types.Order { - var orders []types.Order - for _, order := range m { - // skip empty order - if order.OrderID == 0 { - continue - } - - orders = append(orders, order) - } - - sort.Slice(orders, func(i, j int) bool { - return orders[i].UpdateTime.Before(orders[j].UpdateTime.Time()) - }) - - return orders -} - //go:generate mockgen -destination=mocks/order_executor.go -package=mocks . OrderExecutor type OrderExecutor interface { SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) @@ -1422,19 +1400,19 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context } // 1. build pin-order map - pinOrderMapWithOpenOrders, err := s.buildPinOrderMap(grid.Pins, openOrdersOnGrid) + pinOrdersOpen, err := s.buildPinOrderMap(grid.Pins, openOrdersOnGrid) if err != nil { return errors.Wrapf(err, "failed to build pin order map with open orders") } // 2. build the filled pin-order map by querying trades - pinOrderMapWithFilledOrders, err := s.buildFilledPinOrderMapFromTrades(ctx, historyService, pinOrderMapWithOpenOrders) + pinOrdersFilled, err := s.buildFilledPinOrderMapFromTrades(ctx, historyService, pinOrdersOpen) if err != nil { return errors.Wrapf(err, "failed to build filled pin order map") } // 3. get the filled orders from pin-order map - filledOrders := pinOrderMapWithFilledOrders.AscendingOrders() + filledOrders := pinOrdersFilled.AscendingOrders() numsFilledOrders := len(filledOrders) i := 0 if numsFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { @@ -1471,12 +1449,12 @@ func (s *Strategy) buildPinOrderMap(pins []Pin, openOrders []types.Order) (PinOr pinOrderMap := make(PinOrderMap) for _, pin := range pins { - priceStr := s.FormatPrice(fixedpoint.Value(pin)) + priceStr := s.Market.FormatPrice(fixedpoint.Value(pin)) pinOrderMap[priceStr] = types.Order{} } for _, openOrder := range openOrders { - priceStr := s.FormatPrice(openOrder.Price) + priceStr := s.Market.FormatPrice(openOrder.Price) v, exist := pinOrderMap[priceStr] if !exist { return nil, fmt.Errorf("the price of the order (id: %d) is not in pins", openOrder.OrderID) @@ -1494,8 +1472,11 @@ func (s *Strategy) buildPinOrderMap(pins []Pin, openOrders []types.Order) (PinOr // buildFilledPinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map // It will skip the orders on pins at which open orders are already -func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openPinOrderMap PinOrderMap) (PinOrderMap, error) { - filledPinOrderMap := make(PinOrderMap) +func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, pinOrdersOpen PinOrderMap) (PinOrderMap, error) { + pinOrdersFilled := make(PinOrderMap) + + // existedOrders is used to avoid re-query the same orders + existedOrders := pinOrdersOpen.buildSyncOrderMap() var limit int64 = 1000 // get the filled orders when bbgo is down in order from trades @@ -1514,6 +1495,11 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history s.logger.Infof("[DEBUG] len of trades: %d", len(trades)) for _, trade := range trades { + if existedOrders.Exists(trade.OrderID) { + // already queries, skip + continue + } + order, err := s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ OrderID: strconv.FormatUint(trade.OrderID, 10), }) @@ -1525,6 +1511,9 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history s.logger.Infof("[DEBUG] trade: %s", trade.String()) s.logger.Infof("[DEBUG] (group_id: %d) order: %s", order.GroupID, order.String()) + // avoid query this order again + existedOrders.Add(*order) + // add 1 to avoid duplicate fromTradeID = trade.ID + 1 @@ -1534,8 +1523,8 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history } // checked the trade's order is filled order - priceStr := s.FormatPrice(order.Price) - v, exist := openPinOrderMap[priceStr] + priceStr := s.Market.FormatPrice(order.Price) + v, exist := pinOrdersOpen[priceStr] if !exist { return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") } @@ -1546,13 +1535,16 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history } // check the order's creation time - if pinOrder, exist := filledPinOrderMap[priceStr]; exist && pinOrder.CreationTime.Time().After(order.CreationTime.Time()) { + if pinOrder, exist := pinOrdersFilled[priceStr]; exist && pinOrder.CreationTime.Time().After(order.CreationTime.Time()) { // do not replace the pin order if the order's creation time is not after pin order's creation time // this situation should not happen actually, because the trades is already sorted. s.logger.Infof("pinOrder's creation time (%s) should not be after order's creation time (%s)", pinOrder.CreationTime, order.CreationTime) continue } - filledPinOrderMap[priceStr] = *order + pinOrdersFilled[priceStr] = *order + + // wait 100 ms to avoid rate limit + time.Sleep(100 * time.Millisecond) } // stop condition @@ -1561,7 +1553,7 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history } } - return filledPinOrderMap, nil + return pinOrdersFilled, nil } func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { From dce1e4c7d4a8efc7ca0a1c5e3d5a84444edcb776 Mon Sep 17 00:00:00 2001 From: chiahung Date: Tue, 14 Mar 2023 14:35:15 +0800 Subject: [PATCH 0573/1392] rename buildSyncOrderMap to SyncOrderMap --- pkg/strategy/grid2/pin_order_map.go | 2 +- pkg/strategy/grid2/strategy.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/pin_order_map.go b/pkg/strategy/grid2/pin_order_map.go index 9f650971cf..31fe48f5b0 100644 --- a/pkg/strategy/grid2/pin_order_map.go +++ b/pkg/strategy/grid2/pin_order_map.go @@ -24,7 +24,7 @@ func (m PinOrderMap) AscendingOrders() []types.Order { return orders } -func (m PinOrderMap) buildSyncOrderMap() *types.SyncOrderMap { +func (m PinOrderMap) SyncOrderMap() *types.SyncOrderMap { orderMap := types.NewSyncOrderMap() for _, order := range m { orderMap.Add(order) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e065def993..7b19a537c3 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1476,7 +1476,7 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history pinOrdersFilled := make(PinOrderMap) // existedOrders is used to avoid re-query the same orders - existedOrders := pinOrdersOpen.buildSyncOrderMap() + existedOrders := pinOrdersOpen.SyncOrderMap() var limit int64 = 1000 // get the filled orders when bbgo is down in order from trades From e0b445f1c1993307fb1ceeb83199596511114b43 Mon Sep 17 00:00:00 2001 From: chiahung Date: Tue, 14 Mar 2023 15:13:34 +0800 Subject: [PATCH 0574/1392] FEATURE: make MAX QueryTrades support start_time, end_time --- pkg/exchange/max/exchange.go | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 7aa223f1ac..3b5bfed877 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -780,6 +780,18 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, return allDeposits, err } +// QueryTrades +// For MAX API spec +// start_time and end_time need to be within 3 days +// without any parameters -> return trades within 24 hours +// give start_time or end_time -> ignore parameter from_id +// give start_time or from_id -> order by time asc +// give end_time -> order by time desc +// limit should b1 1~1000 +// For this QueryTrades spec (to be compatible with batch.TradeBatchQuery) +// give LastTradeID -> ignore start_time (but still can filter the end_time) +// give only end_time -> start_time will be set as end_time - 3 days +// without any parameters -> return trades within 24 hours func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { if err := tradeQueryLimiter.Wait(ctx); err != nil { return nil, err @@ -800,18 +812,35 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type req.Limit(1000) } - // MAX uses exclusive last trade ID - // the timestamp parameter is used for reverse order, we can't use it. + // If we use start_time as parameter, MAX will ignore from_id. + // However, we want to use from_id as main parameter for batch.TradeBatchQuery if options.LastTradeID > 0 { + // MAX uses inclusive last trade ID req.From(options.LastTradeID) + } else { + if options.StartTime != nil { + req.StartTime(*options.StartTime) + } else if options.EndTime != nil { + // if only give end_time, we automatically add start_time within 3 days limit + endTime := *options.EndTime + req.StartTime(endTime.Add(-72 * time.Hour)) + req.EndTime(endTime) + } } + // option's start_time and end_time need to be within 3 days + // so if the start_time and end_time is over 3 days, we only give start_time as parameter and then filter the time > end_time + maxTrades, err := req.Do(ctx) if err != nil { return nil, err } for _, t := range maxTrades { + if options.EndTime != nil && t.CreatedAt.Time().After(*options.EndTime) { + continue + } + localTrades, err := toGlobalTradeV3(t) if err != nil { log.WithError(err).Errorf("can not convert trade: %+v", t) From da48e0fc8584c08695621c50a367ce241ab569db Mon Sep 17 00:00:00 2001 From: chiahung Date: Tue, 14 Mar 2023 18:39:36 +0800 Subject: [PATCH 0575/1392] make end_time down to start_time + 3 days if end_time > start_time + 3 days --- pkg/exchange/batch/trade.go | 1 + pkg/exchange/max/exchange.go | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pkg/exchange/batch/trade.go b/pkg/exchange/batch/trade.go index 39fc5bd410..43603a407c 100644 --- a/pkg/exchange/batch/trade.go +++ b/pkg/exchange/batch/trade.go @@ -40,6 +40,7 @@ func (e TradeBatchQuery) Query(ctx context.Context, symbol string, options *type } return trade.Key().String() }, + JumpIfEmpty: 24 * time.Hour, } c = make(chan types.Trade, 100) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 3b5bfed877..ad58d05400 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -790,7 +790,6 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, // limit should b1 1~1000 // For this QueryTrades spec (to be compatible with batch.TradeBatchQuery) // give LastTradeID -> ignore start_time (but still can filter the end_time) -// give only end_time -> start_time will be set as end_time - 3 days // without any parameters -> return trades within 24 hours func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { if err := tradeQueryLimiter.Wait(ctx); err != nil { @@ -818,29 +817,30 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type // MAX uses inclusive last trade ID req.From(options.LastTradeID) } else { - if options.StartTime != nil { - req.StartTime(*options.StartTime) - } else if options.EndTime != nil { - // if only give end_time, we automatically add start_time within 3 days limit + // option's start_time and end_time need to be within 3 days + // so if the start_time and end_time is over 3 days, we make end_time down to start_time + 3 days + if options.StartTime != nil && options.EndTime != nil { endTime := *options.EndTime - req.StartTime(endTime.Add(-72 * time.Hour)) + startTime := *options.StartTime + if endTime.Sub(startTime) > 72*time.Hour { + startTime := *options.StartTime + endTime = startTime.Add(72 * time.Hour) + } + req.StartTime(startTime) req.EndTime(endTime) + } else if options.StartTime != nil { + req.StartTime(*options.StartTime) + } else if options.EndTime != nil { + req.EndTime(*options.EndTime) } } - // option's start_time and end_time need to be within 3 days - // so if the start_time and end_time is over 3 days, we only give start_time as parameter and then filter the time > end_time - maxTrades, err := req.Do(ctx) if err != nil { return nil, err } for _, t := range maxTrades { - if options.EndTime != nil && t.CreatedAt.Time().After(*options.EndTime) { - continue - } - localTrades, err := toGlobalTradeV3(t) if err != nil { log.WithError(err).Errorf("can not convert trade: %+v", t) From 0458858de01a4ff3db82e419cf1cba6b1e46a2c0 Mon Sep 17 00:00:00 2001 From: narumi Date: Tue, 14 Mar 2023 14:37:54 +0800 Subject: [PATCH 0576/1392] fix position and profitstats --- config/rebalance.yaml | 5 ++--- pkg/strategy/rebalance/order_executor_map.go | 3 +++ pkg/strategy/rebalance/position_map.go | 10 ++++++---- pkg/strategy/rebalance/profit_stats_map.go | 10 ++++++---- pkg/strategy/rebalance/strategy.go | 6 ++++-- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/config/rebalance.yaml b/config/rebalance.yaml index 8a6f7f8bd2..14a784c931 100644 --- a/config/rebalance.yaml +++ b/config/rebalance.yaml @@ -33,10 +33,9 @@ exchangeStrategies: targetWeights: BTC: 50% ETH: 25% - MAX: 15% - USDT: 10% + USDT: 25% threshold: 1% maxAmount: 1_000 # max amount to buy or sell per order orderType: LIMIT_MAKER # LIMIT, LIMIT_MAKER or MARKET dryRun: false - onStart: false + onStart: true diff --git a/pkg/strategy/rebalance/order_executor_map.go b/pkg/strategy/rebalance/order_executor_map.go index 45fd3272e3..a4d0d386f9 100644 --- a/pkg/strategy/rebalance/order_executor_map.go +++ b/pkg/strategy/rebalance/order_executor_map.go @@ -14,6 +14,7 @@ func NewGeneralOrderExecutorMap(session *bbgo.ExchangeSession, positionMap Posit m := make(GeneralOrderExecutorMap) for symbol, position := range positionMap { + log.Infof("creating order executor for symbol %s", symbol) orderExecutor := bbgo.NewGeneralOrderExecutor(session, symbol, ID, instanceID(symbol), position) m[symbol] = orderExecutor } @@ -29,6 +30,7 @@ func (m GeneralOrderExecutorMap) BindEnvironment(environ *bbgo.Environment) { func (m GeneralOrderExecutorMap) BindProfitStats(profitStatsMap ProfitStatsMap) { for symbol, orderExecutor := range m { + log.Infof("binding profit stats for symbol %s", symbol) orderExecutor.BindProfitStats(profitStatsMap[symbol]) } } @@ -50,6 +52,7 @@ func (m GeneralOrderExecutorMap) Sync(ctx context.Context, obj interface{}) { func (m GeneralOrderExecutorMap) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { var allCreatedOrders types.OrderSlice for _, submitOrder := range submitOrders { + log.Infof("submitting order: %+v", submitOrder) orderExecutor, ok := m[submitOrder.Symbol] if !ok { return nil, fmt.Errorf("order executor not found for symbol %s", submitOrder.Symbol) diff --git a/pkg/strategy/rebalance/position_map.go b/pkg/strategy/rebalance/position_map.go index 162da96f95..5bbbfeb9ca 100644 --- a/pkg/strategy/rebalance/position_map.go +++ b/pkg/strategy/rebalance/position_map.go @@ -6,15 +6,17 @@ import ( type PositionMap map[string]*types.Position -func NewPositionMap(markets []types.Market) PositionMap { - m := make(PositionMap) - +func (m PositionMap) createPositions(markets []types.Market) PositionMap { for _, market := range markets { + if _, ok := m[market.Symbol]; ok { + continue + } + + log.Infof("creating position for symbol %s", market.Symbol) position := types.NewPositionFromMarket(market) position.Strategy = ID position.StrategyInstanceID = instanceID(market.Symbol) m[market.Symbol] = position } - return m } diff --git a/pkg/strategy/rebalance/profit_stats_map.go b/pkg/strategy/rebalance/profit_stats_map.go index eba460be03..b7db6c2fb1 100644 --- a/pkg/strategy/rebalance/profit_stats_map.go +++ b/pkg/strategy/rebalance/profit_stats_map.go @@ -4,12 +4,14 @@ import "github.com/c9s/bbgo/pkg/types" type ProfitStatsMap map[string]*types.ProfitStats -func NewProfitStatsMap(markets []types.Market) ProfitStatsMap { - m := make(ProfitStatsMap) - +func (m ProfitStatsMap) createProfitStats(markets []types.Market) ProfitStatsMap { for _, market := range markets { + if _, ok := m[market.Symbol]; ok { + continue + } + + log.Infof("creating profit stats for symbol %s", market.Symbol) m[market.Symbol] = types.NewProfitStats(market) } - return m } diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index 1774fc7d39..e9650b7047 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -99,12 +99,14 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } if s.PositionMap == nil { - s.PositionMap = NewPositionMap(markets) + s.PositionMap = make(PositionMap) } + s.PositionMap.createPositions(markets) if s.ProfitStatsMap == nil { - s.ProfitStatsMap = NewProfitStatsMap(markets) + s.ProfitStatsMap = make(ProfitStatsMap) } + s.ProfitStatsMap.createProfitStats(markets) s.orderExecutorMap = NewGeneralOrderExecutorMap(session, s.PositionMap) s.orderExecutorMap.BindEnvironment(s.Environment) From ca4890425c19005fb69f965aa1c21610e91b850e Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 15 Mar 2023 10:57:18 +0800 Subject: [PATCH 0577/1392] fix/bollmaker: MinProfitActivationRate is disabled if it's not set --- pkg/strategy/bollmaker/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 71003d345e..a8ea3837dc 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -83,7 +83,7 @@ type Strategy struct { MinProfitSpread fixedpoint.Value `json:"minProfitSpread"` // MinProfitActivationRate activates MinProfitSpread when position RoI higher than the specified percentage - MinProfitActivationRate fixedpoint.Value `json:"minProfitActivationRate"` + MinProfitActivationRate *fixedpoint.Value `json:"minProfitActivationRate"` // UseTickerPrice use the ticker api to get the mid price instead of the closed kline price. // The back-test engine is kline-based, so the ticker price api is not supported. @@ -385,7 +385,7 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k isLongPosition := s.Position.IsLong() isShortPosition := s.Position.IsShort() - if s.Position.ROI(midPrice).Compare(s.MinProfitActivationRate) >= 0 { + if s.MinProfitActivationRate == nil || s.Position.ROI(midPrice).Compare(*s.MinProfitActivationRate) >= 0 { minProfitPrice := s.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread)) if isShortPosition { minProfitPrice = s.Position.AverageCost.Mul(fixedpoint.One.Sub(s.MinProfitSpread)) From 0a6c41cfe7e2c74b52b0d207b1be4f3363dc9861 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 15 Mar 2023 11:06:26 +0800 Subject: [PATCH 0578/1392] fix/bollmaker: fix s.MinProfitActivationRate condition --- pkg/strategy/bollmaker/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index a8ea3837dc..ccbdc47a86 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -85,7 +85,7 @@ type Strategy struct { // MinProfitActivationRate activates MinProfitSpread when position RoI higher than the specified percentage MinProfitActivationRate *fixedpoint.Value `json:"minProfitActivationRate"` - // UseTickerPrice use the ticker api to get the mid price instead of the closed kline price. + // UseTickerPrice use the ticker api to get the mid-price instead of the closed kline price. // The back-test engine is kline-based, so the ticker price api is not supported. // Turn this on if you want to do real trading. UseTickerPrice bool `json:"useTickerPrice"` @@ -336,7 +336,7 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k // Apply quantity skew // CASE #1: - // WHEN: price is in the neutral bollginer band (window 1) == neutral + // WHEN: price is in the neutral bollinger band (window 1) == neutral // THEN: we don't apply skew // CASE #2: // WHEN: price is in the upper band (window 2 > price > window 1) == upTrend @@ -385,7 +385,7 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k isLongPosition := s.Position.IsLong() isShortPosition := s.Position.IsShort() - if s.MinProfitActivationRate == nil || s.Position.ROI(midPrice).Compare(*s.MinProfitActivationRate) >= 0 { + if s.MinProfitActivationRate == nil || (s.MinProfitActivationRate != nil && s.Position.ROI(midPrice).Compare(*s.MinProfitActivationRate) >= 0) { minProfitPrice := s.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread)) if isShortPosition { minProfitPrice = s.Position.AverageCost.Mul(fixedpoint.One.Sub(s.MinProfitSpread)) From af847f76ec710a5879cf72e230955e84d67696c1 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Mar 2023 13:22:34 +0800 Subject: [PATCH 0579/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index ea110d2a0f..c8f86f90d3 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -60,4 +60,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index b1173b9824..4d6ee00c88 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -43,4 +43,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index e35345e166..807af1d6d9 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -52,4 +52,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index c22fff8b54..86f5efdb4c 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -42,4 +42,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index b6c5608ce3..33347ca691 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -41,4 +41,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index 4314f684eb..15117c9c8f 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -51,4 +51,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index 1b0e09363a..78e9aa7117 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -43,4 +43,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 2918dfe8af..e8d0b330e6 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -50,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 8158237c42..0727e3a193 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -44,4 +44,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index 3566527530..ae10a63de5 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -47,4 +47,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 5ccd16c2f0..be954a0731 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -44,4 +44,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index aff25a102b..347aca129d 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -43,4 +43,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index 91722cc1b8..031983372f 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -40,4 +40,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index 4d8edfd95f..f733599759 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -43,4 +43,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 1f982537cc..fe3223548b 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -43,4 +43,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index 376377e89d..91bad8bcd0 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -43,4 +43,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index beffa4c021..b2896f321e 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -42,4 +42,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index bb5712a6fa..3f980b451e 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -46,4 +46,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index fca07896b1..ae8c309306 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -44,4 +44,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index 1e958cf703..c98929544c 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -42,4 +42,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index 5aeb284a5c..6414e0ca61 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -51,4 +51,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 006037c555..ec6adf8364 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -53,4 +53,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 332491d7d2..da45e54018 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -48,4 +48,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index a4002afd2c..20c8715f07 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -44,4 +44,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index 73f9463abd..62700541bb 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -44,4 +44,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 1e6b18a1a9..50be5401e3 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -42,4 +42,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 7427b776e9..131aa464f8 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -44,4 +44,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index 7fb0cd60c7..a0d19c6d38 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -42,4 +42,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index bf764e3f32..c55dbdf189 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -41,4 +41,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 13-Mar-2023 +###### Auto generated by spf13/cobra on 15-Mar-2023 From 40040ff3993f78731523fd983cf054e582e71a6b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Mar 2023 13:22:35 +0800 Subject: [PATCH 0580/1392] bump version to v1.44.1 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index d98e155dfa..7efd9e3c48 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.44.0-4b3f00fe-dev" +const Version = "v1.44.1-7d91fd01-dev" -const VersionGitRef = "4b3f00fe" +const VersionGitRef = "7d91fd01" diff --git a/pkg/version/version.go b/pkg/version/version.go index bd31cbf394..441ce99c19 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.44.0-4b3f00fe" +const Version = "v1.44.1-7d91fd01" -const VersionGitRef = "4b3f00fe" +const VersionGitRef = "7d91fd01" From 4c6fc4a68d9e3b00b90f029fb5277f317f949ecc Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Mar 2023 13:22:35 +0800 Subject: [PATCH 0581/1392] add v1.44.1 release note --- doc/release/v1.44.1.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/release/v1.44.1.md diff --git a/doc/release/v1.44.1.md b/doc/release/v1.44.1.md new file mode 100644 index 0000000000..ffa3f1d33f --- /dev/null +++ b/doc/release/v1.44.1.md @@ -0,0 +1,7 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.44.0...main) + + - [#1109](https://github.com/c9s/bbgo/pull/1109): FIX: rebalance: fix positions and profit stats map + - [#1110](https://github.com/c9s/bbgo/pull/1110): FIX: [bollmaker] MinProfitActivationRate is disabled if it's not set + - [#1107](https://github.com/c9s/bbgo/pull/1107): FEATURE: make MAX QueryTrades support start_time, end_time + - [#1097](https://github.com/c9s/bbgo/pull/1097): FEATURE: get filled orders when bbgo down + - [#1104](https://github.com/c9s/bbgo/pull/1104): FIX: rebalance: adjust max amount by balance From 0882bc49605504a2dcab4f2aa05dcf786e8e8dfc Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Mar 2023 13:26:27 +0800 Subject: [PATCH 0582/1392] bollmaker: log submit order error --- pkg/strategy/bollmaker/strategy.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index ccbdc47a86..92b3fc4af7 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -444,7 +444,10 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k submitOrders[i] = adjustOrderQuantity(submitOrders[i], s.Market) } - _, _ = s.orderExecutor.SubmitOrders(ctx, submitOrders...) + _, err = s.orderExecutor.SubmitOrders(ctx, submitOrders...) + if err != nil { + log.WithError(err).Errorf("submit order error") + } } func (s *Strategy) hasLongSet() bool { From 0f9319a2f5e3d57cea6462e957eb6148b2705d22 Mon Sep 17 00:00:00 2001 From: narumi Date: Wed, 15 Mar 2023 16:01:13 +0800 Subject: [PATCH 0583/1392] make CreatePositions and CreateProfitStats public --- pkg/strategy/rebalance/position_map.go | 2 +- pkg/strategy/rebalance/profit_stats_map.go | 2 +- pkg/strategy/rebalance/strategy.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/rebalance/position_map.go b/pkg/strategy/rebalance/position_map.go index 5bbbfeb9ca..772d1726ce 100644 --- a/pkg/strategy/rebalance/position_map.go +++ b/pkg/strategy/rebalance/position_map.go @@ -6,7 +6,7 @@ import ( type PositionMap map[string]*types.Position -func (m PositionMap) createPositions(markets []types.Market) PositionMap { +func (m PositionMap) CreatePositions(markets []types.Market) PositionMap { for _, market := range markets { if _, ok := m[market.Symbol]; ok { continue diff --git a/pkg/strategy/rebalance/profit_stats_map.go b/pkg/strategy/rebalance/profit_stats_map.go index b7db6c2fb1..a84bf5cc90 100644 --- a/pkg/strategy/rebalance/profit_stats_map.go +++ b/pkg/strategy/rebalance/profit_stats_map.go @@ -4,7 +4,7 @@ import "github.com/c9s/bbgo/pkg/types" type ProfitStatsMap map[string]*types.ProfitStats -func (m ProfitStatsMap) createProfitStats(markets []types.Market) ProfitStatsMap { +func (m ProfitStatsMap) CreateProfitStats(markets []types.Market) ProfitStatsMap { for _, market := range markets { if _, ok := m[market.Symbol]; ok { continue diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index e9650b7047..e15e2507fe 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -101,12 +101,12 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. if s.PositionMap == nil { s.PositionMap = make(PositionMap) } - s.PositionMap.createPositions(markets) + s.PositionMap.CreatePositions(markets) if s.ProfitStatsMap == nil { s.ProfitStatsMap = make(ProfitStatsMap) } - s.ProfitStatsMap.createProfitStats(markets) + s.ProfitStatsMap.CreateProfitStats(markets) s.orderExecutorMap = NewGeneralOrderExecutorMap(session, s.PositionMap) s.orderExecutorMap.BindEnvironment(s.Environment) From 891cac06406769547317491bd8d8d59f80b69c29 Mon Sep 17 00:00:00 2001 From: chiahung Date: Wed, 15 Mar 2023 17:29:17 +0800 Subject: [PATCH 0584/1392] FIX: fix wrong fee currency --- pkg/strategy/grid2/strategy.go | 1 + pkg/strategy/grid2/trade.go | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7b19a537c3..b158ea7db1 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -360,6 +360,7 @@ func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { if o.Side == types.SideTypeSell { feeCurrency = s.Market.QuoteCurrency } + feeCurrency = strings.ToUpper(feeCurrency) for maxTries := maxNumberOfOrderTradesQueryTries; maxTries > 0; maxTries-- { // if one of the trades is missing, we need to query the trades from the RESTful API diff --git a/pkg/strategy/grid2/trade.go b/pkg/strategy/grid2/trade.go index 381744ccd6..f0bbcd1a97 100644 --- a/pkg/strategy/grid2/trade.go +++ b/pkg/strategy/grid2/trade.go @@ -1,6 +1,8 @@ package grid2 import ( + "strings" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -9,10 +11,11 @@ import ( func collectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { fees := make(map[string]fixedpoint.Value) for _, t := range trades { - if fee, ok := fees[t.FeeCurrency]; ok { - fees[t.FeeCurrency] = fee.Add(t.Fee) + feeCurrency := strings.ToUpper(t.FeeCurrency) + if fee, ok := fees[feeCurrency]; ok { + fees[feeCurrency] = fee.Add(t.Fee) } else { - fees[t.FeeCurrency] = t.Fee + fees[feeCurrency] = t.Fee } } return fees From 26054e4958ae422aacc355eb57805c4cf93836e1 Mon Sep 17 00:00:00 2001 From: chiahung Date: Wed, 15 Mar 2023 18:09:46 +0800 Subject: [PATCH 0585/1392] fix on max api level --- pkg/exchange/max/convert.go | 2 +- pkg/strategy/grid2/strategy.go | 1 - pkg/strategy/grid2/trade.go | 9 +++------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index fc169fac44..6047f5ee63 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -226,7 +226,7 @@ func toGlobalTradeV3(t v3.Trade) ([]types.Trade, error) { bidTrade.Side = types.SideTypeBuy bidTrade.OrderID = t.SelfTradeBidOrderID bidTrade.Fee = t.SelfTradeBidFee - bidTrade.FeeCurrency = t.SelfTradeBidFeeCurrency + bidTrade.FeeCurrency = toGlobalCurrency(t.SelfTradeBidFeeCurrency) bidTrade.IsBuyer = !trade.IsBuyer bidTrade.IsMaker = !trade.IsMaker trades = append(trades, bidTrade) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b158ea7db1..7b19a537c3 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -360,7 +360,6 @@ func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { if o.Side == types.SideTypeSell { feeCurrency = s.Market.QuoteCurrency } - feeCurrency = strings.ToUpper(feeCurrency) for maxTries := maxNumberOfOrderTradesQueryTries; maxTries > 0; maxTries-- { // if one of the trades is missing, we need to query the trades from the RESTful API diff --git a/pkg/strategy/grid2/trade.go b/pkg/strategy/grid2/trade.go index f0bbcd1a97..381744ccd6 100644 --- a/pkg/strategy/grid2/trade.go +++ b/pkg/strategy/grid2/trade.go @@ -1,8 +1,6 @@ package grid2 import ( - "strings" - "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -11,11 +9,10 @@ import ( func collectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { fees := make(map[string]fixedpoint.Value) for _, t := range trades { - feeCurrency := strings.ToUpper(t.FeeCurrency) - if fee, ok := fees[feeCurrency]; ok { - fees[feeCurrency] = fee.Add(t.Fee) + if fee, ok := fees[t.FeeCurrency]; ok { + fees[t.FeeCurrency] = fee.Add(t.Fee) } else { - fees[feeCurrency] = t.Fee + fees[t.FeeCurrency] = t.Fee } } return fees From e686a26dda7b2099870ca4e5af2c79b93628e57b Mon Sep 17 00:00:00 2001 From: chiahung Date: Wed, 15 Mar 2023 20:15:53 +0800 Subject: [PATCH 0586/1392] FEATURE: verify the grids before emit filled orders --- pkg/strategy/grid2/strategy.go | 69 +++++++-- pkg/strategy/grid2/strategy_test.go | 211 ++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7b19a537c3..266fe2aad7 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1397,6 +1397,8 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context if expectedOrderNums == openOrdersOnGridNums { // no need to recover return nil + } else if expectedOrderNums < openOrdersOnGridNums { + return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") } // 1. build pin-order map @@ -1414,27 +1416,30 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context // 3. get the filled orders from pin-order map filledOrders := pinOrdersFilled.AscendingOrders() numsFilledOrders := len(filledOrders) - i := 0 if numsFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { // nums of filled order is the same as Size - 1 - num(open orders) } else if numsFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) { - i++ + filledOrders = filledOrders[1:] } else { return fmt.Errorf("not reasonable num of filled orders") } - // 4. emit the filled orders + // 4. verify the grid + if err := s.verifyFilledGrid(s.grid.Pins, pinOrdersOpen, filledOrders); err != nil { + return errors.Wrapf(err, "verify grid with error") + } + + // 5. emit the filled orders activeOrderBook := s.orderExecutor.ActiveMakerOrders() - for ; i < numsFilledOrders; i++ { - filledOrder := filledOrders[i] + for _, filledOrder := range filledOrders { s.logger.Infof("[DEBUG] emit filled order: %s (%s)", filledOrder.String(), filledOrder.UpdateTime) activeOrderBook.EmitFilled(filledOrder) } - // 5. emit grid ready + // 6. emit grid ready s.EmitGridReady() - // 6. debug and send metrics + // 7. debug and send metrics debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) @@ -1442,6 +1447,50 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context return nil } +func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { + for _, filledOrder := range filledOrders { + price := s.Market.FormatPrice(filledOrder.Price) + if o, exist := pinOrders[price]; !exist { + return fmt.Errorf("the price (%s) is not in pins", price) + } else if o.OrderID != 0 { + return fmt.Errorf("there is already an order at this price (%s)", price) + } else { + pinOrders[price] = filledOrder + } + } + + side := types.SideTypeBuy + for _, pin := range pins { + price := s.Market.FormatPrice(fixedpoint.Value(pin)) + order, exist := pinOrders[price] + if !exist { + return fmt.Errorf("there is no order at price (%s)", price) + } + + // if there is order with OrderID = 0, means we hit the empty pin + // there must be only one empty pin in the grid + // all orders below this pin need to be bid orders, above this pin need to be ask orders + if order.OrderID == 0 { + if side == types.SideTypeBuy { + side = types.SideTypeSell + continue + } + + return fmt.Errorf("not only one empty order in this grid") + } + + if order.Side != side { + return fmt.Errorf("the side is wrong !!!") + } + } + + if side != types.SideTypeSell { + return fmt.Errorf("there is no empty pin in the grid !") + } + + return nil +} + // buildPinOrderMap build the pin-order map with grid and open orders. // The keys of this map contains all required pins of this grid. // If the Order of the pin is empty types.Order (OrderID == 0), it means there is no open orders at this pin. @@ -1495,6 +1544,7 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history s.logger.Infof("[DEBUG] len of trades: %d", len(trades)) for _, trade := range trades { + s.logger.Infof("[DEBUG] %s", trade.String()) if existedOrders.Exists(trade.OrderID) { // already queries, skip continue @@ -1508,8 +1558,7 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return nil, errors.Wrapf(err, "failed to query order by trade") } - s.logger.Infof("[DEBUG] trade: %s", trade.String()) - s.logger.Infof("[DEBUG] (group_id: %d) order: %s", order.GroupID, order.String()) + s.logger.Infof("[DEBUG] order: %s (group_id: %d)", order.String(), order.GroupID) // avoid query this order again existedOrders.Add(*order) @@ -2083,9 +2132,11 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { if s.RecoverGridByScanningTrades { + s.logger.Infof("[DEBUG] recover grid by scanning trades") return s.recoverGridByScanningTrades(ctx, session) } + s.logger.Infof("[DEBUG] recover grid by scanning orders") return s.recoverGridByScanningOrders(ctx, session) } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 46f90343b9..63631fcb64 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -1209,3 +1209,214 @@ func Test_getOrdersFromPinOrderMapInAscOrder(t *testing.T) { assert.Equal(uint64(1), orders[1].OrderID) assert.Equal(uint64(3), orders[2].OrderID) } + +func Test_verifyFilledGrid(t *testing.T) { + assert := assert.New(t) + s := newTestStrategy() + s.UpperPrice = number(400.0) + s.LowerPrice = number(100.0) + s.GridNum = 4 + s.grid = s.newGrid() + + t.Run("valid grid with buy/sell orders", func(t *testing.T) { + pinOrderMap := PinOrderMap{ + "100.00": types.Order{ + OrderID: 1, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + "200.00": types.Order{}, + "300.00": types.Order{ + OrderID: 3, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + "400.00": types.Order{ + OrderID: 4, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + } + + assert.NoError(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) + }) + t.Run("valid grid with only buy orders", func(t *testing.T) { + pinOrderMap := PinOrderMap{ + "100.00": types.Order{ + OrderID: 1, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + "200.00": types.Order{ + OrderID: 2, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + "300.00": types.Order{ + OrderID: 3, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + "400.00": types.Order{}, + } + + assert.NoError(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) + }) + t.Run("valid grid with only sell orders", func(t *testing.T) { + pinOrderMap := PinOrderMap{ + "100.00": types.Order{}, + "200.00": types.Order{ + OrderID: 2, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + "300.00": types.Order{ + OrderID: 3, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + "400.00": types.Order{ + OrderID: 4, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + } + + assert.NoError(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) + }) + t.Run("invalid grid with multiple empty pins", func(t *testing.T) { + pinOrderMap := PinOrderMap{ + "100.00": types.Order{ + OrderID: 1, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + "200.00": types.Order{}, + "300.00": types.Order{}, + "400.00": types.Order{ + OrderID: 4, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + } + + assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) + }) + t.Run("invalid grid without empty pin", func(t *testing.T) { + pinOrderMap := PinOrderMap{ + "100.00": types.Order{ + OrderID: 1, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + "200.00": types.Order{ + OrderID: 2, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + "300.00": types.Order{ + OrderID: 3, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + "400.00": types.Order{ + OrderID: 4, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + } + + assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) + }) + t.Run("invalid grid with Buy-empty-Sell-Buy order", func(t *testing.T) { + pinOrderMap := PinOrderMap{ + "100.00": types.Order{ + OrderID: 1, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + "200.00": types.Order{}, + "300.00": types.Order{ + OrderID: 3, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + "400.00": types.Order{ + OrderID: 4, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + } + + assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) + }) + t.Run("invalid grid with Sell-empty order", func(t *testing.T) { + pinOrderMap := PinOrderMap{ + "100.00": types.Order{ + OrderID: 1, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + "200.00": types.Order{ + OrderID: 2, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + "300.00": types.Order{ + OrderID: 3, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeSell, + }, + }, + "400.00": types.Order{}, + } + + assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) + }) + t.Run("invalid grid with empty-Buy order", func(t *testing.T) { + pinOrderMap := PinOrderMap{ + "100.00": types.Order{}, + "200.00": types.Order{ + OrderID: 2, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + "300.00": types.Order{ + OrderID: 3, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + "400.00": types.Order{ + OrderID: 4, + SubmitOrder: types.SubmitOrder{ + Side: types.SideTypeBuy, + }, + }, + } + + assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) + }) + +} From f987c85f17bdf642390b775c94e91d04a2ba2ad7 Mon Sep 17 00:00:00 2001 From: chiahung Date: Wed, 15 Mar 2023 21:12:59 +0800 Subject: [PATCH 0587/1392] move info log to debug log --- pkg/strategy/grid2/strategy.go | 38 ++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 266fe2aad7..902d1ba772 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1393,7 +1393,7 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context expectedOrderNums := s.GridNum - 1 openOrdersOnGridNums := int64(len(openOrdersOnGrid)) - s.logger.Infof("[DEBUG] open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) + s.logger.Debugf("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) if expectedOrderNums == openOrdersOnGridNums { // no need to recover return nil @@ -1415,10 +1415,10 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context // 3. get the filled orders from pin-order map filledOrders := pinOrdersFilled.AscendingOrders() - numsFilledOrders := len(filledOrders) - if numsFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { + numFilledOrders := len(filledOrders) + if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { // nums of filled order is the same as Size - 1 - num(open orders) - } else if numsFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) { + } else if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) { filledOrders = filledOrders[1:] } else { return fmt.Errorf("not reasonable num of filled orders") @@ -1429,10 +1429,11 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context return errors.Wrapf(err, "verify grid with error") } + s.logger.Debugf("emit filled orders %+v", filledOrders) + // 5. emit the filled orders activeOrderBook := s.orderExecutor.ActiveMakerOrders() for _, filledOrder := range filledOrders { - s.logger.Infof("[DEBUG] emit filled order: %s (%s)", filledOrder.String(), filledOrder.UpdateTime) activeOrderBook.EmitFilled(filledOrder) } @@ -1440,6 +1441,8 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context s.EmitGridReady() // 7. debug and send metrics + // wait for the reverse order to be placed + time.Sleep(2 * time.Second) debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) @@ -1448,6 +1451,10 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context } func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { + s.logger.Debugf("pins: %+v", pins) + s.logger.Debugf("open pin orders: %+v", pinOrders) + s.logger.Debugf("filled orders: %+v", filledOrders) + for _, filledOrder := range filledOrders { price := s.Market.FormatPrice(filledOrder.Price) if o, exist := pinOrders[price]; !exist { @@ -1459,6 +1466,8 @@ func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrd } } + s.logger.Debugf("filled pin orders: %+v", pinOrders) + side := types.SideTypeBuy for _, pin := range pins { price := s.Market.FormatPrice(fixedpoint.Value(pin)) @@ -1480,12 +1489,12 @@ func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrd } if order.Side != side { - return fmt.Errorf("the side is wrong !!!") + return fmt.Errorf("the side is wrong") } } if side != types.SideTypeSell { - return fmt.Errorf("there is no empty pin in the grid !") + return fmt.Errorf("there is no empty pin in the grid") } return nil @@ -1541,10 +1550,10 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return nil, errors.Wrapf(err, "failed to query trades to recover the grid with open orders") } - s.logger.Infof("[DEBUG] len of trades: %d", len(trades)) + s.logger.Debugf("QueryTrades return %d trades", len(trades)) for _, trade := range trades { - s.logger.Infof("[DEBUG] %s", trade.String()) + s.logger.Debugf(trade.String()) if existedOrders.Exists(trade.OrderID) { // already queries, skip continue @@ -1558,7 +1567,7 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return nil, errors.Wrapf(err, "failed to query order by trade") } - s.logger.Infof("[DEBUG] order: %s (group_id: %d)", order.String(), order.GroupID) + s.logger.Debugf("%s (group_id: %d)", order.String(), order.GroupID) // avoid query this order again existedOrders.Add(*order) @@ -2132,11 +2141,11 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { if s.RecoverGridByScanningTrades { - s.logger.Infof("[DEBUG] recover grid by scanning trades") + s.logger.Debugf("recover grid by scanning trades") return s.recoverGridByScanningTrades(ctx, session) } - s.logger.Infof("[DEBUG] recover grid by scanning orders") + s.logger.Debugf("recover grid by scanning orders") return s.recoverGridByScanningOrders(ctx, session) } @@ -2170,7 +2179,7 @@ func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbg func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { // no initial order id means we don't need to recover if s.GridProfitStats.InitialOrderID == 0 { - s.logger.Info("[DEBUG] new strategy, no need to recover") + s.logger.Debug("new strategy, no need to recover") return nil } @@ -2181,11 +2190,10 @@ func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbg s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) - s.logger.Infof("[DEBUG] recover grid with group id: %d", s.OrderGroupID) + s.logger.Debugf("recover grid with group id: %d", s.OrderGroupID) // filter out the order with the group id belongs to this grid var openOrdersOnGrid []types.Order for _, order := range openOrders { - s.logger.Infof("[DEBUG] order (%d) group id: %d", order.OrderID, order.GroupID) if order.GroupID == s.OrderGroupID { openOrdersOnGrid = append(openOrdersOnGrid, order) } From ffdc242f66378659013a916d51637577fe158a02 Mon Sep 17 00:00:00 2001 From: chiahung Date: Wed, 15 Mar 2023 21:34:04 +0800 Subject: [PATCH 0588/1392] use debugOrders --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 902d1ba772..ebed906869 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1429,7 +1429,7 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context return errors.Wrapf(err, "verify grid with error") } - s.logger.Debugf("emit filled orders %+v", filledOrders) + s.debugOrders("emit filled orders", filledOrders) // 5. emit the filled orders activeOrderBook := s.orderExecutor.ActiveMakerOrders() @@ -1453,7 +1453,7 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { s.logger.Debugf("pins: %+v", pins) s.logger.Debugf("open pin orders: %+v", pinOrders) - s.logger.Debugf("filled orders: %+v", filledOrders) + s.debugOrders("filled orders", filledOrders) for _, filledOrder := range filledOrders { price := s.Market.FormatPrice(filledOrder.Price) From 74c465d943b14a822eb0c27ea52b61b62ff42f7e Mon Sep 17 00:00:00 2001 From: gx578007 Date: Wed, 15 Mar 2023 21:40:44 +0800 Subject: [PATCH 0589/1392] FIX: [grid2] fix correct price metrics --- pkg/strategy/grid2/strategy.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 41e9b14294..89d0d64c49 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1122,7 +1122,14 @@ func (s *Strategy) updateGridNumOfOrdersMetrics(grid *Grid) { metricsGridNumOfMissingOrders.With(baseLabels).Set(float64(numOfMissingOrders)) var numOfOrdersWithCorrectPrice int + priceSet := make(map[fixedpoint.Value]struct{}) for _, order := range makerOrders.Orders() { + // filter out duplicated prices + if _, ok := priceSet[order.Price]; ok { + continue + } + priceSet[order.Price] = struct{}{} + if grid.HasPin(Pin(order.Price)) { numOfOrdersWithCorrectPrice++ } From 2378951c8542624f58f6a810222edfb3397b9c07 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Mar 2023 22:47:40 +0800 Subject: [PATCH 0590/1392] bbgo: should get isolation from the ctx when saving state --- pkg/bbgo/trader.go | 12 ++++++------ pkg/cmd/run.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index b8cecdea5a..836d86c9bf 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -88,6 +88,8 @@ type Trader struct { crossExchangeStrategies []CrossExchangeStrategy exchangeStrategies map[string][]SingleExchangeStrategy + // gracefulShutdown is used for registering strategy's Shutdown calls + // when strategy implements Shutdown(ctx), the func ref will be stored in the callback. gracefulShutdown GracefulShutdown logger Logger @@ -400,18 +402,16 @@ func (trader *Trader) IterateStrategies(f func(st StrategyID) error) error { return nil } -func (trader *Trader) SaveState() error { +func (trader *Trader) SaveState(ctx context.Context) error { if trader.environment.BacktestService != nil { return nil } - if persistenceServiceFacade == nil { - return nil - } + isolation := GetIsolationFromContext(ctx) - ps := persistenceServiceFacade.Get() + ps := isolation.persistenceServiceFacade.Get() - log.Infof("saving strategies states...") + log.Debugf("saving strategy persistence states...") return trader.IterateStrategies(func(strategy StrategyID) error { id := dynamic.CallID(strategy) if len(id) == 0 { diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index fafeda8aa6..7aef1a1e4a 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -206,7 +206,7 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con bbgo.Shutdown(shtCtx) cancelShutdown() - if err := trader.SaveState(); err != nil { + if err := trader.SaveState(nil); err != nil { log.WithError(err).Errorf("can not save strategy states") } From 2fbe90b1e7fff07a73c42c7e7251978f9439a252 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 15 Mar 2023 22:50:50 +0800 Subject: [PATCH 0591/1392] bbgo: fix: pass isolated context to SaveState() call --- pkg/bbgo/trader.go | 1 + pkg/cmd/run.go | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 836d86c9bf..53fc2086f3 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -402,6 +402,7 @@ func (trader *Trader) IterateStrategies(f func(st StrategyID) error) error { return nil } +// NOTICE: the ctx here is the trading context, which could already be canceled. func (trader *Trader) SaveState(ctx context.Context) error { if trader.environment.BacktestService != nil { return nil diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 7aef1a1e4a..975ede42a3 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -124,7 +124,7 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con _ = grpcBind _ = enableGrpc - ctx, cancelTrading := context.WithCancel(basectx) + tradingCtx, cancelTrading := context.WithCancel(basectx) defer cancelTrading() environ := bbgo.NewEnvironment() @@ -135,21 +135,21 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con } if lightweight { - if err := bbgo.BootstrapEnvironmentLightweight(ctx, environ, userConfig); err != nil { + if err := bbgo.BootstrapEnvironmentLightweight(tradingCtx, environ, userConfig); err != nil { return err } } else { - if err := bbgo.BootstrapEnvironment(ctx, environ, userConfig); err != nil { + if err := bbgo.BootstrapEnvironment(tradingCtx, environ, userConfig); err != nil { return err } } - if err := environ.Init(ctx); err != nil { + if err := environ.Init(tradingCtx); err != nil { return err } if !noSync { - if err := environ.Sync(ctx, userConfig); err != nil { + if err := environ.Sync(tradingCtx, userConfig); err != nil { return err } @@ -167,7 +167,7 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con return err } - if err := trader.Run(ctx); err != nil { + if err := trader.Run(tradingCtx); err != nil { return err } @@ -179,7 +179,7 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con Trader: trader, } - if err := s.Run(ctx, webServerBind); err != nil { + if err := s.Run(tradingCtx, webServerBind); err != nil { log.WithError(err).Errorf("http server bind error") } }() @@ -198,18 +198,19 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con }() } - cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) + cmdutil.WaitForSignal(tradingCtx, syscall.SIGINT, syscall.SIGTERM) cancelTrading() gracefulShutdownPeriod := 30 * time.Second - shtCtx, cancelShutdown := context.WithTimeout(bbgo.NewTodoContextWithExistingIsolation(ctx), gracefulShutdownPeriod) + shtCtx, cancelShutdown := context.WithTimeout(bbgo.NewTodoContextWithExistingIsolation(tradingCtx), gracefulShutdownPeriod) bbgo.Shutdown(shtCtx) - cancelShutdown() - if err := trader.SaveState(nil); err != nil { - log.WithError(err).Errorf("can not save strategy states") + if err := trader.SaveState(shtCtx); err != nil { + log.WithError(err).Errorf("can not save strategy persistence states") } + cancelShutdown() + for _, session := range environ.Sessions() { if err := session.MarketDataStream.Close(); err != nil { log.WithError(err).Errorf("[%s] market data stream close error", session.Name) From cf9a2e55bfddffb0982ab9a2dc012c7b1d33e1c3 Mon Sep 17 00:00:00 2001 From: narumi Date: Wed, 15 Mar 2023 19:00:07 +0800 Subject: [PATCH 0592/1392] add skew to adjust bid/ask price --- pkg/strategy/fixedmaker/strategy.go | 35 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/fixedmaker/strategy.go b/pkg/strategy/fixedmaker/strategy.go index b409721924..d1aa85c726 100644 --- a/pkg/strategy/fixedmaker/strategy.go +++ b/pkg/strategy/fixedmaker/strategy.go @@ -33,6 +33,10 @@ type Strategy struct { OrderType types.OrderType `json:"orderType"` DryRun bool `json:"dryRun"` + // SkewFactor is used to calculate the skew of bid/ask price + SkewFactor fixedpoint.Value `json:"skewFactor"` + TargetWeight fixedpoint.Value `json:"targetWeight"` + // persistence fields Position *types.Position `json:"position,omitempty" persistence:"position"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` @@ -162,6 +166,8 @@ func (s *Strategy) replenish(ctx context.Context) { } func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrder, error) { + orders := []types.SubmitOrder{} + baseBalance, ok := s.session.GetAccount().Balance(s.Market.BaseCurrency) if !ok { return nil, fmt.Errorf("base currency %s balance not found", s.Market.BaseCurrency) @@ -181,24 +187,29 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrde midPrice := ticker.Buy.Add(ticker.Sell).Div(fixedpoint.NewFromFloat(2.0)) log.Infof("mid price: %+v", midPrice) - orders := []types.SubmitOrder{} - - // calculate buy and sell price - // buy price = mid price * (1 - r) - buyPrice := midPrice.Mul(fixedpoint.One.Sub(s.HalfSpreadRatio)) - log.Infof("buy price: %+v", buyPrice) - // sell price = mid price * (1 + r) - sellPrice := midPrice.Mul(fixedpoint.One.Add(s.HalfSpreadRatio)) - log.Infof("sell price: %+v", sellPrice) + // calcualte skew by the difference between base weight and target weight + baseValue := baseBalance.Total().Mul(midPrice) + baseWeight := baseValue.Div(baseValue.Add(quoteBalance.Total())) + skew := s.SkewFactor.Mul(baseWeight.Sub(s.TargetWeight)) + + // calculate bid and ask price + // bid price = mid price * (1 - max(r + skew, 0)) + bidSpreadRatio := fixedpoint.Max(s.HalfSpreadRatio.Add(skew), fixedpoint.Zero) + bidPrice := midPrice.Mul(fixedpoint.One.Sub(bidSpreadRatio)) + log.Infof("bid price: %s", bidPrice.String()) + // ask price = mid price * (1 + max(r - skew, 0)) + askSrasedRatio := fixedpoint.Max(s.HalfSpreadRatio.Sub(skew), fixedpoint.Zero) + askPrice := midPrice.Mul(fixedpoint.One.Add(askSrasedRatio)) + log.Infof("ask price: %s", askPrice.String()) // check balance and generate orders - amount := s.Quantity.Mul(buyPrice) + amount := s.Quantity.Mul(bidPrice) if quoteBalance.Available.Compare(amount) > 0 { orders = append(orders, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, Type: s.OrderType, - Price: buyPrice, + Price: bidPrice, Quantity: s.Quantity, }) } else { @@ -210,7 +221,7 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrde Symbol: s.Symbol, Side: types.SideTypeSell, Type: s.OrderType, - Price: sellPrice, + Price: askPrice, Quantity: s.Quantity, }) } else { From a5675f72ad480b052dc2ff0fd05a7f23e6156de6 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 16 Mar 2023 16:43:56 +0800 Subject: [PATCH 0593/1392] MINOR: use Debug config for debug log --- pkg/strategy/grid2/pin_order_map.go | 14 ++++++++++++++ pkg/strategy/grid2/strategy.go | 27 +++++++++++++++++---------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/grid2/pin_order_map.go b/pkg/strategy/grid2/pin_order_map.go index 31fe48f5b0..7701fdd768 100644 --- a/pkg/strategy/grid2/pin_order_map.go +++ b/pkg/strategy/grid2/pin_order_map.go @@ -1,6 +1,9 @@ package grid2 import ( + "fmt" + "strings" + "github.com/c9s/bbgo/pkg/types" ) @@ -32,3 +35,14 @@ func (m PinOrderMap) SyncOrderMap() *types.SyncOrderMap { return orderMap } + +func (m PinOrderMap) String() string { + var sb strings.Builder + + sb.WriteString("================== PIN ORDER MAP ==================\n") + for pin, order := range m { + sb.WriteString(fmt.Sprintf("%s -> %s\n", pin, order.String())) + } + sb.WriteString("================== END OF PIN ORDER MAP ==================\n") + return sb.String() +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 09ff9b7b55..e4e9a00262 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1217,6 +1217,13 @@ func (s *Strategy) debugOrders(desc string, orders []types.Order) { s.logger.Infof(sb.String()) } +func (s *Strategy) debugLog(format string, args ...interface{}) { + if !s.Debug { + return + } + + s.logger.Infof(format, args...) +} func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoint.Value) ([]types.SubmitOrder, error) { var pins = s.grid.Pins var usedBase = fixedpoint.Zero @@ -1400,7 +1407,7 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context expectedOrderNums := s.GridNum - 1 openOrdersOnGridNums := int64(len(openOrdersOnGrid)) - s.logger.Debugf("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) + s.debugLog("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) if expectedOrderNums == openOrdersOnGridNums { // no need to recover return nil @@ -1458,8 +1465,8 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context } func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { - s.logger.Debugf("pins: %+v", pins) - s.logger.Debugf("open pin orders: %+v", pinOrders) + s.debugLog("pins: %+v", pins) + s.debugLog("open pin orders:\n%s", pinOrders.String()) s.debugOrders("filled orders", filledOrders) for _, filledOrder := range filledOrders { @@ -1473,7 +1480,7 @@ func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrd } } - s.logger.Debugf("filled pin orders: %+v", pinOrders) + s.debugLog("filled pin orders:\n%+v", pinOrders.String()) side := types.SideTypeBuy for _, pin := range pins { @@ -1557,10 +1564,10 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return nil, errors.Wrapf(err, "failed to query trades to recover the grid with open orders") } - s.logger.Debugf("QueryTrades return %d trades", len(trades)) + s.debugLog("QueryTrades return %d trades", len(trades)) for _, trade := range trades { - s.logger.Debugf(trade.String()) + s.debugLog(trade.String()) if existedOrders.Exists(trade.OrderID) { // already queries, skip continue @@ -1574,7 +1581,7 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return nil, errors.Wrapf(err, "failed to query order by trade") } - s.logger.Debugf("%s (group_id: %d)", order.String(), order.GroupID) + s.debugLog("%s (group_id: %d)", order.String(), order.GroupID) // avoid query this order again existedOrders.Add(*order) @@ -2148,11 +2155,11 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { if s.RecoverGridByScanningTrades { - s.logger.Debugf("recover grid by scanning trades") + s.debugLog("recover grid by scanning trades") return s.recoverGridByScanningTrades(ctx, session) } - s.logger.Debugf("recover grid by scanning orders") + s.debugLog("recover grid by scanning orders") return s.recoverGridByScanningOrders(ctx, session) } @@ -2197,7 +2204,7 @@ func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbg s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) - s.logger.Debugf("recover grid with group id: %d", s.OrderGroupID) + s.debugLog("recover grid with group id: %d", s.OrderGroupID) // filter out the order with the group id belongs to this grid var openOrdersOnGrid []types.Order for _, order := range openOrders { From 939427c81f5244fad33a3a15e6451145e7b6c97b Mon Sep 17 00:00:00 2001 From: narumi Date: Thu, 16 Mar 2023 03:15:07 +0800 Subject: [PATCH 0594/1392] use ATR to adjust spread ratio --- pkg/strategy/fixedmaker/strategy.go | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pkg/strategy/fixedmaker/strategy.go b/pkg/strategy/fixedmaker/strategy.go index d1aa85c726..23d86b5e3a 100644 --- a/pkg/strategy/fixedmaker/strategy.go +++ b/pkg/strategy/fixedmaker/strategy.go @@ -9,6 +9,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" ) @@ -37,6 +38,10 @@ type Strategy struct { SkewFactor fixedpoint.Value `json:"skewFactor"` TargetWeight fixedpoint.Value `json:"targetWeight"` + // replace halfSpreadRatio by ATR + ATRMultiplier fixedpoint.Value `json:"atrMultiplier"` + ATRWindow int `json:"atrWindow"` + // persistence fields Position *types.Position `json:"position,omitempty" persistence:"position"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` @@ -44,12 +49,17 @@ type Strategy struct { session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor activeOrderBook *bbgo.ActiveOrderBook + atr *indicator.ATR } func (s *Strategy) Defaults() error { if s.OrderType == "" { s.OrderType = types.OrderTypeLimitMaker } + + if s.ATRWindow == 0 { + s.ATRWindow = 14 + } return nil } func (s *Strategy) Initialize() error { @@ -72,6 +82,18 @@ func (s *Strategy) Validate() error { if s.HalfSpreadRatio.Float64() <= 0 { return fmt.Errorf("halfSpreadRatio should be positive") } + + if s.SkewFactor.Float64() < 0 { + return fmt.Errorf("skewFactor should be non-negative") + } + + if s.ATRMultiplier.Float64() < 0 { + return fmt.Errorf("atrMultiplier should be non-negative") + } + + if s.ATRWindow < 0 { + return fmt.Errorf("atrWindow should be non-negative") + } return nil } @@ -109,6 +131,8 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. bbgo.Sync(ctx, s) }) + s.atr = s.StandardIndicatorSet.ATR(types.IntervalWindow{Interval: s.Interval, Window: s.ATRWindow}) + session.UserDataStream.OnStart(func() { // you can place orders here when bbgo is started, this will be called only once. }) @@ -192,6 +216,13 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrde baseWeight := baseValue.Div(baseValue.Add(quoteBalance.Total())) skew := s.SkewFactor.Mul(baseWeight.Sub(s.TargetWeight)) + if s.ATRMultiplier.Float64() > 0 { + atr := fixedpoint.NewFromFloat(s.atr.Last()) + log.Infof("atr: %s", atr.String()) + s.HalfSpreadRatio = s.ATRMultiplier.Mul(atr).Div(midPrice) + log.Infof("half spread ratio: %s", s.HalfSpreadRatio.String()) + } + // calculate bid and ask price // bid price = mid price * (1 - max(r + skew, 0)) bidSpreadRatio := fixedpoint.Max(s.HalfSpreadRatio.Add(skew), fixedpoint.Zero) From feabadeb59aa586a1a2cc30deb0b41c78306c5f8 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 16 Mar 2023 17:32:56 +0800 Subject: [PATCH 0595/1392] FEATURE: make PinOrderMap's key from string to Pin --- pkg/strategy/grid2/pin_order_map.go | 4 +- pkg/strategy/grid2/strategy.go | 28 +++++------ pkg/strategy/grid2/strategy_test.go | 76 ++++++++++++++--------------- 3 files changed, 53 insertions(+), 55 deletions(-) diff --git a/pkg/strategy/grid2/pin_order_map.go b/pkg/strategy/grid2/pin_order_map.go index 7701fdd768..f098637351 100644 --- a/pkg/strategy/grid2/pin_order_map.go +++ b/pkg/strategy/grid2/pin_order_map.go @@ -8,7 +8,7 @@ import ( ) // PinOrderMap store the pin-order's relation, we will change key from string to fixedpoint.Value when FormatString fixed -type PinOrderMap map[string]types.Order +type PinOrderMap map[Pin]types.Order // AscendingOrders get the orders from pin order map and sort it in asc order func (m PinOrderMap) AscendingOrders() []types.Order { @@ -41,7 +41,7 @@ func (m PinOrderMap) String() string { sb.WriteString("================== PIN ORDER MAP ==================\n") for pin, order := range m { - sb.WriteString(fmt.Sprintf("%s -> %s\n", pin, order.String())) + sb.WriteString(fmt.Sprintf("%+v -> %s\n", pin, order.String())) } sb.WriteString("================== END OF PIN ORDER MAP ==================\n") return sb.String() diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e4e9a00262..0b7df09711 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1470,11 +1470,11 @@ func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrd s.debugOrders("filled orders", filledOrders) for _, filledOrder := range filledOrders { - price := s.Market.FormatPrice(filledOrder.Price) + price := Pin(filledOrder.Price) if o, exist := pinOrders[price]; !exist { - return fmt.Errorf("the price (%s) is not in pins", price) + return fmt.Errorf("the price (%+v) is not in pins", price) } else if o.OrderID != 0 { - return fmt.Errorf("there is already an order at this price (%s)", price) + return fmt.Errorf("there is already an order at this price (%+v)", price) } else { pinOrders[price] = filledOrder } @@ -1484,10 +1484,9 @@ func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrd side := types.SideTypeBuy for _, pin := range pins { - price := s.Market.FormatPrice(fixedpoint.Value(pin)) - order, exist := pinOrders[price] + order, exist := pinOrders[pin] if !exist { - return fmt.Errorf("there is no order at price (%s)", price) + return fmt.Errorf("there is no order at price (%+v)", pin) } // if there is order with OrderID = 0, means we hit the empty pin @@ -1521,13 +1520,12 @@ func (s *Strategy) buildPinOrderMap(pins []Pin, openOrders []types.Order) (PinOr pinOrderMap := make(PinOrderMap) for _, pin := range pins { - priceStr := s.Market.FormatPrice(fixedpoint.Value(pin)) - pinOrderMap[priceStr] = types.Order{} + pinOrderMap[pin] = types.Order{} } for _, openOrder := range openOrders { - priceStr := s.Market.FormatPrice(openOrder.Price) - v, exist := pinOrderMap[priceStr] + pin := Pin(openOrder.Price) + v, exist := pinOrderMap[pin] if !exist { return nil, fmt.Errorf("the price of the order (id: %d) is not in pins", openOrder.OrderID) } @@ -1536,7 +1534,7 @@ func (s *Strategy) buildPinOrderMap(pins []Pin, openOrders []types.Order) (PinOr return nil, fmt.Errorf("there are duplicated open orders at the same pin") } - pinOrderMap[priceStr] = openOrder + pinOrderMap[pin] = openOrder } return pinOrderMap, nil @@ -1595,8 +1593,8 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history } // checked the trade's order is filled order - priceStr := s.Market.FormatPrice(order.Price) - v, exist := pinOrdersOpen[priceStr] + pin := Pin(order.Price) + v, exist := pinOrdersOpen[pin] if !exist { return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") } @@ -1607,13 +1605,13 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history } // check the order's creation time - if pinOrder, exist := pinOrdersFilled[priceStr]; exist && pinOrder.CreationTime.Time().After(order.CreationTime.Time()) { + if pinOrder, exist := pinOrdersFilled[pin]; exist && pinOrder.CreationTime.Time().After(order.CreationTime.Time()) { // do not replace the pin order if the order's creation time is not after pin order's creation time // this situation should not happen actually, because the trades is already sorted. s.logger.Infof("pinOrder's creation time (%s) should not be after order's creation time (%s)", pinOrder.CreationTime, order.CreationTime) continue } - pinOrdersFilled[priceStr] = *order + pinOrdersFilled[pin] = *order // wait 100 ms to avoid rate limit time.Sleep(100 * time.Millisecond) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 63631fcb64..d7f75e605e 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -1100,7 +1100,7 @@ func Test_buildPinOrderMap(t *testing.T) { assert.Len(m, 11) for pin, order := range m { - if pin == s.FormatPrice(openOrders[0].Price) { + if pin == Pin(openOrders[0].Price) { assert.Equal(openOrders[0].OrderID, order.OrderID) } else { assert.Equal(uint64(0), order.OrderID) @@ -1184,19 +1184,19 @@ func Test_getOrdersFromPinOrderMapInAscOrder(t *testing.T) { assert := assert.New(t) now := time.Now() pinOrderMap := PinOrderMap{ - "1000": types.Order{ + Pin(number("1000")): types.Order{ OrderID: 1, CreationTime: types.Time(now.Add(1 * time.Hour)), UpdateTime: types.Time(now.Add(5 * time.Hour)), }, - "1100": types.Order{}, - "1200": types.Order{}, - "1300": types.Order{ + Pin(number("1100")): types.Order{}, + Pin(number("1200")): types.Order{}, + Pin(number("1300")): types.Order{ OrderID: 3, CreationTime: types.Time(now.Add(3 * time.Hour)), UpdateTime: types.Time(now.Add(6 * time.Hour)), }, - "1400": types.Order{ + Pin(number("1400")): types.Order{ OrderID: 2, CreationTime: types.Time(now.Add(2 * time.Hour)), UpdateTime: types.Time(now.Add(4 * time.Hour)), @@ -1220,20 +1220,20 @@ func Test_verifyFilledGrid(t *testing.T) { t.Run("valid grid with buy/sell orders", func(t *testing.T) { pinOrderMap := PinOrderMap{ - "100.00": types.Order{ + Pin(number("100.00")): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - "200.00": types.Order{}, - "300.00": types.Order{ + Pin(number("200.00")): types.Order{}, + Pin(number("300.00")): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - "400.00": types.Order{ + Pin(number("400.00")): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, @@ -1245,45 +1245,45 @@ func Test_verifyFilledGrid(t *testing.T) { }) t.Run("valid grid with only buy orders", func(t *testing.T) { pinOrderMap := PinOrderMap{ - "100.00": types.Order{ + Pin(number("100.00")): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - "200.00": types.Order{ + Pin(number("200.00")): types.Order{ OrderID: 2, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - "300.00": types.Order{ + Pin(number("300.00")): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - "400.00": types.Order{}, + Pin(number("400.00")): types.Order{}, } assert.NoError(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) }) t.Run("valid grid with only sell orders", func(t *testing.T) { pinOrderMap := PinOrderMap{ - "100.00": types.Order{}, - "200.00": types.Order{ + Pin(number("100.00")): types.Order{}, + Pin(number("200.00")): types.Order{ OrderID: 2, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - "300.00": types.Order{ + Pin(number("300.00")): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - "400.00": types.Order{ + Pin(number("400.00")): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, @@ -1295,15 +1295,15 @@ func Test_verifyFilledGrid(t *testing.T) { }) t.Run("invalid grid with multiple empty pins", func(t *testing.T) { pinOrderMap := PinOrderMap{ - "100.00": types.Order{ + Pin(number("100.00")): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - "200.00": types.Order{}, - "300.00": types.Order{}, - "400.00": types.Order{ + Pin(number("200.00")): types.Order{}, + Pin(number("300.00")): types.Order{}, + Pin(number("400.00")): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, @@ -1315,25 +1315,25 @@ func Test_verifyFilledGrid(t *testing.T) { }) t.Run("invalid grid without empty pin", func(t *testing.T) { pinOrderMap := PinOrderMap{ - "100.00": types.Order{ + Pin(number("100.00")): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - "200.00": types.Order{ + Pin(number("200.00")): types.Order{ OrderID: 2, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - "300.00": types.Order{ + Pin(number("300.00")): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - "400.00": types.Order{ + Pin(number("400.00")): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, @@ -1345,20 +1345,20 @@ func Test_verifyFilledGrid(t *testing.T) { }) t.Run("invalid grid with Buy-empty-Sell-Buy order", func(t *testing.T) { pinOrderMap := PinOrderMap{ - "100.00": types.Order{ + Pin(number("100.00")): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - "200.00": types.Order{}, - "300.00": types.Order{ + Pin(number("200.00")): types.Order{}, + Pin(number("300.00")): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - "400.00": types.Order{ + Pin(number("400.00")): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, @@ -1370,45 +1370,45 @@ func Test_verifyFilledGrid(t *testing.T) { }) t.Run("invalid grid with Sell-empty order", func(t *testing.T) { pinOrderMap := PinOrderMap{ - "100.00": types.Order{ + Pin(number("100.00")): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - "200.00": types.Order{ + Pin(number("200.00")): types.Order{ OrderID: 2, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - "300.00": types.Order{ + Pin(number("300.00")): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - "400.00": types.Order{}, + Pin(number("400.00")): types.Order{}, } assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) }) t.Run("invalid grid with empty-Buy order", func(t *testing.T) { pinOrderMap := PinOrderMap{ - "100.00": types.Order{}, - "200.00": types.Order{ + Pin(number("100.00")): types.Order{}, + Pin(number("200.00")): types.Order{ OrderID: 2, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - "300.00": types.Order{ + Pin(number("300.00")): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - "400.00": types.Order{ + Pin(number("400.00")): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, From 0b922a929eb972d8f4df3f88a39454527a06b5c8 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 16 Mar 2023 18:01:56 +0800 Subject: [PATCH 0596/1392] grid2: pull out backoff cancel all to cancelAllOrdersUntilSuccessful --- pkg/strategy/grid2/strategy.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e4e9a00262..89d267b60e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -922,12 +922,7 @@ func (s *Strategy) cancelAll(ctx context.Context) error { s.logger.Infof("found %d open orders left, using cancel all orders api", len(openOrders)) s.logger.Infof("using cancal all orders api for canceling grid orders...") - op := func() error { - _, cancelErr := service.CancelAllOrders(ctx) - return cancelErr - } - - if err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 101)); err != nil { + if err := cancelAllOrdersUntilSuccessful(ctx, service); err != nil { s.logger.WithError(err).Errorf("CancelAllOrders api call error") werr = multierr.Append(werr, err) } @@ -2321,6 +2316,15 @@ func generalBackoff(ctx context.Context, op backoff.Operation) (err error) { return err } +func cancelAllOrdersUntilSuccessful(ctx context.Context, service advancedOrderCancelApi) error { + var op = func() (err2 error) { + _, err2 = service.CancelAllOrders(ctx) + return err2 + } + + return generalBackoff(ctx, op) +} + func cancelOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, orders ...types.Order) error { var op = func() (err2 error) { err2 = ex.CancelOrders(ctx, orders...) From a8438f8f720d5c1c931f9b93b8de327dd76dc5dc Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 25 Aug 2022 10:17:42 +0800 Subject: [PATCH 0597/1392] exits/hhllstop: add basic parameters --- pkg/bbgo/hh_ll_stop.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 pkg/bbgo/hh_ll_stop.go diff --git a/pkg/bbgo/hh_ll_stop.go b/pkg/bbgo/hh_ll_stop.go new file mode 100644 index 0000000000..dade7e0002 --- /dev/null +++ b/pkg/bbgo/hh_ll_stop.go @@ -0,0 +1,32 @@ +package bbgo + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type HigherHighLowerLowStopLoss struct { + Symbol string `json:"symbol"` + + Side types.SideType `json:"side"` + + types.IntervalWindow + + HighLowWindow int `json:"highLowWindow"` + + MaxHighLow int `json:"maxHighLow"` + + MinHighLow int `json:"minHighLow"` + + // ActivationRatio is the trigger condition + // When the price goes higher (lower for side buy) than this ratio, the stop will be activated. + ActivationRatio fixedpoint.Value `json:"activationRatio"` + + // DeactivationRatio is the kill condition + // When the price goes higher (lower for short position) than this ratio, the stop will be deactivated. + // You can use this to combine several exits + DeactivationRatio fixedpoint.Value `json:"deactivationRatio"` + + session *ExchangeSession + orderExecutor *GeneralOrderExecutor +} From eb5479ffdff7eb9bc6ae1ad998642c0140345e8f Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 16 Mar 2023 18:30:48 +0800 Subject: [PATCH 0598/1392] exits/hhllstop: hhllstop prototype --- pkg/bbgo/exit_hh_ll_stop.go | 184 ++++++++++++++++++++++++++++++++++++ pkg/bbgo/hh_ll_stop.go | 32 ------- 2 files changed, 184 insertions(+), 32 deletions(-) create mode 100644 pkg/bbgo/exit_hh_ll_stop.go delete mode 100644 pkg/bbgo/hh_ll_stop.go diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go new file mode 100644 index 0000000000..7c3c0270d0 --- /dev/null +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -0,0 +1,184 @@ +package bbgo + +import ( + "context" + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type HigherHighLowerLowStopLoss struct { + Symbol string `json:"symbol"` + + Side types.SideType `json:"side"` + + types.IntervalWindow + + HighLowWindow int `json:"highLowWindow"` + + MaxHighLow int `json:"maxHighLow"` + + MinHighLow int `json:"minHighLow"` + + // ActivationRatio is the trigger condition + // When the price goes higher (lower for short position) than this ratio, the stop will be activated. + // You can use this to combine several exits + ActivationRatio fixedpoint.Value `json:"activationRatio"` + + // DeactivationRatio is the kill condition + // When the price goes higher (lower for short position) than this ratio, the stop will be deactivated. + // You can use this to combine several exits + DeactivationRatio fixedpoint.Value `json:"deactivationRatio"` + + OppositeDirectionAsPosition bool `json:"oppositeDirectionAsPosition"` + + klines types.KLineWindow + + // activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop + activated bool + + highLows []types.Direction + + session *ExchangeSession + orderExecutor *GeneralOrderExecutor +} + +// Subscribe required k-line stream +func (s *HigherHighLowerLowStopLoss) Subscribe(session *ExchangeSession) { + // use 1m kline to handle roi stop + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +// updateActivated checks the position cost against the close price, activation ratio, and deactivation ratio to +// determine whether this stop should be activated +func (s *HigherHighLowerLowStopLoss) updateActivated(position *types.Position, closePrice fixedpoint.Value) { + if position.IsClosed() || position.IsDust(closePrice) { + s.activated = false + } else if s.activated { + if position.IsLong() { + r := fixedpoint.One.Add(s.DeactivationRatio) + if closePrice.Compare(position.AverageCost.Mul(r)) >= 0 { + s.activated = false + Notify("[hhllStop] Stop of %s deactivated for long position, deactivation ratio %f:", s.Symbol, s.DeactivationRatio) + } + } else if position.IsShort() { + r := fixedpoint.One.Sub(s.DeactivationRatio) + // for short position, if the close price is less than the activation price then this is a profit position. + if closePrice.Compare(position.AverageCost.Mul(r)) <= 0 { + s.activated = false + Notify("[hhllStop] Stop of %s deactivated for short position, deactivation ratio %f:", s.Symbol, s.DeactivationRatio) + } + } + } else { + if position.IsLong() { + r := fixedpoint.One.Add(s.ActivationRatio) + if closePrice.Compare(position.AverageCost.Mul(r)) >= 0 { + s.activated = true + Notify("[hhllStop] Stop of %s activated for long position, activation ratio %f:", s.Symbol, s.ActivationRatio) + } + } else if position.IsShort() { + r := fixedpoint.One.Sub(s.ActivationRatio) + // for short position, if the close price is less than the activation price then this is a profit position. + if closePrice.Compare(position.AverageCost.Mul(r)) <= 0 { + s.activated = true + Notify("[hhllStop] Stop of %s activated for short position, activation ratio %f:", s.Symbol, s.ActivationRatio) + } + } + } +} + +func (s *HigherHighLowerLowStopLoss) updateHighLowNumber(kline types.KLine) { + if !s.activated { + s.reset() + return + } + + if s.klines.GetHigh().Compare(kline.GetHigh()) < 0 { + s.highLows = append(s.highLows, types.DirectionUp) + } else if s.klines.GetLow().Compare(kline.GetLow()) > 0 { + s.highLows = append(s.highLows, types.DirectionDown) + } else { + s.highLows = append(s.highLows, types.DirectionNone) + } + // Truncate highLows + if len(s.highLows) > s.HighLowWindow { + end := len(s.highLows) + start := end - s.HighLowWindow + if start < 0 { + start = 0 + } + kn := s.highLows[start:] + s.highLows = kn + } + + s.klines.Add(kline) + s.klines.Truncate(s.Window - 1) +} + +func (s *HigherHighLowerLowStopLoss) shouldStop(position *types.Position) bool { + if s.activated { + highs := 0 + lows := 0 + for _, hl := range s.highLows { + switch hl { + case types.DirectionUp: + highs++ + case types.DirectionDown: + lows++ + } + } + + log.Debugf("[hhllStop] %d higher highs and %d lower lows in window of %d", highs, lows, s.HighLowWindow) + + // Check higher highs + if (position.IsLong() && !s.OppositeDirectionAsPosition) || (position.IsShort() && s.OppositeDirectionAsPosition) { + if (s.MinHighLow > 0 && highs < s.MinHighLow) || (s.MaxHighLow > 0 && highs > s.MaxHighLow) { + return true + } + // Check lower lows + } else if (position.IsShort() && !s.OppositeDirectionAsPosition) || (position.IsLong() && s.OppositeDirectionAsPosition) { + if (s.MinHighLow > 0 && lows < s.MinHighLow) || (s.MaxHighLow > 0 && lows > s.MaxHighLow) { + return true + } + } + } + return false +} + +func (s *HigherHighLowerLowStopLoss) reset() { + s.highLows = []types.Direction{} + s.klines.Truncate(0) +} + +func (s *HigherHighLowerLowStopLoss) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + s.updateActivated(position, kline.GetClose()) + + s.updateHighLowNumber(kline) + + // Close position & reset + if s.activated && s.shouldStop(position) { + err := s.orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "hhllStop") + if err != nil { + Notify("[hhllStop] Stop of %s triggered but failed to close %s position:", s.Symbol, err) + } else { + s.reset() + s.activated = false + Notify("[hhllStop] Stop of %s triggered and position closed", s.Symbol) + } + } + })) + + // Make sure the stop is reset when position is closed or dust + orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + if position.IsClosed() || position.IsDust(position.AverageCost) { + s.reset() + s.activated = false + } + }) +} diff --git a/pkg/bbgo/hh_ll_stop.go b/pkg/bbgo/hh_ll_stop.go deleted file mode 100644 index dade7e0002..0000000000 --- a/pkg/bbgo/hh_ll_stop.go +++ /dev/null @@ -1,32 +0,0 @@ -package bbgo - -import ( - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -type HigherHighLowerLowStopLoss struct { - Symbol string `json:"symbol"` - - Side types.SideType `json:"side"` - - types.IntervalWindow - - HighLowWindow int `json:"highLowWindow"` - - MaxHighLow int `json:"maxHighLow"` - - MinHighLow int `json:"minHighLow"` - - // ActivationRatio is the trigger condition - // When the price goes higher (lower for side buy) than this ratio, the stop will be activated. - ActivationRatio fixedpoint.Value `json:"activationRatio"` - - // DeactivationRatio is the kill condition - // When the price goes higher (lower for short position) than this ratio, the stop will be deactivated. - // You can use this to combine several exits - DeactivationRatio fixedpoint.Value `json:"deactivationRatio"` - - session *ExchangeSession - orderExecutor *GeneralOrderExecutor -} From 2e00e584421b9235713295fd4e063960afc70cbb Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 16 Mar 2023 18:39:27 +0800 Subject: [PATCH 0599/1392] exits/hhllstop: add hhllstop to exits --- pkg/bbgo/exit.go | 20 +++++++++++++++----- pkg/bbgo/exit_hh_ll_stop.go | 14 +++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index e4a59e6e08..aedc936d6b 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -29,16 +29,17 @@ func (s *ExitMethodSet) Bind(session *ExchangeSession, orderExecutor *GeneralOrd } type ExitMethod struct { - RoiStopLoss *RoiStopLoss `json:"roiStopLoss"` - ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"` - RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"` - TrailingStop *TrailingStop2 `json:"trailingStop"` + RoiStopLoss *RoiStopLoss `json:"roiStopLoss"` + ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"` + RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"` + TrailingStop *TrailingStop2 `json:"trailingStop"` + HigherHighLowerLowStop *HigherHighLowerLowStop `json:"higherHighLowerLowStopLoss"` // Exit methods for short positions // ================================================= LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"` CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"` - SupportTakeProfit *SupportTakeProfit `json:"supportTakeProfit"` + SupportTakeProfit *SupportTakeProfit `json:"supportTakeProfit"` } func (e ExitMethod) String() string { @@ -78,6 +79,11 @@ func (e ExitMethod) String() string { buf.WriteString("supportTakeProfit: " + string(b) + ", ") } + if e.HigherHighLowerLowStop != nil { + b, _ := json.Marshal(e.HigherHighLowerLowStop) + buf.WriteString("hhllStop: " + string(b) + ", ") + } + return buf.String() } @@ -135,4 +141,8 @@ func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderE if m.TrailingStop != nil { m.TrailingStop.Bind(session, orderExecutor) } + + if m.HigherHighLowerLowStop != nil { + m.HigherHighLowerLowStop.Bind(session, orderExecutor) + } } diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index 7c3c0270d0..0b3d57fddc 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -8,7 +8,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -type HigherHighLowerLowStopLoss struct { +type HigherHighLowerLowStop struct { Symbol string `json:"symbol"` Side types.SideType `json:"side"` @@ -45,14 +45,14 @@ type HigherHighLowerLowStopLoss struct { } // Subscribe required k-line stream -func (s *HigherHighLowerLowStopLoss) Subscribe(session *ExchangeSession) { +func (s *HigherHighLowerLowStop) Subscribe(session *ExchangeSession) { // use 1m kline to handle roi stop session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } // updateActivated checks the position cost against the close price, activation ratio, and deactivation ratio to // determine whether this stop should be activated -func (s *HigherHighLowerLowStopLoss) updateActivated(position *types.Position, closePrice fixedpoint.Value) { +func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, closePrice fixedpoint.Value) { if position.IsClosed() || position.IsDust(closePrice) { s.activated = false } else if s.activated { @@ -88,7 +88,7 @@ func (s *HigherHighLowerLowStopLoss) updateActivated(position *types.Position, c } } -func (s *HigherHighLowerLowStopLoss) updateHighLowNumber(kline types.KLine) { +func (s *HigherHighLowerLowStop) updateHighLowNumber(kline types.KLine) { if !s.activated { s.reset() return @@ -116,7 +116,7 @@ func (s *HigherHighLowerLowStopLoss) updateHighLowNumber(kline types.KLine) { s.klines.Truncate(s.Window - 1) } -func (s *HigherHighLowerLowStopLoss) shouldStop(position *types.Position) bool { +func (s *HigherHighLowerLowStop) shouldStop(position *types.Position) bool { if s.activated { highs := 0 lows := 0 @@ -146,12 +146,12 @@ func (s *HigherHighLowerLowStopLoss) shouldStop(position *types.Position) bool { return false } -func (s *HigherHighLowerLowStopLoss) reset() { +func (s *HigherHighLowerLowStop) reset() { s.highLows = []types.Direction{} s.klines.Truncate(0) } -func (s *HigherHighLowerLowStopLoss) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { +func (s *HigherHighLowerLowStop) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { s.session = session s.orderExecutor = orderExecutor From 86bce7403b9a6cea23d2411a097d516511f6abd4 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 16 Mar 2023 19:44:58 +0800 Subject: [PATCH 0600/1392] exits/hhllstop: fix out of index error of klines --- pkg/bbgo/exit_hh_ll_stop.go | 38 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index 0b3d57fddc..eb6f0bbbbe 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -8,11 +8,13 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +// TODO: if parameter not set +// TODO: log and notify +// TODO: check all procedures + type HigherHighLowerLowStop struct { Symbol string `json:"symbol"` - Side types.SideType `json:"side"` - types.IntervalWindow HighLowWindow int `json:"highLowWindow"` @@ -94,23 +96,27 @@ func (s *HigherHighLowerLowStop) updateHighLowNumber(kline types.KLine) { return } - if s.klines.GetHigh().Compare(kline.GetHigh()) < 0 { - s.highLows = append(s.highLows, types.DirectionUp) - } else if s.klines.GetLow().Compare(kline.GetLow()) > 0 { - s.highLows = append(s.highLows, types.DirectionDown) + if s.klines.Len() > 0 { + if s.klines.GetHigh().Compare(kline.GetHigh()) < 0 { + s.highLows = append(s.highLows, types.DirectionUp) + } else if s.klines.GetLow().Compare(kline.GetLow()) > 0 { + s.highLows = append(s.highLows, types.DirectionDown) + } else { + s.highLows = append(s.highLows, types.DirectionNone) + } + // Truncate highLows + if len(s.highLows) > s.HighLowWindow { + end := len(s.highLows) + start := end - s.HighLowWindow + if start < 0 { + start = 0 + } + kn := s.highLows[start:] + s.highLows = kn + } } else { s.highLows = append(s.highLows, types.DirectionNone) } - // Truncate highLows - if len(s.highLows) > s.HighLowWindow { - end := len(s.highLows) - start := end - s.HighLowWindow - if start < 0 { - start = 0 - } - kn := s.highLows[start:] - s.highLows = kn - } s.klines.Add(kline) s.klines.Truncate(s.Window - 1) From f8c0e44e24a9b84cfe9a2558caf4f6e211432df0 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 16 Mar 2023 19:45:48 +0800 Subject: [PATCH 0601/1392] exits/hhllstop: update supertrend config as hhllStop example --- config/supertrend.yaml | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index bee067b5d1..8a0fef446f 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -80,19 +80,29 @@ exchangeStrategies: exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) - roiStopLoss: - percentage: 4.6% - - protectiveStopLoss: - activationRatio: 3.5% - stopLossRatio: 2.9% - placeStopOrder: false - - protectiveStopLoss: - activationRatio: 11.1% - stopLossRatio: 9.5% - placeStopOrder: false + percentage: 2% - trailingStop: - callbackRate: 1.1% + callbackRate: 2% #activationRatio: 20% - minProfit: 19.5% + minProfit: 10% interval: 1m side: both closePosition: 100% + - higherHighLowerLowStopLoss: + interval: 1h + window: 20 + highLowWindow: 5 + minHighLow: 2 + maxHighLow: 0 + activationRatio: 0.5% + deactivationRatio: 10% + oppositeDirectionAsPosition: false + - higherHighLowerLowStopLoss: + interval: 1h + window: 20 + highLowWindow: 5 + minHighLow: 0 + maxHighLow: 3 + activationRatio: 0.5% + deactivationRatio: 10% + oppositeDirectionAsPosition: true From 8182840685509b85f87f5e9b86f027b4f6aefdf6 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 16 Mar 2023 21:58:41 +0800 Subject: [PATCH 0602/1392] use fixedpoint.Value as key --- pkg/strategy/grid2/pin_order_map.go | 3 +- pkg/strategy/grid2/strategy.go | 10 ++-- pkg/strategy/grid2/strategy_test.go | 76 ++++++++++++++--------------- 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/pkg/strategy/grid2/pin_order_map.go b/pkg/strategy/grid2/pin_order_map.go index f098637351..fb27e7c1d0 100644 --- a/pkg/strategy/grid2/pin_order_map.go +++ b/pkg/strategy/grid2/pin_order_map.go @@ -4,11 +4,12 @@ import ( "fmt" "strings" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) // PinOrderMap store the pin-order's relation, we will change key from string to fixedpoint.Value when FormatString fixed -type PinOrderMap map[Pin]types.Order +type PinOrderMap map[fixedpoint.Value]types.Order // AscendingOrders get the orders from pin order map and sort it in asc order func (m PinOrderMap) AscendingOrders() []types.Order { diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 0b7df09711..b55532b1ad 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1470,7 +1470,7 @@ func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrd s.debugOrders("filled orders", filledOrders) for _, filledOrder := range filledOrders { - price := Pin(filledOrder.Price) + price := filledOrder.Price if o, exist := pinOrders[price]; !exist { return fmt.Errorf("the price (%+v) is not in pins", price) } else if o.OrderID != 0 { @@ -1484,7 +1484,7 @@ func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrd side := types.SideTypeBuy for _, pin := range pins { - order, exist := pinOrders[pin] + order, exist := pinOrders[fixedpoint.Value(pin)] if !exist { return fmt.Errorf("there is no order at price (%+v)", pin) } @@ -1520,11 +1520,11 @@ func (s *Strategy) buildPinOrderMap(pins []Pin, openOrders []types.Order) (PinOr pinOrderMap := make(PinOrderMap) for _, pin := range pins { - pinOrderMap[pin] = types.Order{} + pinOrderMap[fixedpoint.Value(pin)] = types.Order{} } for _, openOrder := range openOrders { - pin := Pin(openOrder.Price) + pin := openOrder.Price v, exist := pinOrderMap[pin] if !exist { return nil, fmt.Errorf("the price of the order (id: %d) is not in pins", openOrder.OrderID) @@ -1593,7 +1593,7 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history } // checked the trade's order is filled order - pin := Pin(order.Price) + pin := order.Price v, exist := pinOrdersOpen[pin] if !exist { return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index d7f75e605e..432245916d 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -1100,7 +1100,7 @@ func Test_buildPinOrderMap(t *testing.T) { assert.Len(m, 11) for pin, order := range m { - if pin == Pin(openOrders[0].Price) { + if pin == openOrders[0].Price { assert.Equal(openOrders[0].OrderID, order.OrderID) } else { assert.Equal(uint64(0), order.OrderID) @@ -1184,19 +1184,19 @@ func Test_getOrdersFromPinOrderMapInAscOrder(t *testing.T) { assert := assert.New(t) now := time.Now() pinOrderMap := PinOrderMap{ - Pin(number("1000")): types.Order{ + number("1000"): types.Order{ OrderID: 1, CreationTime: types.Time(now.Add(1 * time.Hour)), UpdateTime: types.Time(now.Add(5 * time.Hour)), }, - Pin(number("1100")): types.Order{}, - Pin(number("1200")): types.Order{}, - Pin(number("1300")): types.Order{ + number("1100"): types.Order{}, + number("1200"): types.Order{}, + number("1300"): types.Order{ OrderID: 3, CreationTime: types.Time(now.Add(3 * time.Hour)), UpdateTime: types.Time(now.Add(6 * time.Hour)), }, - Pin(number("1400")): types.Order{ + number("1400"): types.Order{ OrderID: 2, CreationTime: types.Time(now.Add(2 * time.Hour)), UpdateTime: types.Time(now.Add(4 * time.Hour)), @@ -1220,20 +1220,20 @@ func Test_verifyFilledGrid(t *testing.T) { t.Run("valid grid with buy/sell orders", func(t *testing.T) { pinOrderMap := PinOrderMap{ - Pin(number("100.00")): types.Order{ + number("100.00"): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - Pin(number("200.00")): types.Order{}, - Pin(number("300.00")): types.Order{ + number("200.00"): types.Order{}, + number("300.00"): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - Pin(number("400.00")): types.Order{ + number("400.00"): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, @@ -1245,45 +1245,45 @@ func Test_verifyFilledGrid(t *testing.T) { }) t.Run("valid grid with only buy orders", func(t *testing.T) { pinOrderMap := PinOrderMap{ - Pin(number("100.00")): types.Order{ + number("100.00"): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - Pin(number("200.00")): types.Order{ + number("200.00"): types.Order{ OrderID: 2, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - Pin(number("300.00")): types.Order{ + number("300.00"): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - Pin(number("400.00")): types.Order{}, + number("400.00"): types.Order{}, } assert.NoError(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) }) t.Run("valid grid with only sell orders", func(t *testing.T) { pinOrderMap := PinOrderMap{ - Pin(number("100.00")): types.Order{}, - Pin(number("200.00")): types.Order{ + number("100.00"): types.Order{}, + number("200.00"): types.Order{ OrderID: 2, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - Pin(number("300.00")): types.Order{ + number("300.00"): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - Pin(number("400.00")): types.Order{ + number("400.00"): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, @@ -1295,15 +1295,15 @@ func Test_verifyFilledGrid(t *testing.T) { }) t.Run("invalid grid with multiple empty pins", func(t *testing.T) { pinOrderMap := PinOrderMap{ - Pin(number("100.00")): types.Order{ + number("100.00"): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - Pin(number("200.00")): types.Order{}, - Pin(number("300.00")): types.Order{}, - Pin(number("400.00")): types.Order{ + number("200.00"): types.Order{}, + number("300.00"): types.Order{}, + number("400.00"): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, @@ -1315,25 +1315,25 @@ func Test_verifyFilledGrid(t *testing.T) { }) t.Run("invalid grid without empty pin", func(t *testing.T) { pinOrderMap := PinOrderMap{ - Pin(number("100.00")): types.Order{ + number("100.00"): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - Pin(number("200.00")): types.Order{ + number("200.00"): types.Order{ OrderID: 2, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - Pin(number("300.00")): types.Order{ + number("300.00"): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - Pin(number("400.00")): types.Order{ + number("400.00"): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, @@ -1345,20 +1345,20 @@ func Test_verifyFilledGrid(t *testing.T) { }) t.Run("invalid grid with Buy-empty-Sell-Buy order", func(t *testing.T) { pinOrderMap := PinOrderMap{ - Pin(number("100.00")): types.Order{ + number("100.00"): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - Pin(number("200.00")): types.Order{}, - Pin(number("300.00")): types.Order{ + number("200.00"): types.Order{}, + number("300.00"): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - Pin(number("400.00")): types.Order{ + number("400.00"): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, @@ -1370,45 +1370,45 @@ func Test_verifyFilledGrid(t *testing.T) { }) t.Run("invalid grid with Sell-empty order", func(t *testing.T) { pinOrderMap := PinOrderMap{ - Pin(number("100.00")): types.Order{ + number("100.00"): types.Order{ OrderID: 1, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - Pin(number("200.00")): types.Order{ + number("200.00"): types.Order{ OrderID: 2, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - Pin(number("300.00")): types.Order{ + number("300.00"): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeSell, }, }, - Pin(number("400.00")): types.Order{}, + number("400.00"): types.Order{}, } assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil)) }) t.Run("invalid grid with empty-Buy order", func(t *testing.T) { pinOrderMap := PinOrderMap{ - Pin(number("100.00")): types.Order{}, - Pin(number("200.00")): types.Order{ + number("100.00"): types.Order{}, + number("200.00"): types.Order{ OrderID: 2, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - Pin(number("300.00")): types.Order{ + number("300.00"): types.Order{ OrderID: 3, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, }, }, - Pin(number("400.00")): types.Order{ + number("400.00"): types.Order{ OrderID: 4, SubmitOrder: types.SubmitOrder{ Side: types.SideTypeBuy, From bb8dbb155f8311345f3a9ecaf483f0816af16f02 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 17 Mar 2023 10:43:47 +0800 Subject: [PATCH 0603/1392] exits/trailingstop: fix typo --- pkg/bbgo/exit_trailing_stop.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/exit_trailing_stop.go b/pkg/bbgo/exit_trailing_stop.go index ba8d4d2d51..4de0ad66b0 100644 --- a/pkg/bbgo/exit_trailing_stop.go +++ b/pkg/bbgo/exit_trailing_stop.go @@ -100,7 +100,7 @@ func (s *TrailingStop2) checkStopPrice(price fixedpoint.Value, position *types.P // check if we have the minimal profit roi := position.ROI(price) if roi.Compare(s.MinProfit) >= 0 { - Notify("[trailingStop] activated: %s ROI %f > minimal profit ratio %f", s.Symbol, roi.Percentage(), s.MinProfit.Float64()) + Notify("[trailingStop] activated: %s ROI %s > minimal profit ratio %f", s.Symbol, roi.Percentage(), s.MinProfit.Float64()) s.activated = true } } else if !s.ActivationRatio.IsZero() { From 7114016bc952af0f8cbdafa3ecfedf6228a01f66 Mon Sep 17 00:00:00 2001 From: narumi Date: Fri, 17 Mar 2023 03:10:03 +0800 Subject: [PATCH 0604/1392] clamp skew --- pkg/fixedpoint/convert.go | 20 ++++++++++++++++++++ pkg/fixedpoint/dec.go | 20 ++++++++++++++++++++ pkg/strategy/fixedmaker/strategy.go | 17 ++++++++++------- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/pkg/fixedpoint/convert.go b/pkg/fixedpoint/convert.go index 6600185a6f..4f9b8b7ebe 100644 --- a/pkg/fixedpoint/convert.go +++ b/pkg/fixedpoint/convert.go @@ -590,3 +590,23 @@ func Abs(a Value) Value { } return a } + +func Clamp(x, min, max Value) Value { + if x < min { + return min + } + if x > max { + return max + } + return x +} + +func (x Value) Clamp(min, max Value) Value { + if x < min { + return min + } + if x > max { + return max + } + return x +} diff --git a/pkg/fixedpoint/dec.go b/pkg/fixedpoint/dec.go index 43c52d499c..c8e112867f 100644 --- a/pkg/fixedpoint/dec.go +++ b/pkg/fixedpoint/dec.go @@ -1323,3 +1323,23 @@ func (dn Value) Format(mask string) string { } return buf.String() } + +func Clamp(x, min, max Value) Value { + if x.Compare(min) < 0 { + return min + } + if x.Compare(max) > 0 { + return max + } + return x +} + +func (x Value) Clamp(min, max Value) Value { + if x.Compare(min) < 0 { + return min + } + if x.Compare(max) > 0 { + return max + } + return x +} diff --git a/pkg/strategy/fixedmaker/strategy.go b/pkg/strategy/fixedmaker/strategy.go index 23d86b5e3a..1f4bf1d24e 100644 --- a/pkg/strategy/fixedmaker/strategy.go +++ b/pkg/strategy/fixedmaker/strategy.go @@ -211,11 +211,6 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrde midPrice := ticker.Buy.Add(ticker.Sell).Div(fixedpoint.NewFromFloat(2.0)) log.Infof("mid price: %+v", midPrice) - // calcualte skew by the difference between base weight and target weight - baseValue := baseBalance.Total().Mul(midPrice) - baseWeight := baseValue.Div(baseValue.Add(quoteBalance.Total())) - skew := s.SkewFactor.Mul(baseWeight.Sub(s.TargetWeight)) - if s.ATRMultiplier.Float64() > 0 { atr := fixedpoint.NewFromFloat(s.atr.Last()) log.Infof("atr: %s", atr.String()) @@ -223,12 +218,20 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrde log.Infof("half spread ratio: %s", s.HalfSpreadRatio.String()) } + // calcualte skew by the difference between base weight and target weight + baseValue := baseBalance.Total().Mul(midPrice) + baseWeight := baseValue.Div(baseValue.Add(quoteBalance.Total())) + skew := s.SkewFactor.Mul(s.HalfSpreadRatio).Mul(baseWeight.Sub(s.TargetWeight)) + + // let the skew be in the range of [-r, r] + skew = skew.Clamp(s.HalfSpreadRatio.Neg(), s.HalfSpreadRatio) + // calculate bid and ask price - // bid price = mid price * (1 - max(r + skew, 0)) + // bid price = mid price * (1 - r - skew)) bidSpreadRatio := fixedpoint.Max(s.HalfSpreadRatio.Add(skew), fixedpoint.Zero) bidPrice := midPrice.Mul(fixedpoint.One.Sub(bidSpreadRatio)) log.Infof("bid price: %s", bidPrice.String()) - // ask price = mid price * (1 + max(r - skew, 0)) + // ask price = mid price * (1 + r - skew)) askSrasedRatio := fixedpoint.Max(s.HalfSpreadRatio.Sub(skew), fixedpoint.Zero) askPrice := midPrice.Mul(fixedpoint.One.Add(askSrasedRatio)) log.Infof("ask price: %s", askPrice.String()) From 170d41a4927170c44499a9cc54fb929fa1e59420 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 20 Mar 2023 15:47:44 +0800 Subject: [PATCH 0605/1392] exits/trailingstop: updateHighLowNumber no matter activated or not --- pkg/bbgo/exit_hh_ll_stop.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index eb6f0bbbbe..9f72299aa4 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -91,11 +91,6 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close } func (s *HigherHighLowerLowStop) updateHighLowNumber(kline types.KLine) { - if !s.activated { - s.reset() - return - } - if s.klines.Len() > 0 { if s.klines.GetHigh().Compare(kline.GetHigh()) < 0 { s.highLows = append(s.highLows, types.DirectionUp) @@ -152,11 +147,6 @@ func (s *HigherHighLowerLowStop) shouldStop(position *types.Position) bool { return false } -func (s *HigherHighLowerLowStop) reset() { - s.highLows = []types.Direction{} - s.klines.Truncate(0) -} - func (s *HigherHighLowerLowStop) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { s.session = session s.orderExecutor = orderExecutor @@ -173,7 +163,6 @@ func (s *HigherHighLowerLowStop) Bind(session *ExchangeSession, orderExecutor *G if err != nil { Notify("[hhllStop] Stop of %s triggered but failed to close %s position:", s.Symbol, err) } else { - s.reset() s.activated = false Notify("[hhllStop] Stop of %s triggered and position closed", s.Symbol) } @@ -183,7 +172,6 @@ func (s *HigherHighLowerLowStop) Bind(session *ExchangeSession, orderExecutor *G // Make sure the stop is reset when position is closed or dust orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { if position.IsClosed() || position.IsDust(position.AverageCost) { - s.reset() s.activated = false } }) From 1f3579e3ece6f367e1621b430aca597f111315e9 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 20 Mar 2023 15:56:51 +0800 Subject: [PATCH 0606/1392] exits/trailingstop: shouldStop() only works after enough data collected --- pkg/bbgo/exit_hh_ll_stop.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index 9f72299aa4..224bacc530 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -62,14 +62,14 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close r := fixedpoint.One.Add(s.DeactivationRatio) if closePrice.Compare(position.AverageCost.Mul(r)) >= 0 { s.activated = false - Notify("[hhllStop] Stop of %s deactivated for long position, deactivation ratio %f:", s.Symbol, s.DeactivationRatio) + Notify("[hhllStop] Stop of %s deactivated for long position, deactivation ratio %s:", s.Symbol, s.DeactivationRatio.Percentage()) } } else if position.IsShort() { r := fixedpoint.One.Sub(s.DeactivationRatio) // for short position, if the close price is less than the activation price then this is a profit position. if closePrice.Compare(position.AverageCost.Mul(r)) <= 0 { s.activated = false - Notify("[hhllStop] Stop of %s deactivated for short position, deactivation ratio %f:", s.Symbol, s.DeactivationRatio) + Notify("[hhllStop] Stop of %s deactivated for short position, deactivation ratio %s:", s.Symbol, s.DeactivationRatio.Percentage()) } } } else { @@ -77,20 +77,22 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close r := fixedpoint.One.Add(s.ActivationRatio) if closePrice.Compare(position.AverageCost.Mul(r)) >= 0 { s.activated = true - Notify("[hhllStop] Stop of %s activated for long position, activation ratio %f:", s.Symbol, s.ActivationRatio) + Notify("[hhllStop] Stop of %s activated for long position, activation ratio %s:", s.Symbol, s.ActivationRatio.Percentage()) } } else if position.IsShort() { r := fixedpoint.One.Sub(s.ActivationRatio) // for short position, if the close price is less than the activation price then this is a profit position. if closePrice.Compare(position.AverageCost.Mul(r)) <= 0 { s.activated = true - Notify("[hhllStop] Stop of %s activated for short position, activation ratio %f:", s.Symbol, s.ActivationRatio) + Notify("[hhllStop] Stop of %s activated for short position, activation ratio %s:", s.Symbol, s.ActivationRatio.Percentage()) } } } } func (s *HigherHighLowerLowStop) updateHighLowNumber(kline types.KLine) { + s.klines.Truncate(s.Window - 1) + if s.klines.Len() > 0 { if s.klines.GetHigh().Compare(kline.GetHigh()) < 0 { s.highLows = append(s.highLows, types.DirectionUp) @@ -114,10 +116,14 @@ func (s *HigherHighLowerLowStop) updateHighLowNumber(kline types.KLine) { } s.klines.Add(kline) - s.klines.Truncate(s.Window - 1) } func (s *HigherHighLowerLowStop) shouldStop(position *types.Position) bool { + if s.klines.Len() < s.Window || len(s.highLows) < s.HighLowWindow { + log.Debugf("[hhllStop] not enough data for %s yet", s.Symbol) + return false + } + if s.activated { highs := 0 lows := 0 From bc2305553625541ed72338f48b0edcecd65c9563 Mon Sep 17 00:00:00 2001 From: chiahung Date: Mon, 20 Mar 2023 16:27:08 +0800 Subject: [PATCH 0607/1392] FEATURE: emit grid error when failed to recover or open grid --- pkg/strategy/grid2/strategy.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f3afb018c8..e28ab1d3b4 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -2136,13 +2136,16 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi // do recover only when triggerPrice is not set and not in the back-test mode s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") if err := s.recoverGrid(ctx, session); err != nil { - s.logger.WithError(err).Errorf("recover error") + // if recover fail, return and do not open grid + s.EmitGridError(errors.Wrapf(err, "failed to start process, recover error")) + return } } // avoid using goroutine here for back-test if err := s.openGrid(ctx, session); err != nil { - s.logger.WithError(err).Errorf("failed to setup grid orders") + s.EmitGridError(errors.Wrapf(err, "failed to start process, setup grid orders error")) + return } } From 0e2e8306b4e5182261a19e48a1654bac1b011941 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Mon, 20 Mar 2023 18:00:41 +0800 Subject: [PATCH 0608/1392] FEATURE: [grid2] using dnum --- pkg/fixedpoint/dec.go | 5 +- pkg/strategy/grid2/grid_dnum_test.go | 93 +++++++++++++++++++++++++++ pkg/strategy/grid2/grid_int64_test.go | 93 +++++++++++++++++++++++++++ pkg/strategy/grid2/grid_test.go | 89 +------------------------ 4 files changed, 191 insertions(+), 89 deletions(-) create mode 100644 pkg/strategy/grid2/grid_dnum_test.go create mode 100644 pkg/strategy/grid2/grid_int64_test.go diff --git a/pkg/fixedpoint/dec.go b/pkg/fixedpoint/dec.go index c8e112867f..85aad5d68f 100644 --- a/pkg/fixedpoint/dec.go +++ b/pkg/fixedpoint/dec.go @@ -1176,7 +1176,8 @@ func align(x, y *Value) bool { } yshift = e // check(0 <= yshift && yshift <= 20) - y.coef = (y.coef + halfpow10[yshift]) / pow10[yshift] + //y.coef = (y.coef + halfpow10[yshift]) / pow10[yshift] + y.coef = (y.coef) / pow10[yshift] // check(int(y.exp)+yshift == int(x.exp)) return true } @@ -1342,4 +1343,4 @@ func (x Value) Clamp(min, max Value) Value { return max } return x -} +} \ No newline at end of file diff --git a/pkg/strategy/grid2/grid_dnum_test.go b/pkg/strategy/grid2/grid_dnum_test.go new file mode 100644 index 0000000000..bffb79ea31 --- /dev/null +++ b/pkg/strategy/grid2/grid_dnum_test.go @@ -0,0 +1,93 @@ +//go:build dnum + +package grid2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGrid_HasPrice_Dnum(t *testing.T) { + t.Run("case1", func(t *testing.T) { + upper := number(500.0) + lower := number(100.0) + size := number(5.0) + grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculateArithmeticPins() + + assert.True(t, grid.HasPrice(number(500.0)), "upper price") + assert.True(t, grid.HasPrice(number(100.0)), "lower price") + assert.True(t, grid.HasPrice(number(200.0)), "found 200 price ok") + assert.True(t, grid.HasPrice(number(300.0)), "found 300 price ok") + }) + + t.Run("case2", func(t *testing.T) { + upper := number(0.9) + lower := number(0.1) + size := number(7.0) + grid := NewGrid(lower, upper, size, number(0.00000001)) + grid.CalculateArithmeticPins() + + assert.Equal(t, []Pin{ + Pin(number("0.1")), + Pin(number("0.23333333")), + Pin(number("0.36666666")), + Pin(number("0.50000000")), + Pin(number("0.63333333")), + Pin(number("0.76666666")), + Pin(number("0.9")), + }, grid.Pins) + + assert.False(t, grid.HasPrice(number(200.0)), "out of range") + assert.True(t, grid.HasPrice(number(0.9)), "upper price") + assert.True(t, grid.HasPrice(number(0.1)), "lower price") + assert.True(t, grid.HasPrice(number(0.5)), "found 0.49999999 price ok") + }) + + t.Run("case3", func(t *testing.T) { + upper := number(0.9) + lower := number(0.1) + size := number(7.0) + grid := NewGrid(lower, upper, size, number(0.0001)) + grid.CalculateArithmeticPins() + + assert.Equal(t, []Pin{ + Pin(number(0.1)), + Pin(number(0.2333)), + Pin(number(0.3666)), + Pin(number(0.5000)), + Pin(number(0.6333)), + Pin(number(0.7666)), + Pin(number(0.9)), + }, grid.Pins) + + assert.False(t, grid.HasPrice(number(200.0)), "out of range") + assert.True(t, grid.HasPrice(number(0.9)), "upper price") + assert.True(t, grid.HasPrice(number(0.1)), "lower price") + assert.True(t, grid.HasPrice(number(0.5)), "found 0.5 price ok") + assert.True(t, grid.HasPrice(number(0.2333)), "found 0.2333 price ok") + }) + + t.Run("case4", func(t *testing.T) { + upper := number(90.0) + lower := number(10.0) + size := number(7.0) + grid := NewGrid(lower, upper, size, number(0.001)) + grid.CalculateArithmeticPins() + + assert.Equal(t, []Pin{ + Pin(number("10.0")), + Pin(number("23.333")), + Pin(number("36.666")), + Pin(number("50.00")), + Pin(number("63.333")), + Pin(number("76.666")), + Pin(number("90.0")), + }, grid.Pins) + + assert.False(t, grid.HasPrice(number(200.0)), "out of range") + assert.True(t, grid.HasPrice(number("36.666")), "found 36.666 price ok") + }) + +} \ No newline at end of file diff --git a/pkg/strategy/grid2/grid_int64_test.go b/pkg/strategy/grid2/grid_int64_test.go new file mode 100644 index 0000000000..1603642c72 --- /dev/null +++ b/pkg/strategy/grid2/grid_int64_test.go @@ -0,0 +1,93 @@ +//go:build !dnum + +package grid2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGrid_HasPrice(t *testing.T) { + t.Run("case1", func(t *testing.T) { + upper := number(500.0) + lower := number(100.0) + size := number(5.0) + grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculateArithmeticPins() + + assert.True(t, grid.HasPrice(number(500.0)), "upper price") + assert.True(t, grid.HasPrice(number(100.0)), "lower price") + assert.True(t, grid.HasPrice(number(200.0)), "found 200 price ok") + assert.True(t, grid.HasPrice(number(300.0)), "found 300 price ok") + }) + + t.Run("case2", func(t *testing.T) { + upper := number(0.9) + lower := number(0.1) + size := number(7.0) + grid := NewGrid(lower, upper, size, number(0.00000001)) + grid.CalculateArithmeticPins() + + assert.Equal(t, []Pin{ + Pin(number(0.1)), + Pin(number(0.23333333)), + Pin(number(0.36666666)), + Pin(number(0.49999999)), + Pin(number(0.63333332)), + Pin(number(0.76666665)), + Pin(number(0.9)), + }, grid.Pins) + + assert.False(t, grid.HasPrice(number(200.0)), "out of range") + assert.True(t, grid.HasPrice(number(0.9)), "upper price") + assert.True(t, grid.HasPrice(number(0.1)), "lower price") + assert.True(t, grid.HasPrice(number(0.49999999)), "found 0.49999999 price ok") + }) + + t.Run("case3", func(t *testing.T) { + upper := number(0.9) + lower := number(0.1) + size := number(7.0) + grid := NewGrid(lower, upper, size, number(0.0001)) + grid.CalculateArithmeticPins() + + assert.Equal(t, []Pin{ + Pin(number(0.1)), + Pin(number(0.2333)), + Pin(number(0.3666)), + Pin(number(0.5000)), + Pin(number(0.6333)), + Pin(number(0.7666)), + Pin(number(0.9)), + }, grid.Pins) + + assert.False(t, grid.HasPrice(number(200.0)), "out of range") + assert.True(t, grid.HasPrice(number(0.9)), "upper price") + assert.True(t, grid.HasPrice(number(0.1)), "lower price") + assert.True(t, grid.HasPrice(number(0.5)), "found 0.5 price ok") + assert.True(t, grid.HasPrice(number(0.2333)), "found 0.2333 price ok") + }) + + t.Run("case4", func(t *testing.T) { + upper := number(90.0) + lower := number(10.0) + size := number(7.0) + grid := NewGrid(lower, upper, size, number(0.001)) + grid.CalculateArithmeticPins() + + assert.Equal(t, []Pin{ + Pin(number("10.0")), + Pin(number("23.333")), + Pin(number("36.666")), + Pin(number("50.00")), + Pin(number("63.333")), + Pin(number("76.666")), + Pin(number("90.0")), + }, grid.Pins) + + assert.False(t, grid.HasPrice(number(200.0)), "out of range") + assert.True(t, grid.HasPrice(number("36.666")), "found 36.666 price ok") + }) + +} \ No newline at end of file diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 20de41626c..cf242b7e8a 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -1,5 +1,3 @@ -//go:build !dnum - package grid2 import ( @@ -126,90 +124,6 @@ func TestGrid_NextLowerPin(t *testing.T) { assert.Equal(t, Pin(fixedpoint.Zero), next) } -func TestGrid_HasPrice(t *testing.T) { - t.Run("case1", func(t *testing.T) { - upper := number(500.0) - lower := number(100.0) - size := number(5.0) - grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculateArithmeticPins() - - assert.True(t, grid.HasPrice(number(500.0)), "upper price") - assert.True(t, grid.HasPrice(number(100.0)), "lower price") - assert.True(t, grid.HasPrice(number(200.0)), "found 200 price ok") - assert.True(t, grid.HasPrice(number(300.0)), "found 300 price ok") - }) - - t.Run("case2", func(t *testing.T) { - upper := number(0.9) - lower := number(0.1) - size := number(7.0) - grid := NewGrid(lower, upper, size, number(0.00000001)) - grid.CalculateArithmeticPins() - - assert.Equal(t, []Pin{ - Pin(number(0.1)), - Pin(number(0.23333333)), - Pin(number(0.36666666)), - Pin(number(0.49999999)), - Pin(number(0.63333332)), - Pin(number(0.76666665)), - Pin(number(0.9)), - }, grid.Pins) - - assert.False(t, grid.HasPrice(number(200.0)), "out of range") - assert.True(t, grid.HasPrice(number(0.9)), "upper price") - assert.True(t, grid.HasPrice(number(0.1)), "lower price") - assert.True(t, grid.HasPrice(number(0.49999999)), "found 0.49999999 price ok") - }) - - t.Run("case3", func(t *testing.T) { - upper := number(0.9) - lower := number(0.1) - size := number(7.0) - grid := NewGrid(lower, upper, size, number(0.0001)) - grid.CalculateArithmeticPins() - - assert.Equal(t, []Pin{ - Pin(number(0.1)), - Pin(number(0.2333)), - Pin(number(0.3666)), - Pin(number(0.5000)), - Pin(number(0.6333)), - Pin(number(0.7666)), - Pin(number(0.9)), - }, grid.Pins) - - assert.False(t, grid.HasPrice(number(200.0)), "out of range") - assert.True(t, grid.HasPrice(number(0.9)), "upper price") - assert.True(t, grid.HasPrice(number(0.1)), "lower price") - assert.True(t, grid.HasPrice(number(0.5)), "found 0.5 price ok") - assert.True(t, grid.HasPrice(number(0.2333)), "found 0.2333 price ok") - }) - - t.Run("case4", func(t *testing.T) { - upper := number(90.0) - lower := number(10.0) - size := number(7.0) - grid := NewGrid(lower, upper, size, number(0.001)) - grid.CalculateArithmeticPins() - - assert.Equal(t, []Pin{ - Pin(number("10.0")), - Pin(number("23.333")), - Pin(number("36.666")), - Pin(number("50.00")), - Pin(number("63.333")), - Pin(number("76.666")), - Pin(number("90.0")), - }, grid.Pins) - - assert.False(t, grid.HasPrice(number(200.0)), "out of range") - assert.True(t, grid.HasPrice(number("36.666")), "found 36.666 price ok") - }) - -} - func TestGrid_NextHigherPin(t *testing.T) { upper := number(500.0) lower := number(100.0) @@ -283,6 +197,7 @@ func Test_calculateArithmeticPins(t *testing.T) { Pin(number(2800.000)), Pin(number(2866.660)), Pin(number(2933.330)), + Pin(number("3000.00")), }, }, } @@ -298,4 +213,4 @@ func Test_calculateArithmeticPins(t *testing.T) { } }) } -} +} \ No newline at end of file From 8c337cddec97f245bbb35b54841407aebcaf9904 Mon Sep 17 00:00:00 2001 From: chiahung Date: Mon, 20 Mar 2023 21:18:42 +0800 Subject: [PATCH 0609/1392] add test for dnum --- pkg/fixedpoint/convert_test.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pkg/fixedpoint/convert_test.go b/pkg/fixedpoint/convert_test.go index c0f5369148..866c89b52b 100644 --- a/pkg/fixedpoint/convert_test.go +++ b/pkg/fixedpoint/convert_test.go @@ -52,17 +52,15 @@ func Test_FormatString(t *testing.T) { }) // comment out negative precision for dnum testing - /* - t.Run("12.3456789 with prec = -1, expected 10", func(t *testing.T) { - v := MustNewFromString("12.3456789") - s := v.FormatString(-1) - assert.Equal("10", s) - }) + t.Run("12.3456789 with prec = -1, expected 10", func(t *testing.T) { + v := MustNewFromString("12.3456789") + s := v.FormatString(-1) + assert.Equal("10", s) + }) - t.Run("12.3456789 with prec = -3, expected = 0", func(t *testing.T) { - v := MustNewFromString("12.3456789") - s := v.FormatString(-2) - assert.Equal("0", s) - }) - */ + t.Run("12.3456789 with prec = -3, expected = 0", func(t *testing.T) { + v := MustNewFromString("12.3456789") + s := v.FormatString(-2) + assert.Equal("0", s) + }) } From aa419e84688454578e6910c9d5eb72da0b600b72 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Mon, 20 Mar 2023 22:34:02 +0800 Subject: [PATCH 0610/1392] make dnum support negative precision --- pkg/fixedpoint/convert_test.go | 160 ++++++++++++++++++++++----------- pkg/fixedpoint/dec.go | 31 +++++-- 2 files changed, 135 insertions(+), 56 deletions(-) diff --git a/pkg/fixedpoint/convert_test.go b/pkg/fixedpoint/convert_test.go index 866c89b52b..a992c0f79e 100644 --- a/pkg/fixedpoint/convert_test.go +++ b/pkg/fixedpoint/convert_test.go @@ -1,66 +1,124 @@ package fixedpoint import ( + "fmt" "testing" "github.com/stretchr/testify/assert" ) func Test_FormatString(t *testing.T) { - assert := assert.New(t) + cases := []struct { + input string + prec int + expected string + }{ + {input: "0.57", prec: 5, expected: "0.57000"}, + {input: "-0.57", prec: 5, expected: "-0.57000"}, + {input: "0.57123456", prec: 8, expected: "0.57123456"}, + {input: "-0.57123456", prec: 8, expected: "-0.57123456"}, + {input: "0.57123456", prec: 5, expected: "0.57123"}, + {input: "-0.57123456", prec: 5, expected: "-0.57123"}, + {input: "0.57123456", prec: 0, expected: "0"}, + {input: "-0.57123456", prec: 0, expected: "0"}, + {input: "0.57123456", prec: -1, expected: "0"}, + {input: "-0.57123456", prec: -1, expected: "0"}, + {input: "0.57123456", prec: -5, expected: "0"}, + {input: "-0.57123456", prec: -5, expected: "0"}, + {input: "0.57123456", prec: -9, expected: "0"}, + {input: "-0.57123456", prec: -9, expected: "0"}, - t.Run("0.57 with prec = 5, expected 0.57", func(t *testing.T) { - v := MustNewFromString("0.57") - s := v.FormatString(5) - assert.Equal("0.57000", s) - }) + {input: "1.23456789", prec: 9, expected: "1.234567890"}, + {input: "-1.23456789", prec: 9, expected: "-1.234567890"}, + {input: "1.02345678", prec: 9, expected: "1.023456780"}, + {input: "-1.02345678", prec: 9, expected: "-1.023456780"}, + {input: "1.02345678", prec: 2, expected: "1.02"}, + {input: "-1.02345678", prec: 2, expected: "-1.02"}, + {input: "1.02345678", prec: 0, expected: "1"}, + {input: "-1.02345678", prec: 0, expected: "-1"}, + {input: "1.02345678", prec: -1, expected: "0"}, + {input: "-1.02345678", prec: -1, expected: "0"}, + {input: "1.02345678", prec: -10, expected: "0"}, + {input: "-1.02345678", prec: -10, expected: "0"}, - t.Run("0.57123456 with prec = 5, expected 0.57123", func(t *testing.T) { - v := MustNewFromString("0.57123456") - s := v.FormatString(5) - assert.Equal("0.57123", s) - }) + {input: "0.0001234", prec: 9, expected: "0.000123400"}, + {input: "-0.0001234", prec: 9, expected: "-0.000123400"}, + {input: "0.0001234", prec: 7, expected: "0.0001234"}, + {input: "-0.0001234", prec: 7, expected: "-0.0001234"}, + {input: "0.0001234", prec: 5, expected: "0.00012"}, + {input: "-0.0001234", prec: 5, expected: "-0.00012"}, + {input: "0.0001234", prec: 3, expected: "0.000"}, + {input: "-0.0001234", prec: 3, expected: "0.000"}, + {input: "0.0001234", prec: 2, expected: "0.00"}, + {input: "-0.0001234", prec: 2, expected: "0.00"}, + {input: "0.0001234", prec: 0, expected: "0"}, + {input: "-0.0001234", prec: 0, expected: "0"}, + {input: "0.00001234", prec: -1, expected: "0"}, + {input: "-0.00001234", prec: -1, expected: "0"}, + {input: "0.00001234", prec: -5, expected: "0"}, + {input: "-0.00001234", prec: -5, expected: "0"}, + {input: "0.00001234", prec: -9, expected: "0"}, + {input: "-0.00001234", prec: -9, expected: "0"}, - t.Run("1.23456789 with prec = 9, expected 1.23456789", func(t *testing.T) { - v := MustNewFromString("1.23456789") - s := v.FormatString(9) - assert.Equal("1.234567890", s) - }) + {input: "12.3456789", prec: 10, expected: "12.3456789000"}, + {input: "-12.3456789", prec: 10, expected: "-12.3456789000"}, + {input: "12.3456789", prec: 9, expected: "12.345678900"}, + {input: "-12.3456789", prec: 9, expected: "-12.345678900"}, + {input: "12.3456789", prec: 7, expected: "12.3456789"}, + {input: "-12.3456789", prec: 7, expected: "-12.3456789"}, + {input: "12.3456789", prec: 5, expected: "12.34567"}, + {input: "-12.3456789", prec: 5, expected: "-12.34567"}, + {input: "12.3456789", prec: 1, expected: "12.3"}, + {input: "-12.3456789", prec: 1, expected: "-12.3"}, + {input: "12.3456789", prec: 0, expected: "12"}, + {input: "-12.3456789", prec: 0, expected: "-12"}, + {input: "12.3456789", prec: -1, expected: "10"}, + {input: "-12.3456789", prec: -1, expected: "-10"}, + {input: "12.3456789", prec: -2, expected: "0"}, + {input: "-12.3456789", prec: -2, expected: "0"}, + {input: "12.3456789", prec: -3, expected: "0"}, + {input: "-12.3456789", prec: -3, expected: "0"}, - t.Run("1.02345678 with prec = 9, expected 1.02345678", func(t *testing.T) { - v := MustNewFromString("1.02345678") - s := v.FormatString(9) - assert.Equal("1.023456780", s) - }) + {input: "12345678.9", prec: 10, expected: "12345678.9000000000"}, + {input: "-12345678.9", prec: 10, expected: "-12345678.9000000000"}, + {input: "12345678.9", prec: 3, expected: "12345678.900"}, + {input: "-12345678.9", prec: 3, expected: "-12345678.900"}, + {input: "12345678.9", prec: 1, expected: "12345678.9"}, + {input: "-12345678.9", prec: 1, expected: "-12345678.9"}, + {input: "12345678.9", prec: 0, expected: "12345678"}, + {input: "-12345678.9", prec: 0, expected: "-12345678"}, + {input: "12345678.9", prec: -2, expected: "12345600"}, + {input: "-12345678.9", prec: -2, expected: "-12345600"}, + {input: "12345678.9", prec: -5, expected: "12300000"}, + {input: "-12345678.9", prec: -5, expected: "-12300000"}, + {input: "12345678.9", prec: -7, expected: "10000000"}, + {input: "-12345678.9", prec: -7, expected: "-10000000"}, + {input: "12345678.9", prec: -8, expected: "0"}, + {input: "-12345678.9", prec: -8, expected: "0"}, + {input: "12345678.9", prec: -10, expected: "0"}, + {input: "-12345678.9", prec: -10, expected: "0"}, - t.Run("-0.57 with prec = 5, expected -0.57", func(t *testing.T) { - v := MustNewFromString("-0.57") - s := v.FormatString(5) - assert.Equal("-0.57000", s) - }) + {input: "123000", prec: 7, expected: "123000.0000000"}, + {input: "-123000", prec: 7, expected: "-123000.0000000"}, + {input: "123000", prec: 2, expected: "123000.00"}, + {input: "-123000", prec: 2, expected: "-123000.00"}, + {input: "123000", prec: 0, expected: "123000"}, + {input: "-123000", prec: 0, expected: "-123000"}, + {input: "123000", prec: -1, expected: "123000"}, + {input: "-123000", prec: -1, expected: "-123000"}, + {input: "123000", prec: -5, expected: "100000"}, + {input: "-123000", prec: -5, expected: "-100000"}, + {input: "123000", prec: -6, expected: "0"}, + {input: "-123000", prec: -6, expected: "0"}, + {input: "123000", prec: -8, expected: "0"}, + {input: "-123000", prec: -8, expected: "0"}, + } - t.Run("-1.23456789 with prec = 9, expected 1.23456789", func(t *testing.T) { - v := MustNewFromString("-1.23456789") - s := v.FormatString(9) - assert.Equal("-1.234567890", s) - }) - - t.Run("-0.00001234 with prec = 3, expected = 0.000", func(t *testing.T) { - v := MustNewFromString("-0.0001234") - s := v.FormatString(3) - assert.Equal("0.000", s) - }) - - // comment out negative precision for dnum testing - t.Run("12.3456789 with prec = -1, expected 10", func(t *testing.T) { - v := MustNewFromString("12.3456789") - s := v.FormatString(-1) - assert.Equal("10", s) - }) - - t.Run("12.3456789 with prec = -3, expected = 0", func(t *testing.T) { - v := MustNewFromString("12.3456789") - s := v.FormatString(-2) - assert.Equal("0", s) - }) -} + for _, c := range cases { + t.Run(fmt.Sprintf("%s with prec = %d, expected %s", c.input, c.prec, c.expected), func(t *testing.T) { + v := MustNewFromString(c.input) + s := v.FormatString(c.prec) + assert.Equal(t, c.expected, s) + }) + } +} \ No newline at end of file diff --git a/pkg/fixedpoint/dec.go b/pkg/fixedpoint/dec.go index 85aad5d68f..41318f76d2 100644 --- a/pkg/fixedpoint/dec.go +++ b/pkg/fixedpoint/dec.go @@ -270,25 +270,46 @@ func (dn Value) FormatString(prec int) string { nd := len(digits) e := int(dn.exp) - nd if -maxLeadingZeros <= dn.exp && dn.exp <= 0 { + if prec < 0 { + return "0" + } // decimal to the left if prec+e+nd > 0 { return sign + "0." + strings.Repeat("0", -e-nd) + digits[:min(prec+e+nd, nd)] + strings.Repeat("0", max(0, prec-nd+e+nd)) - } else if -e-nd > 0 { - return "0." + strings.Repeat("0", -e-nd) + } else if -e-nd > 0 && prec != 0 { + return "0." + strings.Repeat("0", min(prec, -e-nd)) } else { return "0" } } else if -nd < e && e <= -1 { // decimal within dec := nd + e - decimals := digits[dec:min(dec+prec, nd)] - return sign + digits[:dec] + "." + decimals + strings.Repeat("0", max(0, prec-len(decimals))) + if prec > 0 { + decimals := digits[dec:min(dec+prec, nd)] + return sign + digits[:dec] + "." + decimals + strings.Repeat("0", max(0, prec-len(decimals))) + } else if prec == 0 { + return sign + digits[:dec] + } + + sigFigures := digits[0:max(dec+prec, 0)] + if len(sigFigures) == 0 { + return "0" + } + + return sign + sigFigures + strings.Repeat("0", max(-prec, 0)) + } else if 0 < dn.exp && dn.exp <= digitsMax { // decimal to the right if prec > 0 { return sign + digits + strings.Repeat("0", e) + "." + strings.Repeat("0", prec) - } else { + } else if prec+e >= 0 { return sign + digits + strings.Repeat("0", e) + } else { + if len(digits) <= -prec-e { + return "0" + } + + return sign + digits[0:len(digits)+prec+e] + strings.Repeat("0", -prec) } } else { // scientific notation From fda4e48146657cc3516c02c8097b0123295fa46b Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 21 Mar 2023 16:25:16 +0800 Subject: [PATCH 0611/1392] max: move submitOrderLimiter to the exchange wide var --- pkg/exchange/max/exchange.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index ad58d05400..4ed59aa148 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -25,7 +25,6 @@ var closedOrderQueryLimiter = rate.NewLimiter(rate.Every(1*time.Second), 1) var tradeQueryLimiter = rate.NewLimiter(rate.Every(3*time.Second), 1) var accountQueryLimiter = rate.NewLimiter(rate.Every(3*time.Second), 1) var marketDataLimiter = rate.NewLimiter(rate.Every(2*time.Second), 10) -var submitOrderLimiter = rate.NewLimiter(rate.Every(300*time.Millisecond), 10) var log = logrus.WithField("exchange", "max") @@ -37,6 +36,8 @@ type Exchange struct { v3order *v3.OrderService v3margin *v3.MarginService + + submitOrderLimiter *rate.Limiter } func New(key, secret string) *Exchange { @@ -54,6 +55,8 @@ func New(key, secret string) *Exchange { secret: secret, v3order: &v3.OrderService{Client: client}, v3margin: &v3.MarginService{Client: client}, + + submitOrderLimiter: rate.NewLimiter(rate.Every(100*time.Millisecond), 10), } } @@ -452,7 +455,7 @@ func (e *Exchange) Withdraw(ctx context.Context, asset string, amount fixedpoint } func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) { - if err := submitOrderLimiter.Wait(ctx); err != nil { + if err := e.submitOrderLimiter.Wait(ctx); err != nil { return nil, err } From 88af0a18f9861a51dde1b5b86e73ecdef53291d0 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 21 Mar 2023 16:26:47 +0800 Subject: [PATCH 0612/1392] max: move tradeQueryLimiter to the exchange instance --- pkg/exchange/max/exchange.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 4ed59aa148..9863bfecec 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -22,7 +22,6 @@ import ( // closedOrderQueryLimiter is used for the closed orders query rate limit, 1 request per second var closedOrderQueryLimiter = rate.NewLimiter(rate.Every(1*time.Second), 1) -var tradeQueryLimiter = rate.NewLimiter(rate.Every(3*time.Second), 1) var accountQueryLimiter = rate.NewLimiter(rate.Every(3*time.Second), 1) var marketDataLimiter = rate.NewLimiter(rate.Every(2*time.Second), 10) @@ -37,7 +36,7 @@ type Exchange struct { v3order *v3.OrderService v3margin *v3.MarginService - submitOrderLimiter *rate.Limiter + submitOrderLimiter, queryTradeLimiter *rate.Limiter } func New(key, secret string) *Exchange { @@ -56,6 +55,7 @@ func New(key, secret string) *Exchange { v3order: &v3.OrderService{Client: client}, v3margin: &v3.MarginService{Client: client}, + queryTradeLimiter: rate.NewLimiter(rate.Every(1*time.Second), 2), submitOrderLimiter: rate.NewLimiter(rate.Every(100*time.Millisecond), 10), } } @@ -795,7 +795,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, // give LastTradeID -> ignore start_time (but still can filter the end_time) // without any parameters -> return trades within 24 hours func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { - if err := tradeQueryLimiter.Wait(ctx); err != nil { + if err := e.queryTradeLimiter.Wait(ctx); err != nil { return nil, err } From ab52dd6349dab3e3d7c3df6f1be39d81ca4b8d4a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:11:58 +0800 Subject: [PATCH 0613/1392] funding: filter kline event with types.KLineWith --- pkg/strategy/funding/strategy.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/funding/strategy.go b/pkg/strategy/funding/strategy.go index 58361e9634..d056a8fb77 100644 --- a/pkg/strategy/funding/strategy.go +++ b/pkg/strategy/funding/strategy.go @@ -127,11 +127,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { // skip k-lines from other symbols - if kline.Symbol != s.Symbol { - return - } for _, detection := range s.SupportDetection { var lastMA = ma.Last() @@ -195,6 +192,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.Notify(kline) } } - }) + })) return nil } From 12b9775eb3310e69f030cc0adf59ba2d571e2090 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:17:33 +0800 Subject: [PATCH 0614/1392] rename funding to xfunding --- pkg/cmd/strategy/builtin.go | 2 +- .../{funding => xfunding}/strategy.go | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) rename pkg/strategy/{funding => xfunding}/strategy.go (92%) diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index edfdce4751..50e1ec8bbd 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -16,7 +16,6 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/fixedmaker" _ "github.com/c9s/bbgo/pkg/strategy/flashcrash" _ "github.com/c9s/bbgo/pkg/strategy/fmaker" - _ "github.com/c9s/bbgo/pkg/strategy/funding" _ "github.com/c9s/bbgo/pkg/strategy/grid" _ "github.com/c9s/bbgo/pkg/strategy/grid2" _ "github.com/c9s/bbgo/pkg/strategy/harmonic" @@ -38,6 +37,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/trendtrader" _ "github.com/c9s/bbgo/pkg/strategy/wall" _ "github.com/c9s/bbgo/pkg/strategy/xbalance" + _ "github.com/c9s/bbgo/pkg/strategy/xfunding" _ "github.com/c9s/bbgo/pkg/strategy/xgap" _ "github.com/c9s/bbgo/pkg/strategy/xmaker" _ "github.com/c9s/bbgo/pkg/strategy/xnav" diff --git a/pkg/strategy/funding/strategy.go b/pkg/strategy/xfunding/strategy.go similarity index 92% rename from pkg/strategy/funding/strategy.go rename to pkg/strategy/xfunding/strategy.go index d056a8fb77..407fcc6f0b 100644 --- a/pkg/strategy/funding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -1,4 +1,4 @@ -package funding +package xfunding import ( "context" @@ -14,7 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -const ID = "funding" +const ID = "xfunding" var log = logrus.WithField("strategy", ID) @@ -66,6 +66,11 @@ func (s *Strategy) ID() string { return ID } +func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { + // TODO implement me + panic("implement me") +} + func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) @@ -99,11 +104,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } - premiumIndex, err := session.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) - if err != nil { - log.Error("exchange does not support funding rate api") - } - var ma types.Float64Indicator for _, detection := range s.SupportDetection { @@ -124,10 +124,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Window: detection.MovingAverageIntervalWindow.Window, }) } - } session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + premiumIndex, err := session.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) + if err != nil { + log.Error("exchange does not support funding rate api") + } + // skip k-lines from other symbols for _, detection := range s.SupportDetection { var lastMA = ma.Last() @@ -195,3 +199,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se })) return nil } + +func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { + // TODO implement me + panic("implement me") +} From b2c0a343f33a26ae9b03409ec8c8934502a2ffbe Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:17:45 +0800 Subject: [PATCH 0615/1392] rename funding config file --- config/{funding.yaml => xfunding.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config/{funding.yaml => xfunding.yaml} (100%) diff --git a/config/funding.yaml b/config/xfunding.yaml similarity index 100% rename from config/funding.yaml rename to config/xfunding.yaml From e93d13e425bc0b82c6dd6b2a361ada7d47eb9867 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:36:42 +0800 Subject: [PATCH 0616/1392] xfunding: implement CrossRun --- pkg/strategy/xfunding/strategy.go | 64 ++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 407fcc6f0b..fa511e611c 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -3,6 +3,7 @@ package xfunding import ( "context" "errors" + "fmt" "strings" "github.com/sirupsen/logrus" @@ -26,6 +27,8 @@ func init() { } type Strategy struct { + Environment *bbgo.Environment + // These fields will be filled from the config file (it translates YAML to JSON) Symbol string `json:"symbol"` Market types.Market `json:"-"` @@ -34,9 +37,8 @@ type Strategy struct { // Interval types.Interval `json:"interval"` FundingRate *struct { - High fixedpoint.Value `json:"high"` - Neutral fixedpoint.Value `json:"neutral"` - DiffThreshold fixedpoint.Value `json:"diffThreshold"` + High fixedpoint.Value `json:"high"` + Neutral fixedpoint.Value `json:"neutral"` } `json:"fundingRate"` SupportDetection []struct { @@ -60,6 +62,19 @@ type Strategy struct { MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` } `json:"supportDetection"` + + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + + SpotPosition *types.Position `persistence:"spot_position"` + FuturesPosition *types.Position `persistence:"futures_position"` + + spotSession, futuresSession *bbgo.ExchangeSession + + spotOrderExecutor, futuresOrderExecutor bbgo.OrderExecutor + spotMarket, futuresMarket types.Market + + SpotSession string `json:"spotSession"` + FuturesSession string `json:"futuresSession"` } func (s *Strategy) ID() string { @@ -96,6 +111,10 @@ func (s *Strategy) Validate() error { return nil } +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s-%s", ID, s.Symbol) +} + func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { standardIndicatorSet := session.StandardIndicatorSet(s.Symbol) @@ -201,6 +220,41 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { - // TODO implement me - panic("implement me") + instanceID := s.InstanceID() + + // TODO: add safety check + s.spotSession = sessions[s.SpotSession] + s.futuresSession = sessions[s.FuturesSession] + + s.spotMarket, _ = s.spotSession.Market(s.Symbol) + s.futuresMarket, _ = s.futuresSession.Market(s.Symbol) + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.FuturesPosition == nil { + s.FuturesPosition = types.NewPositionFromMarket(s.futuresMarket) + } + + if s.SpotPosition == nil { + s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) + } + + s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) + s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) + return nil +} + +func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { + orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position) + orderExecutor.BindEnvironment(s.Environment) + orderExecutor.Bind() + orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { + s.ProfitStats.AddTrade(trade) + }) + orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + return orderExecutor } From b881aea2282c3f4411350f5b66d494e36bf110b4 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:38:56 +0800 Subject: [PATCH 0617/1392] add position action --- pkg/strategy/xfunding/strategy.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index fa511e611c..44b3cab841 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -17,6 +17,12 @@ import ( const ID = "xfunding" +const ( + PositionNoOp = iota + PositionOpening + PositionClosing +) + var log = logrus.WithField("strategy", ID) func init() { From d6c430a4b4d8f66ea5d6abe65de9cee0fddbfc49 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:42:06 +0800 Subject: [PATCH 0618/1392] xfunding: implement CrossSubscribe --- pkg/strategy/xfunding/strategy.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 44b3cab841..834cf48841 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -17,8 +17,10 @@ import ( const ID = "xfunding" +type PositionAction int + const ( - PositionNoOp = iota + PositionNoOp PositionAction = iota PositionOpening PositionClosing ) @@ -81,6 +83,9 @@ type Strategy struct { SpotSession string `json:"spotSession"` FuturesSession string `json:"futuresSession"` + + // positionAction is default to NoOp + positionAction PositionAction } func (s *Strategy) ID() string { @@ -88,8 +93,17 @@ func (s *Strategy) ID() string { } func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { - // TODO implement me - panic("implement me") + // TODO: add safety check + spotSession := sessions[s.SpotSession] + futuresSession := sessions[s.FuturesSession] + + spotSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: types.Interval1m, + }) + + futuresSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: types.Interval1m, + }) } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { From e607fc19acc252defa22b61e92181cde0ef92eb6 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:42:44 +0800 Subject: [PATCH 0619/1392] xfunding: check spotSession, futuresSession names --- pkg/strategy/xfunding/strategy.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 834cf48841..1879d626d4 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -128,6 +128,14 @@ func (s *Strategy) Validate() error { return errors.New("symbol is required") } + if len(s.SpotSession) == 0 { + return errors.New("spotSession name is required") + } + + if len(s.FuturesSession) == 0 { + return errors.New("futuresSession name is required") + } + return nil } From 98b0ffa510a8a98cf904989baf86ab249d2980fb Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 22:01:59 +0800 Subject: [PATCH 0620/1392] all: add more futures channel types --- pkg/strategy/xfunding/strategy.go | 6 ------ pkg/types/channel.go | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 1879d626d4..055d2c6760 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -107,12 +107,6 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - // session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) - - // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - // Interval: string(s.Interval), - // }) - for _, detection := range s.SupportDetection { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: detection.Interval, diff --git a/pkg/types/channel.go b/pkg/types/channel.go index 8b9b48e0f6..b9aeb54605 100644 --- a/pkg/types/channel.go +++ b/pkg/types/channel.go @@ -2,8 +2,15 @@ package types type Channel string -var BookChannel = Channel("book") -var KLineChannel = Channel("kline") -var BookTickerChannel = Channel("bookticker") -var MarketTradeChannel = Channel("trade") -var AggTradeChannel = Channel("aggTrade") +const ( + BookChannel = Channel("book") + KLineChannel = Channel("kline") + BookTickerChannel = Channel("bookTicker") + MarketTradeChannel = Channel("trade") + AggTradeChannel = Channel("aggTrade") + + // channels for futures + MarkPriceChannel = Channel("markPrice") + LiquidationOrderChannel = Channel("liquidationOrder") + ContractChannel = Channel("contract") +) From 3c69ccc25a56c36195dbe406da8625b6a96cea6c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 22:04:02 +0800 Subject: [PATCH 0621/1392] types: update channel names --- pkg/types/channel.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/types/channel.go b/pkg/types/channel.go index b9aeb54605..2e85b92364 100644 --- a/pkg/types/channel.go +++ b/pkg/types/channel.go @@ -10,7 +10,10 @@ const ( AggTradeChannel = Channel("aggTrade") // channels for futures - MarkPriceChannel = Channel("markPrice") + MarkPriceChannel = Channel("markPrice") + LiquidationOrderChannel = Channel("liquidationOrder") - ContractChannel = Channel("contract") + + // ContractInfoChannel is the contract info provided by the exchange + ContractInfoChannel = Channel("contractInfo") ) From 6265ad248ee0d76ea4d2455255d61b57b167c555 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 22:15:01 +0800 Subject: [PATCH 0622/1392] xfunding: add premium checker --- pkg/strategy/xfunding/strategy.go | 102 ++++++++---------------------- 1 file changed, 27 insertions(+), 75 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 055d2c6760..29e62bb3db 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -44,10 +44,12 @@ type Strategy struct { MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` // Interval types.Interval `json:"interval"` - FundingRate *struct { - High fixedpoint.Value `json:"high"` - Neutral fixedpoint.Value `json:"neutral"` - } `json:"fundingRate"` + // ShortFundingRate is the funding rate range for short positions + // TODO: right now we don't support negative funding rate (long position) since it's rarer + ShortFundingRate *struct { + High fixedpoint.Value `json:"high"` + Low fixedpoint.Value `json:"low"` + } `json:"shortFundingRate"` SupportDetection []struct { Interval types.Interval `json:"interval"` @@ -86,6 +88,7 @@ type Strategy struct { // positionAction is default to NoOp positionAction PositionAction + positionType types.PositionType } func (s *Strategy) ID() string { @@ -167,77 +170,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { - premiumIndex, err := session.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) - if err != nil { - log.Error("exchange does not support funding rate api") - } - - // skip k-lines from other symbols - for _, detection := range s.SupportDetection { - var lastMA = ma.Last() - - closePrice := kline.GetClose() - closePriceF := closePrice.Float64() - // skip if the closed price is under the moving average - if closePriceF < lastMA { - log.Infof("skip %s closed price %v < last ma %f", s.Symbol, closePrice, lastMA) - return - } - - fundingRate := premiumIndex.LastFundingRate - - if fundingRate.Compare(s.FundingRate.High) >= 0 { - bbgo.Notify("%s funding rate %s is too high! threshold %s", - s.Symbol, - fundingRate.Percentage(), - s.FundingRate.High.Percentage(), - ) - } else { - log.Infof("skip funding rate is too low") - return - } - - prettyBaseVolume := s.Market.BaseCurrencyFormatter() - prettyQuoteVolume := s.Market.QuoteCurrencyFormatter() - - if detection.MinVolume.Sign() > 0 && kline.Volume.Compare(detection.MinVolume) > 0 { - bbgo.Notify("Detected %s %s resistance base volume %s > min base volume %s, quote volume %s", - s.Symbol, detection.Interval.String(), - prettyBaseVolume.FormatMoney(kline.Volume.Trunc()), - prettyBaseVolume.FormatMoney(detection.MinVolume.Trunc()), - prettyQuoteVolume.FormatMoney(kline.QuoteVolume.Trunc()), - ) - bbgo.Notify(kline) - - baseBalance, ok := session.GetAccount().Balance(s.Market.BaseCurrency) - if !ok { - return - } - - if baseBalance.Available.Sign() > 0 && baseBalance.Total().Compare(s.MaxExposurePosition) < 0 { - log.Infof("opening a short position") - _, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: kline.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeMarket, - Quantity: s.Quantity, - }) - if err != nil { - log.WithError(err).Error("submit order error") - } - } - } else if detection.MinQuoteVolume.Sign() > 0 && kline.QuoteVolume.Compare(detection.MinQuoteVolume) > 0 { - bbgo.Notify("Detected %s %s resistance quote volume %s > min quote volume %s, base volume %s", - s.Symbol, detection.Interval.String(), - prettyQuoteVolume.FormatMoney(kline.QuoteVolume.Trunc()), - prettyQuoteVolume.FormatMoney(detection.MinQuoteVolume.Trunc()), - prettyBaseVolume.FormatMoney(kline.Volume.Trunc()), - ) - bbgo.Notify(kline) - } - } - })) return nil } @@ -265,6 +197,26 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) + + s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + premiumIndex, err := s.futuresSession.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) + if err != nil { + log.WithError(err).Error("premium index query error") + return + } + + fundingRate := premiumIndex.LastFundingRate + + if s.ShortFundingRate != nil { + if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + s.positionAction = PositionOpening + s.positionType = types.PositionShort + } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + s.positionAction = PositionClosing + } + } + })) + return nil } From dc5e0cbcc24c77e0c7ebb33c336efd52fe83499d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 22:15:24 +0800 Subject: [PATCH 0623/1392] xfunding: solve lint error --- pkg/strategy/xfunding/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 29e62bb3db..7dcf100a58 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -169,6 +169,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) } } + _ = ma return nil } From 928f668fecf21d85b23436310f068a656e3fd137 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 22:17:37 +0800 Subject: [PATCH 0624/1392] xfunding: pull out premium check to detectPremiumIndex --- pkg/strategy/xfunding/strategy.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 7dcf100a58..4fb91a94ac 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -206,21 +206,24 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return } - fundingRate := premiumIndex.LastFundingRate - - if s.ShortFundingRate != nil { - if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { - s.positionAction = PositionOpening - s.positionType = types.PositionShort - } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { - s.positionAction = PositionClosing - } - } + s.detectPremiumIndex(premiumIndex) })) return nil } +func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) { + fundingRate := premiumIndex.LastFundingRate + if s.ShortFundingRate != nil { + if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + s.positionAction = PositionOpening + s.positionType = types.PositionShort + } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + s.positionAction = PositionClosing + } + } +} + func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position) orderExecutor.BindEnvironment(s.Environment) From 684f6c6e1dd20e493ffac4651f7b33022063d584 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 00:23:51 +0800 Subject: [PATCH 0625/1392] xfunding: document spot trade handler --- pkg/strategy/xfunding/strategy.go | 133 ++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 14 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 4fb91a94ac..9958ef4854 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -38,11 +38,15 @@ type Strategy struct { Environment *bbgo.Environment // These fields will be filled from the config file (it translates YAML to JSON) - Symbol string `json:"symbol"` - Market types.Market `json:"-"` - Quantity fixedpoint.Value `json:"quantity,omitempty"` - MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` - // Interval types.Interval `json:"interval"` + Symbol string `json:"symbol"` + Market types.Market `json:"-"` + Quantity fixedpoint.Value `json:"quantity,omitempty"` + + // IncrementalQuoteQuantity is used for opening position incrementally with a small fixed quote quantity + // for example, 100usdt per order + IncrementalQuoteQuantity fixedpoint.Value `json:"incrementalQuoteQuantity"` + + QuoteInvestment fixedpoint.Value `json:"quoteInvestment"` // ShortFundingRate is the funding rate range for short positions // TODO: right now we don't support negative funding rate (long position) since it's rarer @@ -80,7 +84,7 @@ type Strategy struct { spotSession, futuresSession *bbgo.ExchangeSession - spotOrderExecutor, futuresOrderExecutor bbgo.OrderExecutor + spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor spotMarket, futuresMarket types.Market SpotSession string `json:"spotSession"` @@ -88,7 +92,12 @@ type Strategy struct { // positionAction is default to NoOp positionAction PositionAction - positionType types.PositionType + + // positionType is the futures position type + // currently we only support short position for the positive funding rate + positionType types.PositionType + + usedQuoteInvestment fixedpoint.Value } func (s *Strategy) ID() string { @@ -133,6 +142,10 @@ func (s *Strategy) Validate() error { return errors.New("futuresSession name is required") } + if s.QuoteInvestment.IsZero() { + return errors.New("quoteInvestment can not be zero") + } + return nil } @@ -143,11 +156,6 @@ func (s *Strategy) InstanceID() string { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { standardIndicatorSet := session.StandardIndicatorSet(s.Symbol) - if !session.Futures { - log.Error("futures not enabled in config for this strategy") - return nil - } - var ma types.Float64Indicator for _, detection := range s.SupportDetection { @@ -177,7 +185,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { instanceID := s.InstanceID() - // TODO: add safety check + s.usedQuoteInvestment = fixedpoint.Zero s.spotSession = sessions[s.SpotSession] s.futuresSession = sessions[s.FuturesSession] @@ -197,10 +205,48 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) + s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + // we act differently on the spot account + // when opening a position, we place orders on the spot account first, then the futures account, + // and we need to accumulate the used quote amount + // + // when closing a position, we place orders on the futures account first, then the spot account + // we need to close the position according to its base quantity instead of quote quantity + if s.positionType == types.PositionShort { + switch s.positionAction { + case PositionOpening: + if trade.Side != types.SideTypeSell { + log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade) + return + } + + // TODO: add mutex lock for this modification + s.usedQuoteInvestment = s.usedQuoteInvestment.Add(trade.QuoteQuantity) + if s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + s.positionAction = PositionNoOp + + // 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account + + // 2) transferred successfully, sync futures position + + // 3) compare spot position and futures position, increase the position size until they are the same size + } + + case PositionClosing: + if trade.Side != types.SideTypeBuy { + log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) + return + } + + } + } + }) + s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) + binanceExchange := s.futuresSession.Exchange.(*binance.Exchange) s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { - premiumIndex, err := s.futuresSession.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) + premiumIndex, err := binanceExchange.QueryPremiumIndex(ctx, s.Symbol) if err != nil { log.WithError(err).Error("premium index query error") return @@ -209,9 +255,68 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.detectPremiumIndex(premiumIndex) })) + s.spotSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { + // TODO: use go routine and time.Ticker + s.triggerPositionAction(ctx) + })) + return nil } +func (s *Strategy) syncSpotPosition(ctx context.Context) { + ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + + if s.positionType != types.PositionShort { + log.Errorf("funding long position type is not supported") + return + } + + switch s.positionAction { + + case PositionClosing: + + case PositionOpening: + if s.usedQuoteInvestment.IsZero() || s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + // stop + return + } + + leftQuote := s.QuoteInvestment.Sub(s.usedQuoteInvestment) + orderPrice := ticker.Sell + orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuote).Div(orderPrice) + orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) + createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.spotMarket, + TimeInForce: types.TimeInForceGTC, + }) + if err != nil { + log.WithError(err).Errorf("can not submit order") + return + } + + log.Infof("created orders: %+v", createdOrders) + } +} + +func (s *Strategy) triggerPositionAction(ctx context.Context) { + switch s.positionAction { + case PositionOpening: + s.syncSpotPosition(ctx) + + case PositionClosing: + + } +} + func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) { fundingRate := premiumIndex.LastFundingRate if s.ShortFundingRate != nil { From 6668d683e169a87cda80f61b9365f970706186bb Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 00:40:20 +0800 Subject: [PATCH 0626/1392] xfunding: adjust quoteInvestment according to the quote balance --- pkg/strategy/xfunding/strategy.go | 49 +++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 9958ef4854..00ba6e9718 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -186,12 +186,26 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order instanceID := s.InstanceID() s.usedQuoteInvestment = fixedpoint.Zero + s.spotSession = sessions[s.SpotSession] s.futuresSession = sessions[s.FuturesSession] s.spotMarket, _ = s.spotSession.Market(s.Symbol) s.futuresMarket, _ = s.futuresSession.Market(s.Symbol) + // adjust QuoteInvestment + if b, ok := s.spotSession.Account.Balance(s.spotMarket.QuoteCurrency); ok { + originalQuoteInvestment := s.QuoteInvestment + s.QuoteInvestment = fixedpoint.Min(b.Available, s.QuoteInvestment) + + if originalQuoteInvestment.Compare(s.QuoteInvestment) != 0 { + log.Infof("adjusted quoteInvestment from %s to %s according to the balance", + originalQuoteInvestment.String(), + s.QuoteInvestment.String(), + ) + } + } + if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.Market) } @@ -204,6 +218,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) } + binanceFutures := s.futuresSession.Exchange.(*binance.Exchange) + binanceSpot := s.spotSession.Exchange.(*binance.Exchange) + _ = binanceSpot + s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { // we act differently on the spot account @@ -224,13 +242,14 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.usedQuoteInvestment = s.usedQuoteInvestment.Add(trade.QuoteQuantity) if s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { s.positionAction = PositionNoOp + } - // 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account + // 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account + // balances, err := binanceSpot.QueryAccountBalances(ctx) - // 2) transferred successfully, sync futures position + // 2) transferred successfully, sync futures position - // 3) compare spot position and futures position, increase the position size until they are the same size - } + // 3) compare spot position and futures position, increase the position size until they are the same size case PositionClosing: if trade.Side != types.SideTypeBuy { @@ -244,9 +263,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) - binanceExchange := s.futuresSession.Exchange.(*binance.Exchange) s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { - premiumIndex, err := binanceExchange.QueryPremiumIndex(ctx, s.Symbol) + premiumIndex, err := binanceFutures.QueryPremiumIndex(ctx, s.Symbol) if err != nil { log.WithError(err).Error("premium index query error") return @@ -263,6 +281,25 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return nil } +// TODO: replace type binance.Exchange with an interface +func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade types.Trade) error { + balances, err := ex.QueryAccountBalances(ctx) + if err != nil { + return err + } + + b, ok := balances[s.spotMarket.BaseCurrency] + if !ok { + return nil + } + + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity + if b.Available.Compare(trade.Quantity) >= 0 { + + } + return nil +} + func (s *Strategy) syncSpotPosition(ctx context.Context) { ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { From c632e6efac50d5777af03dc1729237cd64fd3e8f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 00:54:37 +0800 Subject: [PATCH 0627/1392] binance: add binance futures_transfer_request --- .../binanceapi/futures_transfer_request.go | 33 ++++ .../futures_transfer_request_requestgen.go | 178 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 pkg/exchange/binance/binanceapi/futures_transfer_request.go create mode 100644 pkg/exchange/binance/binanceapi/futures_transfer_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/futures_transfer_request.go b/pkg/exchange/binance/binanceapi/futures_transfer_request.go new file mode 100644 index 0000000000..ee726f74aa --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_transfer_request.go @@ -0,0 +1,33 @@ +package binanceapi + +import "github.com/c9s/requestgen" + +type FuturesTransferType int + +const ( + FuturesTransferSpotToUsdtFutures FuturesTransferType = 1 + FuturesTransferUsdtFuturesToSpot FuturesTransferType = 2 + + FuturesTransferSpotToCoinFutures FuturesTransferType = 3 + FuturesTransferCoinFuturesToSpot FuturesTransferType = 4 +) + +type FuturesTransferResponse struct { + TranId int64 `json:"tranId"` +} + +//go:generate requestgen -method POST -url "/sapi/v1/futures/transfer" -type FuturesTransferRequest -responseType .FuturesTransferResponse +type FuturesTransferRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + + // amount is a decimal in string format + amount string `param:"amount"` + + transferType FuturesTransferType `param:"type"` +} + +func (c *RestClient) NewFuturesTransferRequest() *FuturesTransferRequest { + return &FuturesTransferRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_transfer_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_transfer_request_requestgen.go new file mode 100644 index 0000000000..50f4b46e9c --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_transfer_request_requestgen.go @@ -0,0 +1,178 @@ +// Code generated by "requestgen -method POST -url /sapi/v1/futures/transfer -type FuturesTransferRequest -responseType .FuturesTransferResponse"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (f *FuturesTransferRequest) Asset(asset string) *FuturesTransferRequest { + f.asset = asset + return f +} + +func (f *FuturesTransferRequest) Amount(amount string) *FuturesTransferRequest { + f.amount = amount + return f +} + +func (f *FuturesTransferRequest) TransferType(transferType FuturesTransferType) *FuturesTransferRequest { + f.transferType = transferType + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesTransferRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesTransferRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := f.asset + + // assign parameter of asset + params["asset"] = asset + // check amount field -> json key amount + amount := f.amount + + // assign parameter of amount + params["amount"] = amount + // check transferType field -> json key type + transferType := f.transferType + + // TEMPLATE check-valid-values + switch transferType { + case FuturesTransferSpotToUsdtFutures, FuturesTransferUsdtFuturesToSpot, FuturesTransferSpotToCoinFutures, FuturesTransferCoinFuturesToSpot: + params["type"] = transferType + + default: + return nil, fmt.Errorf("type value %v is invalid", transferType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of transferType + params["type"] = transferType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesTransferRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesTransferRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesTransferRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesTransferRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesTransferRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesTransferRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesTransferRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesTransferRequest) Do(ctx context.Context) (*FuturesTransferResponse, error) { + + params, err := f.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/sapi/v1/futures/transfer" + + req, err := f.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse FuturesTransferResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} From 6848e11e8a5002a814deb4b0ca01c6eff6df76b0 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 00:55:00 +0800 Subject: [PATCH 0628/1392] binance: implement TransferFuturesAsset --- pkg/exchange/binance/exchange.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index aea4e1459f..16dbfd39ed 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -367,6 +367,22 @@ func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) e return nil } +func (e *Exchange) TransferFuturesAsset(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { + req := e.client2.NewFuturesTransferRequest() + req.Asset(asset) + req.Amount(amount.String()) + + if io > 0 { // int + req.TransferType(binanceapi.FuturesTransferSpotToUsdtFutures) + } else if io < 0 { // out + req.TransferType(binanceapi.FuturesTransferUsdtFuturesToSpot) + } + + resp, err := req.Do(ctx) + log.Debugf("futures transfer %s %s, transaction = %+v", amount.String(), asset, resp) + return err +} + // transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { req := e.client.NewMarginTransferService() From 6ca85b175aaac6c34283221b2a91a512104b502f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 00:56:28 +0800 Subject: [PATCH 0629/1392] xfunding: adjust quote investment according to the fee rate --- pkg/strategy/xfunding/strategy.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 00ba6e9718..859aea3336 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -196,7 +196,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order // adjust QuoteInvestment if b, ok := s.spotSession.Account.Balance(s.spotMarket.QuoteCurrency); ok { originalQuoteInvestment := s.QuoteInvestment - s.QuoteInvestment = fixedpoint.Min(b.Available, s.QuoteInvestment) + + // adjust available quote with the fee rate + available := b.Available.Mul(fixedpoint.NewFromFloat(1.0 - (0.01 * 0.075))) + s.QuoteInvestment = fixedpoint.Min(available, s.QuoteInvestment) if originalQuoteInvestment.Compare(s.QuoteInvestment) != 0 { log.Infof("adjusted quoteInvestment from %s to %s according to the balance", From 6797069a40032016c3cf1eff4a550e4f621e1aea Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 02:42:05 +0800 Subject: [PATCH 0630/1392] binanceapi: fix payload encode format --- pkg/exchange/binance/binanceapi/client.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/exchange/binance/binanceapi/client.go b/pkg/exchange/binance/binanceapi/client.go index 0157131294..b9722380f0 100644 --- a/pkg/exchange/binance/binanceapi/client.go +++ b/pkg/exchange/binance/binanceapi/client.go @@ -233,6 +233,14 @@ func castPayload(payload interface{}) ([]byte, error) { case []byte: return v, nil + case map[string]interface{}: + var params = url.Values{} + for a, b := range v { + params.Add(a, fmt.Sprintf("%v", b)) + } + + return []byte(params.Encode()), nil + default: body, err := json.Marshal(v) return body, err From 487fbf868111f2beef1468d1ef3d91cab9256405 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 02:42:26 +0800 Subject: [PATCH 0631/1392] binance: implement TransferFuturesAccountAsset api --- pkg/exchange/binance/exchange.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 16dbfd39ed..9880b6d2db 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -367,33 +367,38 @@ func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) e return nil } -func (e *Exchange) TransferFuturesAsset(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { +func (e *Exchange) TransferFuturesAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { req := e.client2.NewFuturesTransferRequest() req.Asset(asset) req.Amount(amount.String()) - if io > 0 { // int + if io == types.TransferIn { req.TransferType(binanceapi.FuturesTransferSpotToUsdtFutures) - } else if io < 0 { // out + } else if io == types.TransferOut { req.TransferType(binanceapi.FuturesTransferUsdtFuturesToSpot) + } else { + return fmt.Errorf("unexpected transfer direction: %d given", io) } resp, err := req.Do(ctx) - log.Debugf("futures transfer %s %s, transaction = %+v", amount.String(), asset, resp) + log.Infof("futures transfer %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) return err } // transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account -func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { +func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { req := e.client.NewMarginTransferService() req.Asset(asset) req.Amount(amount.String()) - if io > 0 { // in + if io == types.TransferIn { req.Type(binance.MarginTransferTypeToMargin) - } else if io < 0 { // out + } else if io == types.TransferOut { req.Type(binance.MarginTransferTypeToMain) + } else { + return fmt.Errorf("unexpected transfer direction: %d given", io) } + resp, err := req.Do(ctx) if err != nil { return err From 161dc7dc647dcf3906cc2ac5e0566099089adec2 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 09:04:49 +0800 Subject: [PATCH 0632/1392] types: add transfer direction --- pkg/types/transfer.go | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 pkg/types/transfer.go diff --git a/pkg/types/transfer.go b/pkg/types/transfer.go new file mode 100644 index 0000000000..640ef9977d --- /dev/null +++ b/pkg/types/transfer.go @@ -0,0 +1,8 @@ +package types + +type TransferDirection int + +const ( + TransferIn TransferDirection = 1 + TransferOut TransferDirection = -1 +) From 2a927dc34d1c0fd2aa1fac37bebbc781ddb04b32 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 09:20:44 +0800 Subject: [PATCH 0633/1392] interact: reduce info logs --- pkg/interact/interact.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/interact/interact.go b/pkg/interact/interact.go index 820979cfe1..c9358b9ac2 100644 --- a/pkg/interact/interact.go +++ b/pkg/interact/interact.go @@ -247,9 +247,9 @@ func (it *Interact) Start(ctx context.Context) error { } for _, custom := range it.customInteractions { - log.Infof("checking %T custom interaction...", custom) + log.Debugf("checking %T custom interaction...", custom) if initializer, ok := custom.(Initializer); ok { - log.Infof("initializing %T custom interaction...", custom) + log.Debugf("initializing %T custom interaction...", custom) if err := initializer.Initialize(); err != nil { return err } From a838b4991aadecd6eadb694a70c81f6836a51cb2 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 12:51:52 +0800 Subject: [PATCH 0634/1392] bbgo: refactor order executor with max retries --- pkg/bbgo/order_execution.go | 39 ++++++++++++------------------ pkg/bbgo/order_executor_general.go | 14 ++++++++++- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 761dc74e17..4ac34b72a0 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -54,7 +54,7 @@ func (e *ExchangeOrderExecutionRouter) SubmitOrdersTo(ctx context.Context, sessi return nil, err } - createdOrders, _, err := BatchPlaceOrder(ctx, es.Exchange, formattedOrders...) + createdOrders, _, err := BatchPlaceOrder(ctx, es.Exchange, nil, formattedOrders...) return createdOrders, err } @@ -94,7 +94,7 @@ func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...type log.Infof("submitting order: %s", order.String()) } - createdOrders, _, err := BatchPlaceOrder(ctx, e.Session.Exchange, formattedOrders...) + createdOrders, _, err := BatchPlaceOrder(ctx, e.Session.Exchange, nil, formattedOrders...) return createdOrders, err } @@ -297,10 +297,13 @@ func (c *BasicRiskController) ProcessOrders(session *ExchangeSession, orders ... return outOrders, nil } +type OrderCallback func(order types.Order) + // BatchPlaceOrder -func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { +func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, orderCallback OrderCallback, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { var createdOrders types.OrderSlice var err error + var errIndexes []int for i, submitOrder := range submitOrders { createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) @@ -309,6 +312,11 @@ func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders errIndexes = append(errIndexes, i) } else if createdOrder != nil { createdOrder.Tag = submitOrder.Tag + + if orderCallback != nil { + orderCallback(*createdOrder) + } + createdOrders = append(createdOrders, *createdOrder) } } @@ -316,8 +324,6 @@ func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders return createdOrders, errIndexes, err } -type OrderCallback func(order types.Order) - // BatchRetryPlaceOrder places the orders and retries the failed orders func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, orderCallback OrderCallback, logger log.FieldLogger, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { if logger == nil { @@ -329,26 +335,12 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ // if the errIdx is nil, then we should iterate all the submit orders // allocate a variable for new error index - var errIdxNext []int if len(errIdx) == 0 { - for i, submitOrder := range submitOrders { - createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) - if err2 != nil { - werr = multierr.Append(werr, err2) - errIdxNext = append(errIdxNext, i) - } else if createdOrder != nil { - // if the order is successfully created, than we should copy the order tag - createdOrder.Tag = submitOrder.Tag - - if orderCallback != nil { - orderCallback(*createdOrder) - } - - createdOrders = append(createdOrders, *createdOrder) - } + var err2 error + createdOrders, errIdx, err2 = BatchPlaceOrder(ctx, exchange, orderCallback, submitOrders...) + if err2 != nil { + werr = multierr.Append(werr, err2) } - - errIdx = errIdxNext } timeoutCtx, cancelTimeout := context.WithTimeout(ctx, DefaultSubmitOrderRetryTimeout) @@ -359,6 +351,7 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ // set backoff max retries to 101 because https://ja.wikipedia.org/wiki/101%E5%9B%9E%E7%9B%AE%E3%81%AE%E3%83%97%E3%83%AD%E3%83%9D%E3%83%BC%E3%82%BA backoffMaxRetries := uint64(101) + var errIdxNext []int batchRetryOrder: for retryRound := 0; len(errIdx) > 0 && retryRound < 10; retryRound++ { // sleep for 200 millisecond between each retry diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 219955c15f..db97ae7e03 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -40,6 +40,7 @@ type GeneralOrderExecutor struct { marginBaseMaxBorrowable, marginQuoteMaxBorrowable fixedpoint.Value + maxRetries uint disableNotify bool closing int64 } @@ -73,6 +74,10 @@ func (e *GeneralOrderExecutor) DisableNotify() { e.disableNotify = true } +func (e *GeneralOrderExecutor) SetMaxRetries(maxRetries uint) { + e.maxRetries = maxRetries +} + func (e *GeneralOrderExecutor) startMarginAssetUpdater(ctx context.Context) { marginService, ok := e.session.Exchange.(types.MarginBorrowRepayService) if !ok { @@ -194,10 +199,12 @@ func (e *GeneralOrderExecutor) FastSubmitOrders(ctx context.Context, submitOrder if err != nil { return nil, err } - createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, formattedOrders...) + + createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, nil, formattedOrders...) if len(errIdx) > 0 { return nil, err } + if IsBackTesting { e.orderStore.Add(createdOrders...) e.activeMakerOrders.Add(createdOrders...) @@ -229,6 +236,11 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. e.tradeCollector.Process() } + if e.maxRetries == 0 { + createdOrders, _, err := BatchPlaceOrder(ctx, e.session.Exchange, orderCreateCallback, formattedOrders...) + return createdOrders, err + } + createdOrders, _, err := BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, e.logger, formattedOrders...) return createdOrders, err } From 20cd73e6ad92a97c1793a8f69c6a317632903224 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 12:58:10 +0800 Subject: [PATCH 0635/1392] xfunding: fix transfer and refactoring more methods --- config/xfunding.yaml | 30 ++- go.mod | 1 + go.sum | 1 + pkg/strategy/drift/strategy.go | 4 +- .../xfunding/positionaction_string.go | 25 ++ pkg/strategy/xfunding/strategy.go | 241 ++++++++++++++---- pkg/strategy/xgap/strategy.go | 2 +- pkg/util/backoff/generic.go | 18 ++ 8 files changed, 258 insertions(+), 64 deletions(-) create mode 100644 pkg/strategy/xfunding/positionaction_string.go create mode 100644 pkg/util/backoff/generic.go diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 9f7e7352b9..0714a91f97 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -12,20 +12,22 @@ notifications: sessions: binance: exchange: binance - envVarPrefix: binance + envVarPrefix: BINANCE + + binance_futures: + exchange: binance + envVarPrefix: BINANCE futures: true -exchangeStrategies: -- on: binance - funding: +crossExchangeStrategies: + +- xfunding: + spotSession: binance + futuresSession: binance_futures symbol: ETHUSDT - quantity: 0.0001 - fundingRate: - high: 0.01% - supportDetection: - - interval: 1m - movingAverageType: EMA - movingAverageIntervalWindow: - interval: 15m - window: 60 - minVolume: 8_000 + leverage: 1.0 + incrementalQuoteQuantity: 11 + quoteInvestment: 110 + shortFundingRate: + high: 0.000% + low: -0.01% diff --git a/go.mod b/go.mod index ee4b893c32..10a84c69f6 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b github.com/cenkalti/backoff/v4 v4.2.0 github.com/cheggaaa/pb/v3 v3.0.8 + github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 github.com/ethereum/go-ethereum v1.10.23 github.com/evanphx/json-patch/v5 v5.6.0 diff --git a/go.sum b/go.sum index 3e90668ad9..294026040a 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a h1:ym8P2+ZvUvVtpLzy8wFLLvdggUIU31mvldvxixQQI2o= github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 403ee1914e..b3519442ec 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -177,7 +177,7 @@ func (s *Strategy) SubmitOrder(ctx context.Context, submitOrder types.SubmitOrde if err != nil { return nil, err } - createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.Session.Exchange, formattedOrder) + createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.Session.Exchange, nil, formattedOrder) if len(errIdx) > 0 { return nil, err } @@ -539,7 +539,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter s.atr.PushK(kline) atr := s.atr.Last() - price := kline.Close //s.getLastPrice() + price := kline.Close // s.getLastPrice() pricef := price.Float64() lowf := math.Min(kline.Low.Float64(), pricef) highf := math.Max(kline.High.Float64(), pricef) diff --git a/pkg/strategy/xfunding/positionaction_string.go b/pkg/strategy/xfunding/positionaction_string.go new file mode 100644 index 0000000000..6aba4acf8a --- /dev/null +++ b/pkg/strategy/xfunding/positionaction_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type=PositionAction"; DO NOT EDIT. + +package xfunding + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[PositionNoOp-0] + _ = x[PositionOpening-1] + _ = x[PositionClosing-2] +} + +const _PositionAction_name = "PositionNoOpPositionOpeningPositionClosing" + +var _PositionAction_index = [...]uint8{0, 12, 27, 42} + +func (i PositionAction) String() string { + if i < 0 || i >= PositionAction(len(_PositionAction_index)-1) { + return "PositionAction(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _PositionAction_name[_PositionAction_index[i]:_PositionAction_index[i+1]] +} diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 859aea3336..4b61e55d9f 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -5,11 +5,13 @@ import ( "errors" "fmt" "strings" + "time" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/exchange/binance" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/util/backoff" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/types" @@ -17,6 +19,7 @@ import ( const ID = "xfunding" +//go:generate stringer -type=PositionAction type PositionAction int const ( @@ -34,13 +37,18 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +// Strategy is the xfunding fee strategy +// Right now it only supports short position in the USDT futures account. +// When opening the short position, it uses spot account to buy inventory, then transfer the inventory to the futures account as collateral assets. type Strategy struct { Environment *bbgo.Environment // These fields will be filled from the config file (it translates YAML to JSON) - Symbol string `json:"symbol"` - Market types.Market `json:"-"` - Quantity fixedpoint.Value `json:"quantity,omitempty"` + Symbol string `json:"symbol"` + Market types.Market `json:"-"` + + // Leverage is the leverage of the futures position + Leverage fixedpoint.Value `json:"leverage,omitempty"` // IncrementalQuoteQuantity is used for opening position incrementally with a small fixed quote quantity // for example, 100usdt per order @@ -129,6 +137,11 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } +func (s *Strategy) Defaults() error { + s.Leverage = fixedpoint.One + return nil +} + func (s *Strategy) Validate() error { if len(s.Symbol) == 0 { return errors.New("symbol is required") @@ -236,8 +249,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if s.positionType == types.PositionShort { switch s.positionAction { case PositionOpening: - if trade.Side != types.SideTypeSell { - log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade) + if trade.Side != types.SideTypeBuy { + log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) return } @@ -248,15 +261,20 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } // 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account - // balances, err := binanceSpot.QueryAccountBalances(ctx) + // TODO: handle missing trades here. If the process crashed during the transfer, how to recover? + if err := backoff.RetryGeneric(ctx, func() error { + return s.transferIn(ctx, binanceSpot, trade) + }); err != nil { + log.WithError(err).Errorf("transfer in retry failed") + return + } // 2) transferred successfully, sync futures position - - // 3) compare spot position and futures position, increase the position size until they are the same size + // compare spot position and futures position, increase the position size until they are the same size case PositionClosing: - if trade.Side != types.SideTypeBuy { - log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) + if trade.Side != types.SideTypeSell { + log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade) return } @@ -267,42 +285,167 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { - premiumIndex, err := binanceFutures.QueryPremiumIndex(ctx, s.Symbol) - if err != nil { - log.WithError(err).Error("premium index query error") - return - } - - s.detectPremiumIndex(premiumIndex) + // s.queryAndDetectPremiumIndex(ctx, binanceFutures) })) - s.spotSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { - // TODO: use go routine and time.Ticker - s.triggerPositionAction(ctx) - })) + go func() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + s.queryAndDetectPremiumIndex(ctx, binanceFutures) + + } + } + }() + + // TODO: use go routine and time.Ticker to trigger spot sync and futures sync + /* + s.spotSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { + })) + */ return nil } +func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFutures *binance.Exchange) { + premiumIndex, err := binanceFutures.QueryPremiumIndex(ctx, s.Symbol) + if err != nil { + log.WithError(err).Error("premium index query error") + return + } + + log.Infof("premiumIndex: %+v", premiumIndex) + + if changed := s.detectPremiumIndex(premiumIndex); changed { + log.Infof("position action: %s %s", s.positionType, s.positionAction.String()) + s.triggerPositionAction(ctx) + } +} + // TODO: replace type binance.Exchange with an interface func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade types.Trade) error { + currency := s.spotMarket.BaseCurrency + + // base asset needs BUY trades + if trade.Side == types.SideTypeSell { + return nil + } + balances, err := ex.QueryAccountBalances(ctx) if err != nil { return err } - b, ok := balances[s.spotMarket.BaseCurrency] + b, ok := balances[currency] if !ok { - return nil + return fmt.Errorf("%s balance not found", currency) } - // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here if b.Available.Compare(trade.Quantity) >= 0 { - + log.Infof("transfering futures account asset %s %s", trade.Quantity, currency) + if err := ex.TransferFuturesAccountAsset(ctx, currency, trade.Quantity, types.TransferIn); err != nil { + log.WithError(err).Errorf("spot-to-futures transfer error") + return err + } } + return nil } +func (s *Strategy) triggerPositionAction(ctx context.Context) { + switch s.positionAction { + case PositionOpening: + s.syncSpotPosition(ctx) + s.syncFuturesPosition(ctx) + case PositionClosing: + s.syncFuturesPosition(ctx) + s.syncSpotPosition(ctx) + } +} + +func (s *Strategy) syncFuturesPosition(ctx context.Context) { + _ = s.futuresOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.futuresSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + + switch s.positionAction { + + case PositionClosing: + + case PositionOpening: + + if s.positionType != types.PositionShort { + return + } + + spotBase := s.SpotPosition.GetBase() // should be positive base quantity here + futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here + + if spotBase.IsZero() { + // skip when spot base is zero + return + } + + log.Infof("position comparision: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + + if futuresBase.Sign() > 0 { + // unexpected error + log.Errorf("unexpected futures position (got positive, expecting negative)") + return + } + + // compare with the spot position and increase the position + quoteValue, err := bbgo.CalculateQuoteQuantity(ctx, s.futuresSession, s.futuresMarket.QuoteCurrency, s.Leverage) + if err != nil { + log.WithError(err).Errorf("can not calculate futures account quote value") + return + } + log.Infof("calculated futures account quote value = %s", quoteValue.String()) + + if spotBase.Sign() > 0 && futuresBase.Neg().Compare(spotBase) < 0 { + orderPrice := ticker.Sell + diffQuantity := spotBase.Sub(futuresBase.Neg().Mul(s.Leverage)) + + log.Infof("position diff quantity: %s", diffQuantity.String()) + + orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) + orderQuantity = bbgo.AdjustQuantityByMinAmount(orderQuantity, orderPrice, s.futuresMarket.MinNotional) + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) + return + } + + createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.futuresMarket, + // TimeInForce: types.TimeInForceGTC, + }) + + if err != nil { + log.WithError(err).Errorf("can not submit order") + return + } + + log.Infof("created orders: %+v", createdOrders) + } + } +} + func (s *Strategy) syncSpotPosition(ctx context.Context) { ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { @@ -318,26 +461,32 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { switch s.positionAction { case PositionClosing: + // TODO: compare with the futures position and reduce the position case PositionOpening: - if s.usedQuoteInvestment.IsZero() || s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - // stop + if s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { return } leftQuote := s.QuoteInvestment.Sub(s.usedQuoteInvestment) - orderPrice := ticker.Sell + orderPrice := ticker.Buy orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuote).Div(orderPrice) orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) - createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Quantity: orderQuantity, - Price: orderPrice, - Market: s.spotMarket, - TimeInForce: types.TimeInForceGTC, - }) + + _ = s.spotOrderExecutor.GracefulCancel(ctx) + + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.spotMarket, + } + + log.Infof("placing spot order: %+v", submitOrder) + + createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, submitOrder) if err != nil { log.WithError(err).Errorf("can not submit order") return @@ -347,30 +496,28 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { } } -func (s *Strategy) triggerPositionAction(ctx context.Context) { - switch s.positionAction { - case PositionOpening: - s.syncSpotPosition(ctx) - - case PositionClosing: +func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed bool) { + fundingRate := premiumIndex.LastFundingRate - } -} + log.Infof("last %s funding rate: %s", s.Symbol, fundingRate.Percentage()) -func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) { - fundingRate := premiumIndex.LastFundingRate if s.ShortFundingRate != nil { if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { s.positionAction = PositionOpening s.positionType = types.PositionShort + changed = true } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { s.positionAction = PositionClosing + changed = true } } + + return changed } func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position) + orderExecutor.SetMaxRetries(0) orderExecutor.BindEnvironment(s.Environment) orderExecutor.Bind() orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { diff --git a/pkg/strategy/xgap/strategy.go b/pkg/strategy/xgap/strategy.go index fd0b67f604..efa8b6445b 100644 --- a/pkg/strategy/xgap/strategy.go +++ b/pkg/strategy/xgap/strategy.go @@ -331,7 +331,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.tradingMarket.MinNotional.Mul(NotionModifier).Div(price)) } - createdOrders, _, err := bbgo.BatchPlaceOrder(ctx, tradingSession.Exchange, types.SubmitOrder{ + createdOrders, _, err := bbgo.BatchPlaceOrder(ctx, tradingSession.Exchange, nil, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimit, diff --git a/pkg/util/backoff/generic.go b/pkg/util/backoff/generic.go new file mode 100644 index 0000000000..f7303d3368 --- /dev/null +++ b/pkg/util/backoff/generic.go @@ -0,0 +1,18 @@ +package backoff + +import ( + "context" + + "github.com/cenkalti/backoff/v4" +) + +var MaxRetries uint64 = 101 + +func RetryGeneric(ctx context.Context, op backoff.Operation) (err error) { + err = backoff.Retry(op, backoff.WithContext( + backoff.WithMaxRetries( + backoff.NewExponentialBackOff(), + MaxRetries), + ctx)) + return err +} From 80c30d15a0bac5e98eaecb86e49541ab261ecddd Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 13:02:22 +0800 Subject: [PATCH 0636/1392] xfunding: correct method names --- pkg/strategy/xfunding/strategy.go | 108 ++++++++++++++++-------------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 4b61e55d9f..2da6c934cd 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -362,14 +362,17 @@ func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade t func (s *Strategy) triggerPositionAction(ctx context.Context) { switch s.positionAction { case PositionOpening: - s.syncSpotPosition(ctx) + s.increaseSpotPosition(ctx) s.syncFuturesPosition(ctx) case PositionClosing: - s.syncFuturesPosition(ctx) + s.reduceFuturesPosition(ctx) s.syncSpotPosition(ctx) } } +func (s *Strategy) reduceFuturesPosition(ctx context.Context) {} + +// syncFuturesPosition syncs the futures position with the given spot position func (s *Strategy) syncFuturesPosition(ctx context.Context) { _ = s.futuresOrderExecutor.GracefulCancel(ctx) @@ -380,73 +383,76 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { } switch s.positionAction { - case PositionClosing: + return + case PositionOpening, PositionNoOp: + } - case PositionOpening: + if s.positionType != types.PositionShort { + return + } - if s.positionType != types.PositionShort { - return - } + spotBase := s.SpotPosition.GetBase() // should be positive base quantity here + futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here - spotBase := s.SpotPosition.GetBase() // should be positive base quantity here - futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here + if spotBase.IsZero() { + // skip when spot base is zero + return + } - if spotBase.IsZero() { - // skip when spot base is zero - return - } + log.Infof("position comparision: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + + if futuresBase.Sign() > 0 { + // unexpected error + log.Errorf("unexpected futures position (got positive, expecting negative)") + return + } + + // compare with the spot position and increase the position + quoteValue, err := bbgo.CalculateQuoteQuantity(ctx, s.futuresSession, s.futuresMarket.QuoteCurrency, s.Leverage) + if err != nil { + log.WithError(err).Errorf("can not calculate futures account quote value") + return + } + log.Infof("calculated futures account quote value = %s", quoteValue.String()) - log.Infof("position comparision: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + if spotBase.Sign() > 0 && futuresBase.Neg().Compare(spotBase) < 0 { + orderPrice := ticker.Sell + diffQuantity := spotBase.Sub(futuresBase.Neg().Mul(s.Leverage)) - if futuresBase.Sign() > 0 { - // unexpected error - log.Errorf("unexpected futures position (got positive, expecting negative)") + log.Infof("position diff quantity: %s", diffQuantity.String()) + + orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) + orderQuantity = bbgo.AdjustQuantityByMinAmount(orderQuantity, orderPrice, s.futuresMarket.MinNotional) + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) return } - // compare with the spot position and increase the position - quoteValue, err := bbgo.CalculateQuoteQuantity(ctx, s.futuresSession, s.futuresMarket.QuoteCurrency, s.Leverage) + createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.futuresMarket, + // TimeInForce: types.TimeInForceGTC, + }) + if err != nil { - log.WithError(err).Errorf("can not calculate futures account quote value") + log.WithError(err).Errorf("can not submit order") return } - log.Infof("calculated futures account quote value = %s", quoteValue.String()) - - if spotBase.Sign() > 0 && futuresBase.Neg().Compare(spotBase) < 0 { - orderPrice := ticker.Sell - diffQuantity := spotBase.Sub(futuresBase.Neg().Mul(s.Leverage)) - log.Infof("position diff quantity: %s", diffQuantity.String()) - - orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) - orderQuantity = bbgo.AdjustQuantityByMinAmount(orderQuantity, orderPrice, s.futuresMarket.MinNotional) - if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { - log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) - return - } - - createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Quantity: orderQuantity, - Price: orderPrice, - Market: s.futuresMarket, - // TimeInForce: types.TimeInForceGTC, - }) - - if err != nil { - log.WithError(err).Errorf("can not submit order") - return - } - - log.Infof("created orders: %+v", createdOrders) - } + log.Infof("created orders: %+v", createdOrders) } } func (s *Strategy) syncSpotPosition(ctx context.Context) { + +} + +func (s *Strategy) increaseSpotPosition(ctx context.Context) { ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { log.WithError(err).Errorf("can not query ticker") From 16608619cac4bb48af12e525b25ccce49d357b29 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 13:07:54 +0800 Subject: [PATCH 0637/1392] xfunding: fix sync guard --- pkg/strategy/xfunding/strategy.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 2da6c934cd..7f07d6bdf7 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -374,11 +374,7 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) {} // syncFuturesPosition syncs the futures position with the given spot position func (s *Strategy) syncFuturesPosition(ctx context.Context) { - _ = s.futuresOrderExecutor.GracefulCancel(ctx) - - ticker, err := s.futuresSession.Exchange.QueryTicker(ctx, s.Symbol) - if err != nil { - log.WithError(err).Errorf("can not query ticker") + if s.positionType != types.PositionShort { return } @@ -388,7 +384,11 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { case PositionOpening, PositionNoOp: } - if s.positionType != types.PositionShort { + _ = s.futuresOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.futuresSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") return } From b7edc38dc795f40941a8f3c82c1a5678cc0d3040 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 13:14:59 +0800 Subject: [PATCH 0638/1392] xfunding: record pending transfer --- pkg/strategy/xfunding/strategy.go | 56 +++++++++++++++++++------------ 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 7f07d6bdf7..b0654e79ee 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -85,27 +85,30 @@ type Strategy struct { MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` } `json:"supportDetection"` - ProfitStats *types.ProfitStats `persistence:"profit_stats"` + SpotSession string `json:"spotSession"` + FuturesSession string `json:"futuresSession"` - SpotPosition *types.Position `persistence:"spot_position"` - FuturesPosition *types.Position `persistence:"futures_position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + SpotPosition *types.Position `persistence:"spot_position"` + FuturesPosition *types.Position `persistence:"futures_position"` - spotSession, futuresSession *bbgo.ExchangeSession + State *State `persistence:"state"` + spotSession, futuresSession *bbgo.ExchangeSession spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor spotMarket, futuresMarket types.Market - SpotSession string `json:"spotSession"` - FuturesSession string `json:"futuresSession"` - // positionAction is default to NoOp positionAction PositionAction // positionType is the futures position type // currently we only support short position for the positive funding rate positionType types.PositionType +} - usedQuoteInvestment fixedpoint.Value +type State struct { + PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` + UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` } func (s *Strategy) ID() string { @@ -198,8 +201,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { instanceID := s.InstanceID() - s.usedQuoteInvestment = fixedpoint.Zero - s.spotSession = sessions[s.SpotSession] s.futuresSession = sessions[s.FuturesSession] @@ -234,6 +235,13 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) } + if s.State == nil { + s.State = &State{ + PendingBaseTransfer: fixedpoint.Zero, + UsedQuoteInvestment: fixedpoint.Zero, + } + } + binanceFutures := s.futuresSession.Exchange.(*binance.Exchange) binanceSpot := s.spotSession.Exchange.(*binance.Exchange) _ = binanceSpot @@ -255,8 +263,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } // TODO: add mutex lock for this modification - s.usedQuoteInvestment = s.usedQuoteInvestment.Add(trade.QuoteQuantity) - if s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) + if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { s.positionAction = PositionNoOp } @@ -265,7 +273,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if err := backoff.RetryGeneric(ctx, func() error { return s.transferIn(ctx, binanceSpot, trade) }); err != nil { - log.WithError(err).Errorf("transfer in retry failed") + log.WithError(err).Errorf("spot-to-futures transfer in retry failed") return } @@ -348,14 +356,20 @@ func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade t } // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(trade.Quantity) >= 0 { - log.Infof("transfering futures account asset %s %s", trade.Quantity, currency) - if err := ex.TransferFuturesAccountAsset(ctx, currency, trade.Quantity, types.TransferIn); err != nil { - log.WithError(err).Errorf("spot-to-futures transfer error") - return err - } + if b.Available.Compare(trade.Quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) + return nil + } + + amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + + log.Infof("transfering futures account asset %s %s", amount, currency) + if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferIn); err != nil { + return err } + s.State.PendingBaseTransfer = fixedpoint.Zero return nil } @@ -470,11 +484,11 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { // TODO: compare with the futures position and reduce the position case PositionOpening: - if s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { return } - leftQuote := s.QuoteInvestment.Sub(s.usedQuoteInvestment) + leftQuote := s.QuoteInvestment.Sub(s.State.UsedQuoteInvestment) orderPrice := ticker.Buy orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuote).Div(orderPrice) orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) From 8b87a8706b595bbdfb4011c467d728aa20cdb4d3 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 14:46:02 +0800 Subject: [PATCH 0639/1392] xfunding: add state for recording TotalBaseTransfer --- config/grid2-max.yaml | 3 ++- pkg/strategy/xfunding/strategy.go | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 596d0069f5..3c55294d8e 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -11,7 +11,8 @@ notifications: sessions: max: exchange: max - envVarPrefix: max + envVarPrefix: MAX + # example command: # godotenv -f .env.local -- go run ./cmd/bbgo backtest --config config/grid2-max.yaml --base-asset-baseline diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index b0654e79ee..977b2bdd1b 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -108,6 +108,7 @@ type Strategy struct { type State struct { PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` + TotalBaseTransfer fixedpoint.Value `json:"totalBaseTransfer"` UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` } @@ -238,6 +239,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if s.State == nil { s.State = &State{ PendingBaseTransfer: fixedpoint.Zero, + TotalBaseTransfer: fixedpoint.Zero, UsedQuoteInvestment: fixedpoint.Zero, } } @@ -364,12 +366,25 @@ func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade t amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + pos := s.SpotPosition.GetBase() + rest := pos.Sub(s.State.TotalBaseTransfer) + + if rest.Sign() < 0 { + return nil + } + + amount = fixedpoint.Min(rest, amount) + log.Infof("transfering futures account asset %s %s", amount, currency) if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferIn); err != nil { return err } + // reset pending transfer s.State.PendingBaseTransfer = fixedpoint.Zero + + // record the transfer in the total base transfer + s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Add(amount) return nil } From 44850e48e88a10e3db8b7c7fec37e644e9c763b3 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 14:48:24 +0800 Subject: [PATCH 0640/1392] xfunding: add mutex protection --- pkg/strategy/xfunding/strategy.go | 62 ++++++++++++++++--------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 977b2bdd1b..3be8fefdc4 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "sync" "time" "github.com/sirupsen/logrus" @@ -93,6 +94,7 @@ type Strategy struct { FuturesPosition *types.Position `persistence:"futures_position"` State *State `persistence:"state"` + mu sync.Mutex spotSession, futuresSession *bbgo.ExchangeSession spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor @@ -264,7 +266,9 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return } - // TODO: add mutex lock for this modification + s.mu.Lock() + defer s.mu.Unlock() + s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { s.positionAction = PositionNoOp @@ -492,43 +496,43 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { log.Errorf("funding long position type is not supported") return } + if s.positionAction != PositionOpening { + return + } - switch s.positionAction { + s.mu.Lock() + defer s.mu.Unlock() - case PositionClosing: - // TODO: compare with the futures position and reduce the position + if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + return + } - case PositionOpening: - if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - return - } + leftQuota := s.QuoteInvestment.Sub(s.State.UsedQuoteInvestment) - leftQuote := s.QuoteInvestment.Sub(s.State.UsedQuoteInvestment) - orderPrice := ticker.Buy - orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuote).Div(orderPrice) - orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) + orderPrice := ticker.Buy + orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuota).Div(orderPrice) + orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) - _ = s.spotOrderExecutor.GracefulCancel(ctx) + _ = s.spotOrderExecutor.GracefulCancel(ctx) - submitOrder := types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimitMaker, - Quantity: orderQuantity, - Price: orderPrice, - Market: s.spotMarket, - } - - log.Infof("placing spot order: %+v", submitOrder) + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.spotMarket, + } - createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, submitOrder) - if err != nil { - log.WithError(err).Errorf("can not submit order") - return - } + log.Infof("placing spot order: %+v", submitOrder) - log.Infof("created orders: %+v", createdOrders) + createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, submitOrder) + if err != nil { + log.WithError(err).Errorf("can not submit order") + return } + + log.Infof("created orders: %+v", createdOrders) } func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed bool) { From 018e281627800890401cc3cb7ee4006d4cff0913 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 16:14:30 +0800 Subject: [PATCH 0641/1392] types: add AdjustQuantityByMinNotional to types.Market --- pkg/types/market.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/types/market.go b/pkg/types/market.go index b7a44db924..6417cf79a8 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -136,6 +136,18 @@ func (m Market) CanonicalizeVolume(val fixedpoint.Value) float64 { return math.Trunc(p*val.Float64()) / p } +// AdjustQuantityByMinNotional adjusts the quantity to make the amount greater than the given minAmount +func (m Market) AdjustQuantityByMinNotional(quantity, currentPrice fixedpoint.Value) fixedpoint.Value { + // modify quantity for the min amount + amount := currentPrice.Mul(quantity) + if amount.Compare(m.MinNotional) < 0 { + ratio := m.MinNotional.Div(amount) + return quantity.Mul(ratio) + } + + return quantity +} + type MarketMap map[string]Market func (m MarketMap) Add(market Market) { From 32c617a2836be518fe8f03ce949a79a85bb36eb4 Mon Sep 17 00:00:00 2001 From: narumi Date: Thu, 23 Mar 2023 16:47:48 +0800 Subject: [PATCH 0642/1392] replenish on start --- pkg/strategy/fixedmaker/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/fixedmaker/strategy.go b/pkg/strategy/fixedmaker/strategy.go index 1f4bf1d24e..d316334062 100644 --- a/pkg/strategy/fixedmaker/strategy.go +++ b/pkg/strategy/fixedmaker/strategy.go @@ -135,6 +135,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. session.UserDataStream.OnStart(func() { // you can place orders here when bbgo is started, this will be called only once. + s.replenish(ctx) }) s.activeOrderBook.OnFilled(func(order types.Order) { From c3ca5b75acd4fa9913d815df5c6c491e4978667c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 16:47:57 +0800 Subject: [PATCH 0643/1392] types: add minNotionalSealant to adjust quantity method --- pkg/types/market.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/types/market.go b/pkg/types/market.go index 6417cf79a8..87eaa6301d 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -136,12 +136,14 @@ func (m Market) CanonicalizeVolume(val fixedpoint.Value) float64 { return math.Trunc(p*val.Float64()) / p } +var minNotionalSealant = fixedpoint.NewFromFloat(1.0001) + // AdjustQuantityByMinNotional adjusts the quantity to make the amount greater than the given minAmount func (m Market) AdjustQuantityByMinNotional(quantity, currentPrice fixedpoint.Value) fixedpoint.Value { // modify quantity for the min amount amount := currentPrice.Mul(quantity) if amount.Compare(m.MinNotional) < 0 { - ratio := m.MinNotional.Div(amount) + ratio := m.MinNotional.Mul(minNotionalSealant).Div(amount) return quantity.Mul(ratio) } From 02c28a07cc3cd7aac3b300a61c2da49f3d259409 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 17:35:54 +0800 Subject: [PATCH 0644/1392] types: fix AdjustQuantityByMinNotional by round up the quantity --- pkg/types/market.go | 21 +++++++++++++---- pkg/types/market_test.go | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/pkg/types/market.go b/pkg/types/market.go index 87eaa6301d..99b2f9efbd 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -2,6 +2,7 @@ package types import ( "math" + "strconv" "github.com/leekchan/accounting" @@ -59,7 +60,14 @@ func (m Market) IsDustQuantity(quantity, price fixedpoint.Value) bool { // TruncateQuantity uses the step size to truncate floating number, in order to avoid the rounding issue func (m Market) TruncateQuantity(quantity fixedpoint.Value) fixedpoint.Value { - return fixedpoint.MustNewFromString(m.FormatQuantity(quantity)) + var ts = m.StepSize.Float64() + var prec = int(math.Round(math.Log10(ts) * -1.0)) + var pow10 = math.Pow10(prec) + + qf := math.Trunc(quantity.Float64() * pow10) + qf = qf / pow10 + qs := strconv.FormatFloat(qf, 'f', prec, 64) + return fixedpoint.MustNewFromString(qs) } func (m Market) TruncatePrice(price fixedpoint.Value) fixedpoint.Value { @@ -136,15 +144,18 @@ func (m Market) CanonicalizeVolume(val fixedpoint.Value) float64 { return math.Trunc(p*val.Float64()) / p } -var minNotionalSealant = fixedpoint.NewFromFloat(1.0001) - // AdjustQuantityByMinNotional adjusts the quantity to make the amount greater than the given minAmount func (m Market) AdjustQuantityByMinNotional(quantity, currentPrice fixedpoint.Value) fixedpoint.Value { // modify quantity for the min amount + quantity = m.TruncateQuantity(quantity) amount := currentPrice.Mul(quantity) if amount.Compare(m.MinNotional) < 0 { - ratio := m.MinNotional.Mul(minNotionalSealant).Div(amount) - return quantity.Mul(ratio) + ratio := m.MinNotional.Div(amount) + quantity = quantity.Mul(ratio) + + ts := m.StepSize.Float64() + prec := int(math.Round(math.Log10(ts) * -1.0)) + return quantity.Round(prec, fixedpoint.Up) } return quantity diff --git a/pkg/types/market_test.go b/pkg/types/market_test.go index 809e60b0d4..493c3a21d9 100644 --- a/pkg/types/market_test.go +++ b/pkg/types/market_test.go @@ -191,3 +191,53 @@ func Test_formatQuantity(t *testing.T) { }) } } + +func TestMarket_TruncateQuantity(t *testing.T) { + market := Market{ + StepSize: fixedpoint.NewFromFloat(0.0001), + } + + testCases := []struct { + input string + expect string + }{ + {"0.00573961", "0.0057"}, + {"0.00579961", "0.0057"}, + {"0.0057", "0.0057"}, + } + + for _, testCase := range testCases { + q := fixedpoint.MustNewFromString(testCase.input) + q2 := market.TruncateQuantity(q) + assert.Equalf(t, testCase.expect, q2.String(), "input: %s stepSize: %s", testCase.input, market.StepSize.String()) + } + +} + +func TestMarket_AdjustQuantityByMinNotional(t *testing.T) { + + market := Market{ + Symbol: "ETHUSDT", + StepSize: fixedpoint.NewFromFloat(0.0001), + MinQuantity: fixedpoint.NewFromFloat(0.0001), + MinNotional: fixedpoint.NewFromFloat(10.0), + VolumePrecision: 8, + PricePrecision: 2, + } + + // Quantity:0.00573961 Price:1750.99 + testCases := []struct { + input string + expect string + }{ + {"0.00573961", "0.0058"}, + } + + price := fixedpoint.NewFromFloat(1750.99) + for _, testCase := range testCases { + q := fixedpoint.MustNewFromString(testCase.input) + q2 := market.AdjustQuantityByMinNotional(q, price) + assert.Equalf(t, testCase.expect, q2.String(), "input: %s stepSize: %s", testCase.input, market.StepSize.String()) + assert.False(t, market.IsDustQuantity(q2, price)) + } +} From 1b5126c9a1d6ff858f66c40e64c74fcfbd874644 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 17:36:30 +0800 Subject: [PATCH 0645/1392] xfunding: add mutex --- config/xfunding.yaml | 6 ++++ pkg/bbgo/order_processor.go | 2 +- pkg/strategy/xfunding/strategy.go | 52 ++++++++++++++++++++++++------- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 0714a91f97..b39712659d 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -9,6 +9,12 @@ notifications: orderUpdate: true submitOrder: true +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 1 + sessions: binance: exchange: binance diff --git a/pkg/bbgo/order_processor.go b/pkg/bbgo/order_processor.go index edf3844f17..0a5a06d7ae 100644 --- a/pkg/bbgo/order_processor.go +++ b/pkg/bbgo/order_processor.go @@ -33,7 +33,7 @@ func AdjustQuantityByMinAmount(quantity, currentPrice, minAmount fixedpoint.Valu amount := currentPrice.Mul(quantity) if amount.Compare(minAmount) < 0 { ratio := minAmount.Div(amount) - quantity = quantity.Mul(ratio) + return quantity.Mul(ratio) } return quantity diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 3be8fefdc4..1f8163d1b5 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -94,7 +94,9 @@ type Strategy struct { FuturesPosition *types.Position `persistence:"futures_position"` State *State `persistence:"state"` - mu sync.Mutex + + // mu is used for locking state + mu sync.Mutex spotSession, futuresSession *bbgo.ExchangeSession spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor @@ -246,6 +248,9 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } } + log.Infof("loaded spot position: %s", s.SpotPosition.String()) + log.Infof("loaded futures position: %s", s.FuturesPosition.String()) + binanceFutures := s.futuresSession.Exchange.(*binance.Exchange) binanceSpot := s.spotSession.Exchange.(*binance.Exchange) _ = binanceSpot @@ -428,7 +433,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { spotBase := s.SpotPosition.GetBase() // should be positive base quantity here futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here - if spotBase.IsZero() { + if spotBase.IsZero() || spotBase.Sign() < 0 { // skip when spot base is zero return } @@ -449,14 +454,25 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { } log.Infof("calculated futures account quote value = %s", quoteValue.String()) - if spotBase.Sign() > 0 && futuresBase.Neg().Compare(spotBase) < 0 { + // max futures base position (without negative sign) + maxFuturesBasePosition := fixedpoint.Min( + spotBase.Mul(s.Leverage), + s.State.TotalBaseTransfer.Mul(s.Leverage)) + + // if - futures position < max futures position, increase it + if futuresBase.Neg().Compare(maxFuturesBasePosition) < 0 { orderPrice := ticker.Sell - diffQuantity := spotBase.Sub(futuresBase.Neg().Mul(s.Leverage)) + diffQuantity := maxFuturesBasePosition.Sub(futuresBase.Neg()) + + if diffQuantity.Sign() < 0 { + log.Errorf("unexpected negative position diff: %s", diffQuantity.String()) + return + } log.Infof("position diff quantity: %s", diffQuantity.String()) orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) - orderQuantity = bbgo.AdjustQuantityByMinAmount(orderQuantity, orderPrice, s.futuresMarket.MinNotional) + orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) return @@ -486,12 +502,6 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { } func (s *Strategy) increaseSpotPosition(ctx context.Context) { - ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) - if err != nil { - log.WithError(err).Errorf("can not query ticker") - return - } - if s.positionType != types.PositionShort { log.Errorf("funding long position type is not supported") return @@ -507,13 +517,27 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { return } + _ = s.spotOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + leftQuota := s.QuoteInvestment.Sub(s.State.UsedQuoteInvestment) orderPrice := ticker.Buy orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuota).Div(orderPrice) + + log.Infof("initial spot order quantity %s", orderQuantity.String()) + orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) + orderQuantity = s.spotMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) - _ = s.spotOrderExecutor.GracefulCancel(ctx) + if s.spotMarket.IsDustQuantity(orderQuantity, orderPrice) { + return + } submitOrder := types.SubmitOrder{ Symbol: s.Symbol, @@ -542,6 +566,10 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed if s.ShortFundingRate != nil { if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + + log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", + fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) + s.positionAction = PositionOpening s.positionType = types.PositionShort changed = true From 7ba7eb8be7315e20288a903a345e031fde9c811d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 18:09:16 +0800 Subject: [PATCH 0646/1392] xfunding: implement reduceFuturesPosition --- pkg/strategy/xfunding/strategy.go | 66 +++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 1f8163d1b5..b4f93456ac 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -408,18 +408,18 @@ func (s *Strategy) triggerPositionAction(ctx context.Context) { } } -func (s *Strategy) reduceFuturesPosition(ctx context.Context) {} - -// syncFuturesPosition syncs the futures position with the given spot position -func (s *Strategy) syncFuturesPosition(ctx context.Context) { - if s.positionType != types.PositionShort { +func (s *Strategy) reduceFuturesPosition(ctx context.Context) { + switch s.positionAction { + case PositionOpening, PositionNoOp: return } - switch s.positionAction { - case PositionClosing: + futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here + + if futuresBase.Sign() > 0 { + // unexpected error + log.Errorf("unexpected futures position (got positive, expecting negative)") return - case PositionOpening, PositionNoOp: } _ = s.futuresOrderExecutor.GracefulCancel(ctx) @@ -430,6 +430,48 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } + if futuresBase.Compare(fixedpoint.Zero) < 0 { + orderPrice := ticker.Sell + + orderQuantity := futuresBase.Abs() + orderQuantity = fixedpoint.Max(orderQuantity, s.futuresMarket.MinQuantity) + orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) + return + } + + createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.futuresMarket, + ReduceOnly: true, + }) + + if err != nil { + log.WithError(err).Errorf("can not submit order") + return + } + + log.Infof("created orders: %+v", createdOrders) + } +} + +// syncFuturesPosition syncs the futures position with the given spot position +func (s *Strategy) syncFuturesPosition(ctx context.Context) { + if s.positionType != types.PositionShort { + return + } + + switch s.positionAction { + case PositionClosing: + return + case PositionOpening, PositionNoOp: + } + spotBase := s.SpotPosition.GetBase() // should be positive base quantity here futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here @@ -446,6 +488,14 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } + _ = s.futuresOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.futuresSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + // compare with the spot position and increase the position quoteValue, err := bbgo.CalculateQuoteQuantity(ctx, s.futuresSession, s.futuresMarket.QuoteCurrency, s.Leverage) if err != nil { From b5f69e7f45d25b7553609f7c4dbbd6477658919b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 18:18:30 +0800 Subject: [PATCH 0647/1392] xfunding: reset stats when direction changed --- pkg/strategy/xfunding/strategy.go | 58 +++------------- pkg/strategy/xfunding/transfer.go | 112 ++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 50 deletions(-) create mode 100644 pkg/strategy/xfunding/transfer.go diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index b4f93456ac..c1ee02dedf 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -347,56 +347,6 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture } } -// TODO: replace type binance.Exchange with an interface -func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade types.Trade) error { - currency := s.spotMarket.BaseCurrency - - // base asset needs BUY trades - if trade.Side == types.SideTypeSell { - return nil - } - - balances, err := ex.QueryAccountBalances(ctx) - if err != nil { - return err - } - - b, ok := balances[currency] - if !ok { - return fmt.Errorf("%s balance not found", currency) - } - - // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(trade.Quantity) < 0 { - log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) - s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) - return nil - } - - amount := s.State.PendingBaseTransfer.Add(trade.Quantity) - - pos := s.SpotPosition.GetBase() - rest := pos.Sub(s.State.TotalBaseTransfer) - - if rest.Sign() < 0 { - return nil - } - - amount = fixedpoint.Min(rest, amount) - - log.Infof("transfering futures account asset %s %s", amount, currency) - if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferIn); err != nil { - return err - } - - // reset pending transfer - s.State.PendingBaseTransfer = fixedpoint.Zero - - // record the transfer in the total base transfer - s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Add(amount) - return nil -} - func (s *Strategy) triggerPositionAction(ctx context.Context) { switch s.positionAction { case PositionOpening: @@ -622,9 +572,17 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed s.positionAction = PositionOpening s.positionType = types.PositionShort + + // reset the transfer stats + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero changed = true } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { s.positionAction = PositionClosing + + // reset the transfer stats + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero changed = true } } diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go new file mode 100644 index 0000000000..00a34afdc7 --- /dev/null +++ b/pkg/strategy/xfunding/transfer.go @@ -0,0 +1,112 @@ +package xfunding + +import ( + "context" + "fmt" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type FuturesTransfer interface { + TransferFuturesAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error + QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) +} + +func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade types.Trade) error { + currency := s.spotMarket.BaseCurrency + + // base asset needs BUY trades + if trade.Side == types.SideTypeBuy { + return nil + } + + balances, err := ex.QueryAccountBalances(ctx) + if err != nil { + return err + } + + b, ok := balances[currency] + if !ok { + return fmt.Errorf("%s balance not found", currency) + } + + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here + if b.Available.Compare(trade.Quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) + return nil + } + + amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + + pos := s.SpotPosition.GetBase() + rest := pos.Sub(s.State.TotalBaseTransfer) + + if rest.Sign() < 0 { + return nil + } + + amount = fixedpoint.Min(rest, amount) + + log.Infof("transfering out futures account asset %s %s", amount, currency) + if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferOut); err != nil { + return err + } + + // reset pending transfer + s.State.PendingBaseTransfer = fixedpoint.Zero + + // record the transfer in the total base transfer + s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Add(amount) + return nil +} + +func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, trade types.Trade) error { + currency := s.spotMarket.BaseCurrency + + // base asset needs BUY trades + if trade.Side == types.SideTypeSell { + return nil + } + + balances, err := ex.QueryAccountBalances(ctx) + if err != nil { + return err + } + + b, ok := balances[currency] + if !ok { + return fmt.Errorf("%s balance not found", currency) + } + + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here + if b.Available.Compare(trade.Quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) + return nil + } + + amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + + pos := s.SpotPosition.GetBase() + rest := pos.Sub(s.State.TotalBaseTransfer) + + if rest.Sign() < 0 { + return nil + } + + amount = fixedpoint.Min(rest, amount) + + log.Infof("transfering in futures account asset %s %s", amount, currency) + if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferIn); err != nil { + return err + } + + // reset pending transfer + s.State.PendingBaseTransfer = fixedpoint.Zero + + // record the transfer in the total base transfer + s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Add(amount) + return nil +} From a933f90cc877ae55a188872570929f8ae2a79d4d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 18:19:30 +0800 Subject: [PATCH 0648/1392] xfunding: log low funding fee --- pkg/strategy/xfunding/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index c1ee02dedf..3694139261 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -578,6 +578,10 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed s.State.TotalBaseTransfer = fixedpoint.Zero changed = true } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + + log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", + fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) + s.positionAction = PositionClosing // reset the transfer stats From aba80398d924e56294a2763b1c8492798418ff7a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 22:36:35 +0800 Subject: [PATCH 0649/1392] xfunding: add MinHoldingPeriod support --- pkg/strategy/xfunding/strategy.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 3694139261..c8d2846763 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -57,6 +57,8 @@ type Strategy struct { QuoteInvestment fixedpoint.Value `json:"quoteInvestment"` + MinHoldingPeriod types.Duration `json:"minHoldingPeriod"` + // ShortFundingRate is the funding rate range for short positions // TODO: right now we don't support negative funding rate (long position) since it's rarer ShortFundingRate *struct { @@ -111,6 +113,7 @@ type Strategy struct { } type State struct { + PositionStartTime time.Time `json:"positionStartTime"` PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` TotalBaseTransfer fixedpoint.Value `json:"totalBaseTransfer"` UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` @@ -146,7 +149,14 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } func (s *Strategy) Defaults() error { - s.Leverage = fixedpoint.One + if s.Leverage.IsZero() { + s.Leverage = fixedpoint.One + } + + if s.MinHoldingPeriod == 0 { + s.MinHoldingPeriod = types.Duration(3 * 24 * time.Hour) + } + return nil } @@ -574,6 +584,7 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed s.positionType = types.PositionShort // reset the transfer stats + s.State.PositionStartTime = premiumIndex.Time s.State.PendingBaseTransfer = fixedpoint.Zero s.State.TotalBaseTransfer = fixedpoint.Zero changed = true @@ -582,6 +593,12 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) + holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) + if holdingPeriod < time.Duration(s.MinHoldingPeriod) { + log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) + return + } + s.positionAction = PositionClosing // reset the transfer stats From 3624dd03389c2c26a579fa7ad2774b3ed66ec43d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 22:54:42 +0800 Subject: [PATCH 0650/1392] xfunding: implement close position transfer --- pkg/strategy/xfunding/strategy.go | 136 +++++++++++--------- pkg/strategy/xfunding/transfer.go | 39 +++--- pkg/util/backoff/{generic.go => general.go} | 2 +- 3 files changed, 100 insertions(+), 77 deletions(-) rename pkg/util/backoff/{generic.go => general.go} (80%) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index c8d2846763..b115177053 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -273,45 +273,60 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order // // when closing a position, we place orders on the futures account first, then the spot account // we need to close the position according to its base quantity instead of quote quantity - if s.positionType == types.PositionShort { - switch s.positionAction { - case PositionOpening: - if trade.Side != types.SideTypeBuy { - log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) - return - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) - if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - s.positionAction = PositionNoOp - } - - // 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account - // TODO: handle missing trades here. If the process crashed during the transfer, how to recover? - if err := backoff.RetryGeneric(ctx, func() error { - return s.transferIn(ctx, binanceSpot, trade) - }); err != nil { - log.WithError(err).Errorf("spot-to-futures transfer in retry failed") - return - } - - // 2) transferred successfully, sync futures position - // compare spot position and futures position, increase the position size until they are the same size - - case PositionClosing: - if trade.Side != types.SideTypeSell { - log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade) - return - } + if s.positionType != types.PositionShort { + return + } + + switch s.positionAction { + case PositionOpening: + if trade.Side != types.SideTypeBuy { + log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) + return + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) + if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + s.positionAction = PositionNoOp + } + // if we have trade, try to query the balance and transfer the balance to the futures wallet account + // TODO: handle missing trades here. If the process crashed during the transfer, how to recover? + if err := backoff.RetryGeneral(ctx, func() error { + return s.transferIn(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade) + }); err != nil { + log.WithError(err).Errorf("spot-to-futures transfer in retry failed") + return } + + case PositionClosing: + if trade.Side != types.SideTypeSell { + log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade) + return + } + } }) s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) + s.futuresOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + if s.positionType != types.PositionShort { + return + } + + switch s.positionAction { + case PositionClosing: + if err := backoff.RetryGeneral(ctx, func() error { + return s.transferOut(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade) + }); err != nil { + log.WithError(err).Errorf("spot-to-futures transfer in retry failed") + return + } + + } + }) s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { // s.queryAndDetectPremiumIndex(ctx, binanceFutures) @@ -421,6 +436,8 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { } // syncFuturesPosition syncs the futures position with the given spot position +// when the spot is transferred successfully, sync futures position +// compare spot position and futures position, increase the position size until they are the same size func (s *Strategy) syncFuturesPosition(ctx context.Context) { if s.positionType != types.PositionShort { return @@ -495,7 +512,6 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { Quantity: orderQuantity, Price: orderPrice, Market: s.futuresMarket, - // TimeInForce: types.TimeInForceGTC, }) if err != nil { @@ -574,38 +590,40 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed log.Infof("last %s funding rate: %s", s.Symbol, fundingRate.Percentage()) - if s.ShortFundingRate != nil { - if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { - - log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", - fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) + if s.ShortFundingRate == nil { + return changed + } - s.positionAction = PositionOpening - s.positionType = types.PositionShort + if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { - // reset the transfer stats - s.State.PositionStartTime = premiumIndex.Time - s.State.PendingBaseTransfer = fixedpoint.Zero - s.State.TotalBaseTransfer = fixedpoint.Zero - changed = true - } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", + fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) - log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", - fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) + s.positionAction = PositionOpening + s.positionType = types.PositionShort - holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) - if holdingPeriod < time.Duration(s.MinHoldingPeriod) { - log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) - return - } + // reset the transfer stats + s.State.PositionStartTime = premiumIndex.Time + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero + changed = true + } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { - s.positionAction = PositionClosing + log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", + fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) - // reset the transfer stats - s.State.PendingBaseTransfer = fixedpoint.Zero - s.State.TotalBaseTransfer = fixedpoint.Zero - changed = true + holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) + if holdingPeriod < time.Duration(s.MinHoldingPeriod) { + log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) + return } + + s.positionAction = PositionClosing + + // reset the transfer stats + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero + changed = true } return changed diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 00a34afdc7..14e5abb9d7 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -13,15 +13,13 @@ type FuturesTransfer interface { QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) } -func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade types.Trade) error { - currency := s.spotMarket.BaseCurrency - +func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency string, trade types.Trade) error { // base asset needs BUY trades if trade.Side == types.SideTypeBuy { return nil } - balances, err := ex.QueryAccountBalances(ctx) + balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) if err != nil { return err } @@ -31,16 +29,23 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade ty return fmt.Errorf("%s balance not found", currency) } + quantity := trade.Quantity + + if s.Leverage.Compare(fixedpoint.One) > 0 { + // de-leverage and get the collateral base quantity for transfer + quantity = quantity.Div(s.Leverage) + } + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(trade.Quantity) < 0 { - log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) - s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) + if b.Available.IsZero() || b.Available.Compare(quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", quantity, currency) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } - amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + amount := s.State.PendingBaseTransfer.Add(quantity) - pos := s.SpotPosition.GetBase() + pos := s.FuturesPosition.GetBase().Abs().Div(s.Leverage) rest := pos.Sub(s.State.TotalBaseTransfer) if rest.Sign() < 0 { @@ -62,15 +67,14 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade ty return nil } -func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, trade types.Trade) error { - currency := s.spotMarket.BaseCurrency +func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, currency string, trade types.Trade) error { // base asset needs BUY trades if trade.Side == types.SideTypeSell { return nil } - balances, err := ex.QueryAccountBalances(ctx) + balances, err := s.spotSession.Exchange.QueryAccountBalances(ctx) if err != nil { return err } @@ -81,15 +85,16 @@ func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, trade typ } // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(trade.Quantity) < 0 { - log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) - s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) + quantity := trade.Quantity + if b.Available.Compare(quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", quantity, currency) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } - amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + amount := s.State.PendingBaseTransfer.Add(quantity) - pos := s.SpotPosition.GetBase() + pos := s.SpotPosition.GetBase().Abs() rest := pos.Sub(s.State.TotalBaseTransfer) if rest.Sign() < 0 { diff --git a/pkg/util/backoff/generic.go b/pkg/util/backoff/general.go similarity index 80% rename from pkg/util/backoff/generic.go rename to pkg/util/backoff/general.go index f7303d3368..968acd8ef5 100644 --- a/pkg/util/backoff/generic.go +++ b/pkg/util/backoff/general.go @@ -8,7 +8,7 @@ import ( var MaxRetries uint64 = 101 -func RetryGeneric(ctx context.Context, op backoff.Operation) (err error) { +func RetryGeneral(ctx context.Context, op backoff.Operation) (err error) { err = backoff.Retry(op, backoff.WithContext( backoff.WithMaxRetries( backoff.NewExponentialBackOff(), From e016892a7016af91418830e1da42adc84da27c7f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 22:57:13 +0800 Subject: [PATCH 0651/1392] xfunding: pull out startClosingPosition, startOpeningPosition method --- pkg/strategy/xfunding/strategy.go | 32 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index b115177053..932a97d005 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -599,13 +599,7 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) - s.positionAction = PositionOpening - s.positionType = types.PositionShort - - // reset the transfer stats - s.State.PositionStartTime = premiumIndex.Time - s.State.PendingBaseTransfer = fixedpoint.Zero - s.State.TotalBaseTransfer = fixedpoint.Zero + s.startOpeningPosition(types.PositionShort, premiumIndex.Time) changed = true } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { @@ -618,17 +612,31 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed return } - s.positionAction = PositionClosing - - // reset the transfer stats - s.State.PendingBaseTransfer = fixedpoint.Zero - s.State.TotalBaseTransfer = fixedpoint.Zero + s.startClosingPosition() changed = true } return changed } +func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { + s.positionAction = PositionOpening + s.positionType = pt + + // reset the transfer stats + s.State.PositionStartTime = t + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero +} + +func (s *Strategy) startClosingPosition() { + s.positionAction = PositionClosing + + // reset the transfer stats + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero +} + func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position) orderExecutor.SetMaxRetries(0) From 108bb5deeb68fa21414432e447e2bad66e93b63a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 22:58:42 +0800 Subject: [PATCH 0652/1392] xfunding: add guard condition for starting and stopping --- pkg/strategy/xfunding/strategy.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 932a97d005..3590564ad2 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -620,6 +620,10 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed } func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { + if s.positionAction == PositionOpening { + return + } + s.positionAction = PositionOpening s.positionType = pt @@ -630,6 +634,10 @@ func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { } func (s *Strategy) startClosingPosition() { + if s.positionAction == PositionClosing { + return + } + s.positionAction = PositionClosing // reset the transfer stats From c1fbbbe4000062184233c8d9ffc39f5d56fb764a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 00:36:28 +0800 Subject: [PATCH 0653/1392] xfunding: move position state to state struct --- .../xfunding/positionaction_string.go | 25 --------- pkg/strategy/xfunding/positionstate_string.go | 25 +++++++++ pkg/strategy/xfunding/strategy.go | 52 ++++++++++++------- 3 files changed, 58 insertions(+), 44 deletions(-) delete mode 100644 pkg/strategy/xfunding/positionaction_string.go create mode 100644 pkg/strategy/xfunding/positionstate_string.go diff --git a/pkg/strategy/xfunding/positionaction_string.go b/pkg/strategy/xfunding/positionaction_string.go deleted file mode 100644 index 6aba4acf8a..0000000000 --- a/pkg/strategy/xfunding/positionaction_string.go +++ /dev/null @@ -1,25 +0,0 @@ -// Code generated by "stringer -type=PositionAction"; DO NOT EDIT. - -package xfunding - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[PositionNoOp-0] - _ = x[PositionOpening-1] - _ = x[PositionClosing-2] -} - -const _PositionAction_name = "PositionNoOpPositionOpeningPositionClosing" - -var _PositionAction_index = [...]uint8{0, 12, 27, 42} - -func (i PositionAction) String() string { - if i < 0 || i >= PositionAction(len(_PositionAction_index)-1) { - return "PositionAction(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _PositionAction_name[_PositionAction_index[i]:_PositionAction_index[i+1]] -} diff --git a/pkg/strategy/xfunding/positionstate_string.go b/pkg/strategy/xfunding/positionstate_string.go new file mode 100644 index 0000000000..c227a848b7 --- /dev/null +++ b/pkg/strategy/xfunding/positionstate_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type=PositionState"; DO NOT EDIT. + +package xfunding + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[PositionNoOp-0] + _ = x[PositionOpening-1] + _ = x[PositionClosing-2] +} + +const _PositionState_name = "PositionNoOpPositionOpeningPositionClosing" + +var _PositionState_index = [...]uint8{0, 12, 27, 42} + +func (i PositionState) String() string { + if i < 0 || i >= PositionState(len(_PositionState_index)-1) { + return "PositionState(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _PositionState_name[_PositionState_index[i]:_PositionState_index[i+1]] +} diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 3590564ad2..84bc1de720 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -20,11 +20,15 @@ import ( const ID = "xfunding" -//go:generate stringer -type=PositionAction -type PositionAction int +// Position State Transitions: +// NoOp -> Opening | Closing +// Opening -> NoOp -> Closing +// Closing -> NoOp -> Opening +//go:generate stringer -type=PositionState +type PositionState int const ( - PositionNoOp PositionAction = iota + PositionNoOp PositionState = iota PositionOpening PositionClosing ) @@ -104,16 +108,17 @@ type Strategy struct { spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor spotMarket, futuresMarket types.Market - // positionAction is default to NoOp - positionAction PositionAction - // positionType is the futures position type // currently we only support short position for the positive funding rate positionType types.PositionType } type State struct { - PositionStartTime time.Time `json:"positionStartTime"` + PositionStartTime time.Time `json:"positionStartTime"` + + // PositionState is default to NoOp + PositionState PositionState + PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` TotalBaseTransfer fixedpoint.Value `json:"totalBaseTransfer"` UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` @@ -252,6 +257,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if s.State == nil { s.State = &State{ + PositionState: PositionNoOp, PendingBaseTransfer: fixedpoint.Zero, TotalBaseTransfer: fixedpoint.Zero, UsedQuoteInvestment: fixedpoint.Zero, @@ -277,7 +283,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return } - switch s.positionAction { + switch s.State.PositionState { case PositionOpening: if trade.Side != types.SideTypeBuy { log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) @@ -289,7 +295,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - s.positionAction = PositionNoOp + s.State.PositionState = PositionNoOp } // if we have trade, try to query the balance and transfer the balance to the futures wallet account @@ -316,7 +322,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return } - switch s.positionAction { + switch s.State.PositionState { case PositionClosing: if err := backoff.RetryGeneral(ctx, func() error { return s.transferOut(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade) @@ -367,13 +373,13 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture log.Infof("premiumIndex: %+v", premiumIndex) if changed := s.detectPremiumIndex(premiumIndex); changed { - log.Infof("position action: %s %s", s.positionType, s.positionAction.String()) + log.Infof("position action: %s %s", s.positionType, s.State.PositionState.String()) s.triggerPositionAction(ctx) } } func (s *Strategy) triggerPositionAction(ctx context.Context) { - switch s.positionAction { + switch s.State.PositionState { case PositionOpening: s.increaseSpotPosition(ctx) s.syncFuturesPosition(ctx) @@ -384,7 +390,7 @@ func (s *Strategy) triggerPositionAction(ctx context.Context) { } func (s *Strategy) reduceFuturesPosition(ctx context.Context) { - switch s.positionAction { + switch s.State.PositionState { case PositionOpening, PositionNoOp: return } @@ -443,7 +449,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } - switch s.positionAction { + switch s.State.PositionState { case PositionClosing: return case PositionOpening, PositionNoOp: @@ -532,7 +538,8 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { log.Errorf("funding long position type is not supported") return } - if s.positionAction != PositionOpening { + + if s.State.PositionState != PositionOpening { return } @@ -540,6 +547,8 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { defer s.mu.Unlock() if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + // stop increase the position + s.State.PositionState = PositionNoOp return } @@ -586,6 +595,10 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { } func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed bool) { + if s.State.PositionState != PositionNoOp { + return changed + } + fundingRate := premiumIndex.LastFundingRate log.Infof("last %s funding rate: %s", s.Symbol, fundingRate.Percentage()) @@ -620,11 +633,12 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed } func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { - if s.positionAction == PositionOpening { + // we should only open a new position when there is no op on the position + if s.State.PositionState != PositionNoOp { return } - s.positionAction = PositionOpening + s.State.PositionState = PositionOpening s.positionType = pt // reset the transfer stats @@ -634,11 +648,11 @@ func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { } func (s *Strategy) startClosingPosition() { - if s.positionAction == PositionClosing { + if s.State.PositionState != PositionNoOp { return } - s.positionAction = PositionClosing + s.State.PositionState = PositionClosing // reset the transfer stats s.State.PendingBaseTransfer = fixedpoint.Zero From 62e6b232ed34e42b9dd45f0f80d19a252ec89874 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 00:52:36 +0800 Subject: [PATCH 0654/1392] xfunding: refactor and refine PositionState checking --- pkg/strategy/xfunding/positionstate_string.go | 9 ++- pkg/strategy/xfunding/strategy.go | 80 +++++++++---------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/pkg/strategy/xfunding/positionstate_string.go b/pkg/strategy/xfunding/positionstate_string.go index c227a848b7..67eb948054 100644 --- a/pkg/strategy/xfunding/positionstate_string.go +++ b/pkg/strategy/xfunding/positionstate_string.go @@ -8,14 +8,15 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[PositionNoOp-0] + _ = x[PositionClosed-0] _ = x[PositionOpening-1] - _ = x[PositionClosing-2] + _ = x[PositionReady-2] + _ = x[PositionClosing-3] } -const _PositionState_name = "PositionNoOpPositionOpeningPositionClosing" +const _PositionState_name = "PositionClosedPositionOpeningPositionReadyPositionClosing" -var _PositionState_index = [...]uint8{0, 12, 27, 42} +var _PositionState_index = [...]uint8{0, 14, 29, 42, 57} func (i PositionState) String() string { if i < 0 || i >= PositionState(len(_PositionState_index)-1) { diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 84bc1de720..ef4520cc1a 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -21,15 +21,16 @@ import ( const ID = "xfunding" // Position State Transitions: -// NoOp -> Opening | Closing -// Opening -> NoOp -> Closing -// Closing -> NoOp -> Opening +// NoOp -> Opening +// Opening -> Ready -> Closing +// Closing -> Closed -> Opening //go:generate stringer -type=PositionState type PositionState int const ( - PositionNoOp PositionState = iota + PositionClosed PositionState = iota PositionOpening + PositionReady PositionClosing ) @@ -142,16 +143,7 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { }) } -func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - for _, detection := range s.SupportDetection { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: detection.Interval, - }) - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: detection.MovingAverageIntervalWindow.Interval, - }) - } -} +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {} func (s *Strategy) Defaults() error { if s.Leverage.IsZero() { @@ -257,7 +249,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if s.State == nil { s.State = &State{ - PositionState: PositionNoOp, + PositionState: PositionClosed, PendingBaseTransfer: fixedpoint.Zero, TotalBaseTransfer: fixedpoint.Zero, UsedQuoteInvestment: fixedpoint.Zero, @@ -295,7 +287,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - s.State.PositionState = PositionNoOp + s.State.PositionState = PositionClosed } // if we have trade, try to query the balance and transfer the balance to the futures wallet account @@ -391,7 +383,7 @@ func (s *Strategy) triggerPositionAction(ctx context.Context) { func (s *Strategy) reduceFuturesPosition(ctx context.Context) { switch s.State.PositionState { - case PositionOpening, PositionNoOp: + case PositionOpening, PositionClosed: return } @@ -452,7 +444,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { switch s.State.PositionState { case PositionClosing: return - case PositionOpening, PositionNoOp: + case PositionOpening, PositionClosed: } spotBase := s.SpotPosition.GetBase() // should be positive base quantity here @@ -539,16 +531,16 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { return } + s.mu.Lock() + defer s.mu.Unlock() + if s.State.PositionState != PositionOpening { return } - s.mu.Lock() - defer s.mu.Unlock() - if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { // stop increase the position - s.State.PositionState = PositionNoOp + s.State.PositionState = PositionReady return } @@ -595,7 +587,7 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { } func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed bool) { - if s.State.PositionState != PositionNoOp { + if s.State.PositionState != PositionClosed { return changed } @@ -607,34 +599,39 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed return changed } - if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + switch s.State.PositionState { - log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", - fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) + case PositionClosed: + if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", + fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) - s.startOpeningPosition(types.PositionShort, premiumIndex.Time) - changed = true - } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + s.startOpeningPosition(types.PositionShort, premiumIndex.Time) + changed = true + } - log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", - fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) + case PositionReady: + if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", + fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) - holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) - if holdingPeriod < time.Duration(s.MinHoldingPeriod) { - log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) - return - } + holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) + if holdingPeriod < time.Duration(s.MinHoldingPeriod) { + log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) + return + } - s.startClosingPosition() - changed = true + s.startClosingPosition() + changed = true + } } return changed } func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { - // we should only open a new position when there is no op on the position - if s.State.PositionState != PositionNoOp { + // only open a new position when there is no position + if s.State.PositionState != PositionClosed { return } @@ -648,7 +645,8 @@ func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { } func (s *Strategy) startClosingPosition() { - if s.State.PositionState != PositionNoOp { + // we can't close a position that is not ready + if s.State.PositionState != PositionReady { return } From 209fb102face897a56236e7e097f703a2e34286f Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 01:57:43 +0800 Subject: [PATCH 0655/1392] xfunding: add stringer support on PremiumIndex --- config/xfunding.yaml | 1 + pkg/strategy/xfunding/strategy.go | 49 ++++++++++++++++++------------- pkg/types/premiumindex.go | 5 ++++ 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index b39712659d..047d278bd0 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -37,3 +37,4 @@ crossExchangeStrategies: shortFundingRate: high: 0.000% low: -0.01% + reset: true diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index ef4520cc1a..1b30366520 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -43,6 +43,24 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +type State struct { + PositionStartTime time.Time `json:"positionStartTime"` + + // PositionState is default to NoOp + PositionState PositionState + + PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` + TotalBaseTransfer fixedpoint.Value `json:"totalBaseTransfer"` + UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` +} + +func (s *State) Reset() { + s.PositionState = PositionClosed + s.PendingBaseTransfer = fixedpoint.Zero + s.TotalBaseTransfer = fixedpoint.Zero + s.UsedQuoteInvestment = fixedpoint.Zero +} + // Strategy is the xfunding fee strategy // Right now it only supports short position in the USDT futures account. // When opening the short position, it uses spot account to buy inventory, then transfer the inventory to the futures account as collateral assets. @@ -95,6 +113,7 @@ type Strategy struct { SpotSession string `json:"spotSession"` FuturesSession string `json:"futuresSession"` + Reset bool `json:"reset"` ProfitStats *types.ProfitStats `persistence:"profit_stats"` SpotPosition *types.Position `persistence:"spot_position"` @@ -114,17 +133,6 @@ type Strategy struct { positionType types.PositionType } -type State struct { - PositionStartTime time.Time `json:"positionStartTime"` - - // PositionState is default to NoOp - PositionState PositionState - - PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` - TotalBaseTransfer fixedpoint.Value `json:"totalBaseTransfer"` - UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` -} - func (s *Strategy) ID() string { return ID } @@ -235,19 +243,19 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } } - if s.ProfitStats == nil { + if s.ProfitStats == nil || s.Reset { s.ProfitStats = types.NewProfitStats(s.Market) } - if s.FuturesPosition == nil { + if s.FuturesPosition == nil || s.Reset { s.FuturesPosition = types.NewPositionFromMarket(s.futuresMarket) } - if s.SpotPosition == nil { + if s.SpotPosition == nil || s.Reset { s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) } - if s.State == nil { + if s.State == nil || s.Reset { s.State = &State{ PositionState: PositionClosed, PendingBaseTransfer: fixedpoint.Zero, @@ -365,12 +373,13 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture log.Infof("premiumIndex: %+v", premiumIndex) if changed := s.detectPremiumIndex(premiumIndex); changed { - log.Infof("position action: %s %s", s.positionType, s.State.PositionState.String()) - s.triggerPositionAction(ctx) + log.Infof("position state changed: %s %s", s.positionType, s.State.PositionState.String()) } + + s.sync(ctx) } -func (s *Strategy) triggerPositionAction(ctx context.Context) { +func (s *Strategy) sync(ctx context.Context) { switch s.State.PositionState { case PositionOpening: s.increaseSpotPosition(ctx) @@ -441,10 +450,8 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } - switch s.State.PositionState { - case PositionClosing: + if s.State.PositionState != PositionOpening { return - case PositionOpening, PositionClosed: } spotBase := s.SpotPosition.GetBase() // should be positive base quantity here diff --git a/pkg/types/premiumindex.go b/pkg/types/premiumindex.go index c9ffcd0aab..5b2e4094ce 100644 --- a/pkg/types/premiumindex.go +++ b/pkg/types/premiumindex.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "time" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -13,3 +14,7 @@ type PremiumIndex struct { NextFundingTime time.Time `json:"nextFundingTime"` Time time.Time `json:"time"` } + +func (i *PremiumIndex) String() string { + return fmt.Sprintf("%s %s %s %s NEXT: %s", i.Symbol, i.MarkPrice.String(), i.LastFundingRate.Percentage(), i.Time, i.NextFundingTime) +} From f3049de2baf0d31ce71935d99aa5d50ceafa0479 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 02:09:49 +0800 Subject: [PATCH 0656/1392] all: improve logging --- config/xfunding.yaml | 11 ++++++++--- pkg/strategy/xfunding/strategy.go | 12 +++++++----- pkg/types/premiumindex.go | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 047d278bd0..3d80c528dc 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -32,9 +32,14 @@ crossExchangeStrategies: futuresSession: binance_futures symbol: ETHUSDT leverage: 1.0 - incrementalQuoteQuantity: 11 - quoteInvestment: 110 + incrementalQuoteQuantity: 20 + quoteInvestment: 50 + shortFundingRate: - high: 0.000% + ## when funding rate is higher than this high value, the strategy will start buying spot and opening a short position + high: 0.001% + ## when funding rate is lower than this low value, the strategy will start closing futures position and sell the spot low: -0.01% + + ## reset will reset the spot/futures positions, the transfer stats and the position state. reset: true diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 1b30366520..430c2e4192 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -349,7 +349,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order case <-ticker.C: s.queryAndDetectPremiumIndex(ctx, binanceFutures) - + s.sync(ctx) } } }() @@ -370,13 +370,11 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture return } - log.Infof("premiumIndex: %+v", premiumIndex) + log.Info(premiumIndex) if changed := s.detectPremiumIndex(premiumIndex); changed { - log.Infof("position state changed: %s %s", s.positionType, s.State.PositionState.String()) + log.Infof("position state changed to -> %s %s", s.positionType, s.State.PositionState.String()) } - - s.sync(ctx) } func (s *Strategy) sync(ctx context.Context) { @@ -548,6 +546,8 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { // stop increase the position s.State.PositionState = PositionReady + + s.startClosingPosition() return } @@ -642,6 +642,7 @@ func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { return } + log.Infof("startOpeningPosition") s.State.PositionState = PositionOpening s.positionType = pt @@ -657,6 +658,7 @@ func (s *Strategy) startClosingPosition() { return } + log.Infof("startClosingPosition") s.State.PositionState = PositionClosing // reset the transfer stats diff --git a/pkg/types/premiumindex.go b/pkg/types/premiumindex.go index 5b2e4094ce..0dd3bc52d1 100644 --- a/pkg/types/premiumindex.go +++ b/pkg/types/premiumindex.go @@ -16,5 +16,5 @@ type PremiumIndex struct { } func (i *PremiumIndex) String() string { - return fmt.Sprintf("%s %s %s %s NEXT: %s", i.Symbol, i.MarkPrice.String(), i.LastFundingRate.Percentage(), i.Time, i.NextFundingTime) + return fmt.Sprintf("PremiumIndex | %s | %.4f | %s | %s | NEXT FUNDING TIME: %s", i.Symbol, i.MarkPrice.Float64(), i.LastFundingRate.Percentage(), i.Time, i.NextFundingTime) } From 84313dbdf9535ccf352d40fe3da0e16197a30769 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 02:52:13 +0800 Subject: [PATCH 0657/1392] xfunding: refactor state functions and fix transfer out --- pkg/strategy/xfunding/strategy.go | 72 ++++++++++++++++++------------- pkg/strategy/xfunding/transfer.go | 48 +++++++++++++-------- 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 430c2e4192..c716521913 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -291,12 +291,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } s.mu.Lock() - defer s.mu.Unlock() - s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) - if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - s.State.PositionState = PositionClosed - } + s.mu.Unlock() // if we have trade, try to query the balance and transfer the balance to the futures wallet account // TODO: handle missing trades here. If the process crashed during the transfer, how to recover? @@ -318,11 +314,14 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) s.futuresOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + + log.Infof("futures trade: %v", trade) + if s.positionType != types.PositionShort { return } - switch s.State.PositionState { + switch s.getPositionState() { case PositionClosing: if err := backoff.RetryGeneral(ctx, func() error { return s.transferOut(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade) @@ -378,7 +377,7 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture } func (s *Strategy) sync(ctx context.Context) { - switch s.State.PositionState { + switch s.getPositionState() { case PositionOpening: s.increaseSpotPosition(ctx) s.syncFuturesPosition(ctx) @@ -389,8 +388,7 @@ func (s *Strategy) sync(ctx context.Context) { } func (s *Strategy) reduceFuturesPosition(ctx context.Context) { - switch s.State.PositionState { - case PositionOpening, PositionClosed: + if s.notPositionState(PositionClosing) { return } @@ -411,8 +409,7 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { } if futuresBase.Compare(fixedpoint.Zero) < 0 { - orderPrice := ticker.Sell - + orderPrice := ticker.Buy orderQuantity := futuresBase.Abs() orderQuantity = fixedpoint.Max(orderQuantity, s.futuresMarket.MinQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) @@ -448,7 +445,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } - if s.State.PositionState != PositionOpening { + if s.notPositionState(PositionOpening) { return } @@ -539,14 +536,15 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { s.mu.Lock() defer s.mu.Unlock() - if s.State.PositionState != PositionOpening { + if s.notPositionState(PositionOpening) { return } if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { // stop increase the position - s.State.PositionState = PositionReady + s.setPositionState(PositionReady) + // DEBUG CODE - triggering closing position automatically s.startClosingPosition() return } @@ -593,20 +591,16 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { log.Infof("created orders: %+v", createdOrders) } -func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed bool) { - if s.State.PositionState != PositionClosed { - return changed - } - +func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) bool { fundingRate := premiumIndex.LastFundingRate log.Infof("last %s funding rate: %s", s.Symbol, fundingRate.Percentage()) if s.ShortFundingRate == nil { - return changed + return false } - switch s.State.PositionState { + switch s.getPositionState() { case PositionClosed: if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { @@ -614,7 +608,7 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) s.startOpeningPosition(types.PositionShort, premiumIndex.Time) - changed = true + return true } case PositionReady: @@ -625,25 +619,26 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) if holdingPeriod < time.Duration(s.MinHoldingPeriod) { log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) - return + return false } s.startClosingPosition() - changed = true + return true } } - return changed + return false } func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { // only open a new position when there is no position - if s.State.PositionState != PositionClosed { + if s.notPositionState(PositionClosed) { return } log.Infof("startOpeningPosition") - s.State.PositionState = PositionOpening + s.setPositionState(PositionOpening) + s.positionType = pt // reset the transfer stats @@ -654,16 +649,33 @@ func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { func (s *Strategy) startClosingPosition() { // we can't close a position that is not ready - if s.State.PositionState != PositionReady { + if s.notPositionState(PositionReady) { return } log.Infof("startClosingPosition") - s.State.PositionState = PositionClosing + s.setPositionState(PositionClosing) // reset the transfer stats s.State.PendingBaseTransfer = fixedpoint.Zero - s.State.TotalBaseTransfer = fixedpoint.Zero +} + +func (s *Strategy) setPositionState(state PositionState) { + origState := s.State.PositionState + s.State.PositionState = state + log.Infof("position state transition: %s -> %s", origState.String(), state.String()) +} + +func (s *Strategy) isPositionState(state PositionState) bool { + return s.State.PositionState == state +} + +func (s *Strategy) getPositionState() PositionState { + return s.State.PositionState +} + +func (s *Strategy) notPositionState(state PositionState) bool { + return s.State.PositionState != state } func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 14e5abb9d7..12bcd2b6b7 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -15,44 +15,54 @@ type FuturesTransfer interface { func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency string, trade types.Trade) error { // base asset needs BUY trades - if trade.Side == types.SideTypeBuy { + if trade.Side != types.SideTypeBuy { return nil } + // if transfer done + if s.State.TotalBaseTransfer.IsZero() { + return nil + } + + // de-leverage and get the collateral base quantity for transfer + quantity := trade.Quantity + quantity = quantity.Div(s.Leverage) + balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) if err != nil { + log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), currency, s.State.PendingBaseTransfer.String()) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return err } b, ok := balances[currency] if !ok { + log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), currency, s.State.PendingBaseTransfer.String()) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return fmt.Errorf("%s balance not found", currency) } - quantity := trade.Quantity + // add the previous pending base transfer and the current trade quantity + amount := s.State.PendingBaseTransfer.Add(quantity) - if s.Leverage.Compare(fixedpoint.One) > 0 { - // de-leverage and get the collateral base quantity for transfer - quantity = quantity.Div(s.Leverage) - } + // try to transfer more if we enough balance + amount = fixedpoint.Min(amount, b.Available) + + // we can only transfer the rest quota (total base transfer) + amount = fixedpoint.Min(s.State.TotalBaseTransfer, amount) // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.IsZero() || b.Available.Compare(quantity) < 0 { - log.Infof("adding to pending base transfer: %s %s", quantity, currency) + if amount.IsZero() { + log.Infof("adding to pending base transfer: %s %s + %s ", quantity.String(), currency, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } - amount := s.State.PendingBaseTransfer.Add(quantity) + // de-leverage and get the collateral base quantity + collateralBase := s.FuturesPosition.GetBase().Abs().Div(s.Leverage) + _ = collateralBase - pos := s.FuturesPosition.GetBase().Abs().Div(s.Leverage) - rest := pos.Sub(s.State.TotalBaseTransfer) - - if rest.Sign() < 0 { - return nil - } - - amount = fixedpoint.Min(rest, amount) + // if s.State.TotalBaseTransfer.Compare(collateralBase) log.Infof("transfering out futures account asset %s %s", amount, currency) if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferOut); err != nil { @@ -62,8 +72,8 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency // reset pending transfer s.State.PendingBaseTransfer = fixedpoint.Zero - // record the transfer in the total base transfer - s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Add(amount) + // reduce the transfer in the total base transfer + s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Sub(amount) return nil } From 0f21c1fd8f423f106e3fc65784475583bbb42504 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 03:19:03 +0800 Subject: [PATCH 0658/1392] xfunding: fix Warnf format --- pkg/strategy/xfunding/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index c716521913..a62a4db693 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -618,7 +618,7 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) bool { holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) if holdingPeriod < time.Duration(s.MinHoldingPeriod) { - log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) + log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod.Duration()) return false } From 1517076f6d72d592b0977b71bb52c4eabc2d547b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 03:14:04 +0800 Subject: [PATCH 0659/1392] xfunding: implement syncSpotPosition --- config/xfunding.yaml | 2 +- pkg/strategy/xfunding/strategy.go | 85 +++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 3d80c528dc..2b65cd1670 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -42,4 +42,4 @@ crossExchangeStrategies: low: -0.01% ## reset will reset the spot/futures positions, the transfer stats and the position state. - reset: true + # reset: true diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index a62a4db693..02414a1a3e 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -524,7 +524,92 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { } func (s *Strategy) syncSpotPosition(ctx context.Context) { + if s.positionType != types.PositionShort { + return + } + + if s.notPositionState(PositionClosing) { + return + } + + spotBase := s.SpotPosition.GetBase() // should be positive base quantity here + futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here + + if spotBase.IsZero() { + s.setPositionState(PositionClosed) + return + } + + // skip short spot position + if spotBase.Sign() < 0 { + return + } + + log.Infof("spot/futures positions: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + + if futuresBase.Sign() > 0 { + // unexpected error + log.Errorf("unexpected futures position (got positive, expecting negative)") + return + } + + _ = s.futuresOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + if s.SpotPosition.IsDust(ticker.Sell) { + dust := s.SpotPosition.GetBase().Abs() + cost := s.SpotPosition.AverageCost + + log.Warnf("spot dust loss: %f %s (average cost = %f)", dust.Float64(), s.spotMarket.BaseCurrency, cost.Float64()) + + s.SpotPosition.Reset() + + s.setPositionState(PositionClosed) + return + } + + // spot pos size > futures pos size ==> reduce spot position + if spotBase.Compare(futuresBase.Neg()) > 0 { + diffQuantity := spotBase.Sub(futuresBase.Neg()) + + if diffQuantity.Sign() < 0 { + log.Errorf("unexpected negative position diff: %s", diffQuantity.String()) + return + } + + orderPrice := ticker.Sell + orderQuantity := diffQuantity + if b, ok := s.spotSession.Account.Balance(s.spotMarket.BaseCurrency); ok { + orderQuantity = fixedpoint.Min(b.Available, orderQuantity) + } + + // avoid increase the order size + if s.spotMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.spotMarket) + return + } + + createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.futuresMarket, + }) + + if err != nil { + log.WithError(err).Errorf("can not submit spot order") + return + } + + log.Infof("created spot orders: %+v", createdOrders) + } } func (s *Strategy) increaseSpotPosition(ctx context.Context) { From 4669692b8db9f620442dda265172781d6292edbd Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 03:16:51 +0800 Subject: [PATCH 0660/1392] xfunding: remove debug log and test code --- pkg/strategy/xfunding/strategy.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 02414a1a3e..2c2dfc72e2 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -314,9 +314,6 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) s.futuresOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - - log.Infof("futures trade: %v", trade) - if s.positionType != types.PositionShort { return } @@ -630,7 +627,7 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { s.setPositionState(PositionReady) // DEBUG CODE - triggering closing position automatically - s.startClosingPosition() + // s.startClosingPosition() return } From ea7af708f91a11437ced2adeb77a547e5ceb32e3 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 14:28:36 +0800 Subject: [PATCH 0661/1392] add mutex lock --- pkg/strategy/xfunding/strategy.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 2c2dfc72e2..15f29d2ac9 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -615,13 +615,12 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { return } - s.mu.Lock() - defer s.mu.Unlock() - if s.notPositionState(PositionOpening) { return } + s.mu.Lock() + defer s.mu.Unlock() if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { // stop increase the position s.setPositionState(PositionReady) @@ -743,13 +742,18 @@ func (s *Strategy) startClosingPosition() { } func (s *Strategy) setPositionState(state PositionState) { + s.mu.Lock() origState := s.State.PositionState s.State.PositionState = state + s.mu.Unlock() log.Infof("position state transition: %s -> %s", origState.String(), state.String()) } func (s *Strategy) isPositionState(state PositionState) bool { - return s.State.PositionState == state + s.mu.Lock() + ret := s.State.PositionState == state + s.mu.Unlock() + return ret } func (s *Strategy) getPositionState() PositionState { @@ -757,7 +761,10 @@ func (s *Strategy) getPositionState() PositionState { } func (s *Strategy) notPositionState(state PositionState) bool { - return s.State.PositionState != state + s.mu.Lock() + ret := s.State.PositionState != state + s.mu.Unlock() + return ret } func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { From feec194843f5cdfbd7b6ea60a6e2c71ed07127c0 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 14:37:18 +0800 Subject: [PATCH 0662/1392] binance: improve transfer logs --- pkg/exchange/binance/exchange.go | 9 ++++++++- pkg/strategy/xfunding/strategy.go | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 9880b6d2db..4e56853b2b 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -381,7 +381,14 @@ func (e *Exchange) TransferFuturesAccountAsset(ctx context.Context, asset string } resp, err := req.Do(ctx) - log.Infof("futures transfer %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) + + switch io { + case types.TransferIn: + log.Infof("internal transfer (spot) => (futures) %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) + case types.TransferOut: + log.Infof("internal transfer (futures) => (spot) %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) + } + return err } diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 15f29d2ac9..77e662d73e 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -162,6 +162,8 @@ func (s *Strategy) Defaults() error { s.MinHoldingPeriod = types.Duration(3 * 24 * time.Hour) } + s.positionType = types.PositionShort + return nil } From 9c0787e6ce672698b46dd7a7b77adcd59f66719e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 15:11:13 +0800 Subject: [PATCH 0663/1392] binance: pull out cancelFuturesOrders method --- pkg/exchange/binance/exchange.go | 48 +++++++++++++++++--------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 4e56853b2b..c2c7d8e035 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -892,34 +892,38 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, return toGlobalOrders(binanceOrders, e.IsMargin) } +func (e *Exchange) cancelFuturesOrders(ctx context.Context, orders ...types.Order) (err error) { + for _, o := range orders { + var req = e.futuresClient.NewCancelOrderService() + + // Mandatory + req.Symbol(o.Symbol) + + if o.OrderID > 0 { + req.OrderID(int64(o.OrderID)) + } else { + err = multierr.Append(err, types.NewOrderError( + fmt.Errorf("can not cancel %s order, order does not contain orderID or clientOrderID", o.Symbol), + o)) + continue + } + + _, err2 := req.Do(ctx) + if err2 != nil { + err = multierr.Append(err, types.NewOrderError(err2, o)) + } + } + + return err +} + func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err error) { if err := orderLimiter.Wait(ctx); err != nil { log.WithError(err).Errorf("order rate limiter wait error") } if e.IsFutures { - for _, o := range orders { - var req = e.futuresClient.NewCancelOrderService() - - // Mandatory - req.Symbol(o.Symbol) - - if o.OrderID > 0 { - req.OrderID(int64(o.OrderID)) - } else { - err = multierr.Append(err, types.NewOrderError( - fmt.Errorf("can not cancel %s order, order does not contain orderID or clientOrderID", o.Symbol), - o)) - continue - } - - _, err2 := req.Do(ctx) - if err2 != nil { - err = multierr.Append(err, types.NewOrderError(err2, o)) - } - } - - return err + return e.cancelFuturesOrders(ctx, orders...) } for _, o := range orders { From 071825e982e97a111b8ebdcc9be6e756a5990755 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 15:13:25 +0800 Subject: [PATCH 0664/1392] binance: move futures methods --- pkg/exchange/binance/exchange.go | 274 ----------------------------- pkg/exchange/binance/futures.go | 288 +++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+), 274 deletions(-) create mode 100644 pkg/exchange/binance/futures.go diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index c2c7d8e035..96eb12450e 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -367,31 +367,6 @@ func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) e return nil } -func (e *Exchange) TransferFuturesAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { - req := e.client2.NewFuturesTransferRequest() - req.Asset(asset) - req.Amount(amount.String()) - - if io == types.TransferIn { - req.TransferType(binanceapi.FuturesTransferSpotToUsdtFutures) - } else if io == types.TransferOut { - req.TransferType(binanceapi.FuturesTransferUsdtFuturesToSpot) - } else { - return fmt.Errorf("unexpected transfer direction: %d given", io) - } - - resp, err := req.Do(ctx) - - switch io { - case types.TransferIn: - log.Infof("internal transfer (spot) => (futures) %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) - case types.TransferOut: - log.Infof("internal transfer (futures) => (spot) %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) - } - - return err -} - // transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { req := e.client.NewMarginTransferService() @@ -676,42 +651,6 @@ func (e *Exchange) QuerySpotAccount(ctx context.Context) (*types.Account, error) return a, nil } -// QueryFuturesAccount gets the futures account balances from Binance -// Balance.Available = Wallet Balance(in Binance UI) - Used Margin -// Balance.Locked = Used Margin -func (e *Exchange) QueryFuturesAccount(ctx context.Context) (*types.Account, error) { - account, err := e.futuresClient.NewGetAccountService().Do(ctx) - if err != nil { - return nil, err - } - accountBalances, err := e.futuresClient.NewGetBalanceService().Do(ctx) - if err != nil { - return nil, err - } - - var balances = map[string]types.Balance{} - for _, b := range accountBalances { - balanceAvailable := fixedpoint.Must(fixedpoint.NewFromString(b.AvailableBalance)) - balanceTotal := fixedpoint.Must(fixedpoint.NewFromString(b.Balance)) - unrealizedPnl := fixedpoint.Must(fixedpoint.NewFromString(b.CrossUnPnl)) - balances[b.Asset] = types.Balance{ - Currency: b.Asset, - Available: balanceAvailable, - Locked: balanceTotal.Sub(balanceAvailable.Sub(unrealizedPnl)), - } - } - - a := &types.Account{ - AccountType: types.AccountTypeFutures, - FuturesInfo: toGlobalFuturesAccountInfo(account), // In binance GO api, Account define account info which mantain []*AccountAsset and []*AccountPosition. - CanDeposit: account.CanDeposit, // if can transfer in asset - CanTrade: account.CanTrade, // if can trade - CanWithdraw: account.CanWithdraw, // if can transfer out asset - } - a.UpdateBalances(balances) - return a, nil -} - func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { var account *types.Account var err error @@ -892,31 +831,6 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, return toGlobalOrders(binanceOrders, e.IsMargin) } -func (e *Exchange) cancelFuturesOrders(ctx context.Context, orders ...types.Order) (err error) { - for _, o := range orders { - var req = e.futuresClient.NewCancelOrderService() - - // Mandatory - req.Symbol(o.Symbol) - - if o.OrderID > 0 { - req.OrderID(int64(o.OrderID)) - } else { - err = multierr.Append(err, types.NewOrderError( - fmt.Errorf("can not cancel %s order, order does not contain orderID or clientOrderID", o.Symbol), - o)) - continue - } - - _, err2 := req.Do(ctx) - if err2 != nil { - err = multierr.Append(err, types.NewOrderError(err2, o)) - } - } - - return err -} - func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err error) { if err := orderLimiter.Wait(ctx); err != nil { log.WithError(err).Errorf("order rate limiter wait error") @@ -1068,91 +982,6 @@ func (e *Exchange) submitMarginOrder(ctx context.Context, order types.SubmitOrde return createdOrder, err } -func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { - orderType, err := toLocalFuturesOrderType(order.Type) - if err != nil { - return nil, err - } - - req := e.futuresClient.NewCreateOrderService(). - Symbol(order.Symbol). - Type(orderType). - Side(futures.SideType(order.Side)). - ReduceOnly(order.ReduceOnly) - - clientOrderID := newFuturesClientOrderID(order.ClientOrderID) - if len(clientOrderID) > 0 { - req.NewClientOrderID(clientOrderID) - } - - // use response result format - req.NewOrderResponseType(futures.NewOrderRespTypeRESULT) - - if order.Market.Symbol != "" { - req.Quantity(order.Market.FormatQuantity(order.Quantity)) - } else { - // TODO report error - req.Quantity(order.Quantity.FormatString(8)) - } - - // set price field for limit orders - switch order.Type { - case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker: - if order.Market.Symbol != "" { - req.Price(order.Market.FormatPrice(order.Price)) - } else { - // TODO report error - req.Price(order.Price.FormatString(8)) - } - } - - // set stop price - switch order.Type { - - case types.OrderTypeStopLimit, types.OrderTypeStopMarket: - if order.Market.Symbol != "" { - req.StopPrice(order.Market.FormatPrice(order.StopPrice)) - } else { - // TODO report error - req.StopPrice(order.StopPrice.FormatString(8)) - } - } - - // could be IOC or FOK - if len(order.TimeInForce) > 0 { - // TODO: check the TimeInForce value - req.TimeInForce(futures.TimeInForceType(order.TimeInForce)) - } else { - switch order.Type { - case types.OrderTypeLimit, types.OrderTypeLimitMaker, types.OrderTypeStopLimit: - req.TimeInForce(futures.TimeInForceTypeGTC) - } - } - - response, err := req.Do(ctx) - if err != nil { - return nil, err - } - - log.Infof("futures order creation response: %+v", response) - - createdOrder, err := toGlobalFuturesOrder(&futures.Order{ - Symbol: response.Symbol, - OrderID: response.OrderID, - ClientOrderID: response.ClientOrderID, - Price: response.Price, - OrigQuantity: response.OrigQuantity, - ExecutedQuantity: response.ExecutedQuantity, - Status: response.Status, - TimeInForce: response.TimeInForce, - Type: response.Type, - Side: response.Side, - ReduceOnly: response.ReduceOnly, - }, false) - - return createdOrder, err -} - // BBGO is a broker on Binance const spotBrokerID = "NSUYEBKM" @@ -1379,60 +1208,6 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type return kLines, nil } -func (e *Exchange) QueryFuturesKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - - var limit = 1000 - if options.Limit > 0 { - // default limit == 1000 - limit = options.Limit - } - - log.Infof("querying kline %s %s %v", symbol, interval, options) - - req := e.futuresClient.NewKlinesService(). - Symbol(symbol). - Interval(string(interval)). - Limit(limit) - - if options.StartTime != nil { - req.StartTime(options.StartTime.UnixMilli()) - } - - if options.EndTime != nil { - req.EndTime(options.EndTime.UnixMilli()) - } - - resp, err := req.Do(ctx) - if err != nil { - return nil, err - } - - var kLines []types.KLine - for _, k := range resp { - kLines = append(kLines, types.KLine{ - Exchange: types.ExchangeBinance, - Symbol: symbol, - Interval: interval, - StartTime: types.NewTimeFromUnix(0, k.OpenTime*int64(time.Millisecond)), - EndTime: types.NewTimeFromUnix(0, k.CloseTime*int64(time.Millisecond)), - Open: fixedpoint.MustNewFromString(k.Open), - Close: fixedpoint.MustNewFromString(k.Close), - High: fixedpoint.MustNewFromString(k.High), - Low: fixedpoint.MustNewFromString(k.Low), - Volume: fixedpoint.MustNewFromString(k.Volume), - QuoteVolume: fixedpoint.MustNewFromString(k.QuoteAssetVolume), - TakerBuyBaseAssetVolume: fixedpoint.MustNewFromString(k.TakerBuyBaseAssetVolume), - TakerBuyQuoteAssetVolume: fixedpoint.MustNewFromString(k.TakerBuyQuoteAssetVolume), - LastTradeID: 0, - NumberOfTrades: uint64(k.TradeNum), - Closed: true, - }) - } - - kLines = types.SortKLinesAscending(kLines) - return kLines, nil -} - func (e *Exchange) queryMarginTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { var remoteTrades []*binance.TradeV3 req := e.client.NewListMarginTradesService(). @@ -1482,55 +1257,6 @@ func (e *Exchange) queryMarginTrades(ctx context.Context, symbol string, options return trades, nil } -func (e *Exchange) queryFuturesTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { - - var remoteTrades []*futures.AccountTrade - req := e.futuresClient.NewListAccountTradeService(). - Symbol(symbol) - if options.Limit > 0 { - req.Limit(int(options.Limit)) - } else { - req.Limit(1000) - } - - // BINANCE uses inclusive last trade ID - if options.LastTradeID > 0 { - req.FromID(int64(options.LastTradeID)) - } - - // The parameter fromId cannot be sent with startTime or endTime. - // Mentioned in binance futures docs - if options.LastTradeID <= 0 { - if options.StartTime != nil && options.EndTime != nil { - if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { - req.StartTime(options.StartTime.UnixMilli()) - req.EndTime(options.EndTime.UnixMilli()) - } else { - req.StartTime(options.StartTime.UnixMilli()) - } - } else if options.EndTime != nil { - req.EndTime(options.EndTime.UnixMilli()) - } - } - - remoteTrades, err = req.Do(ctx) - if err != nil { - return nil, err - } - for _, t := range remoteTrades { - localTrade, err := toGlobalFuturesTrade(*t) - if err != nil { - log.WithError(err).Errorf("can not convert binance futures trade: %+v", t) - continue - } - - trades = append(trades, *localTrade) - } - - trades = types.SortTradesAscending(trades) - return trades, nil -} - func (e *Exchange) querySpotTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { req := e.client2.NewGetMyTradesRequest() req.Symbol(symbol) diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go new file mode 100644 index 0000000000..0f72573877 --- /dev/null +++ b/pkg/exchange/binance/futures.go @@ -0,0 +1,288 @@ +package binance + +import ( + "context" + "fmt" + "time" + + "github.com/adshao/go-binance/v2/futures" + "go.uber.org/multierr" + + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func (e *Exchange) TransferFuturesAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { + req := e.client2.NewFuturesTransferRequest() + req.Asset(asset) + req.Amount(amount.String()) + + if io == types.TransferIn { + req.TransferType(binanceapi.FuturesTransferSpotToUsdtFutures) + } else if io == types.TransferOut { + req.TransferType(binanceapi.FuturesTransferUsdtFuturesToSpot) + } else { + return fmt.Errorf("unexpected transfer direction: %d given", io) + } + + resp, err := req.Do(ctx) + + switch io { + case types.TransferIn: + log.Infof("internal transfer (spot) => (futures) %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) + case types.TransferOut: + log.Infof("internal transfer (futures) => (spot) %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) + } + + return err +} + +// QueryFuturesAccount gets the futures account balances from Binance +// Balance.Available = Wallet Balance(in Binance UI) - Used Margin +// Balance.Locked = Used Margin +func (e *Exchange) QueryFuturesAccount(ctx context.Context) (*types.Account, error) { + account, err := e.futuresClient.NewGetAccountService().Do(ctx) + if err != nil { + return nil, err + } + accountBalances, err := e.futuresClient.NewGetBalanceService().Do(ctx) + if err != nil { + return nil, err + } + + var balances = map[string]types.Balance{} + for _, b := range accountBalances { + balanceAvailable := fixedpoint.Must(fixedpoint.NewFromString(b.AvailableBalance)) + balanceTotal := fixedpoint.Must(fixedpoint.NewFromString(b.Balance)) + unrealizedPnl := fixedpoint.Must(fixedpoint.NewFromString(b.CrossUnPnl)) + balances[b.Asset] = types.Balance{ + Currency: b.Asset, + Available: balanceAvailable, + Locked: balanceTotal.Sub(balanceAvailable.Sub(unrealizedPnl)), + } + } + + a := &types.Account{ + AccountType: types.AccountTypeFutures, + FuturesInfo: toGlobalFuturesAccountInfo(account), // In binance GO api, Account define account info which mantain []*AccountAsset and []*AccountPosition. + CanDeposit: account.CanDeposit, // if can transfer in asset + CanTrade: account.CanTrade, // if can trade + CanWithdraw: account.CanWithdraw, // if can transfer out asset + } + a.UpdateBalances(balances) + return a, nil +} + +func (e *Exchange) cancelFuturesOrders(ctx context.Context, orders ...types.Order) (err error) { + for _, o := range orders { + var req = e.futuresClient.NewCancelOrderService() + + // Mandatory + req.Symbol(o.Symbol) + + if o.OrderID > 0 { + req.OrderID(int64(o.OrderID)) + } else { + err = multierr.Append(err, types.NewOrderError( + fmt.Errorf("can not cancel %s order, order does not contain orderID or clientOrderID", o.Symbol), + o)) + continue + } + + _, err2 := req.Do(ctx) + if err2 != nil { + err = multierr.Append(err, types.NewOrderError(err2, o)) + } + } + + return err +} + +func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { + orderType, err := toLocalFuturesOrderType(order.Type) + if err != nil { + return nil, err + } + + req := e.futuresClient.NewCreateOrderService(). + Symbol(order.Symbol). + Type(orderType). + Side(futures.SideType(order.Side)). + ReduceOnly(order.ReduceOnly) + + clientOrderID := newFuturesClientOrderID(order.ClientOrderID) + if len(clientOrderID) > 0 { + req.NewClientOrderID(clientOrderID) + } + + // use response result format + req.NewOrderResponseType(futures.NewOrderRespTypeRESULT) + + if order.Market.Symbol != "" { + req.Quantity(order.Market.FormatQuantity(order.Quantity)) + } else { + // TODO report error + req.Quantity(order.Quantity.FormatString(8)) + } + + // set price field for limit orders + switch order.Type { + case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker: + if order.Market.Symbol != "" { + req.Price(order.Market.FormatPrice(order.Price)) + } else { + // TODO report error + req.Price(order.Price.FormatString(8)) + } + } + + // set stop price + switch order.Type { + + case types.OrderTypeStopLimit, types.OrderTypeStopMarket: + if order.Market.Symbol != "" { + req.StopPrice(order.Market.FormatPrice(order.StopPrice)) + } else { + // TODO report error + req.StopPrice(order.StopPrice.FormatString(8)) + } + } + + // could be IOC or FOK + if len(order.TimeInForce) > 0 { + // TODO: check the TimeInForce value + req.TimeInForce(futures.TimeInForceType(order.TimeInForce)) + } else { + switch order.Type { + case types.OrderTypeLimit, types.OrderTypeLimitMaker, types.OrderTypeStopLimit: + req.TimeInForce(futures.TimeInForceTypeGTC) + } + } + + response, err := req.Do(ctx) + if err != nil { + return nil, err + } + + log.Infof("futures order creation response: %+v", response) + + createdOrder, err := toGlobalFuturesOrder(&futures.Order{ + Symbol: response.Symbol, + OrderID: response.OrderID, + ClientOrderID: response.ClientOrderID, + Price: response.Price, + OrigQuantity: response.OrigQuantity, + ExecutedQuantity: response.ExecutedQuantity, + Status: response.Status, + TimeInForce: response.TimeInForce, + Type: response.Type, + Side: response.Side, + ReduceOnly: response.ReduceOnly, + }, false) + + return createdOrder, err +} + +func (e *Exchange) QueryFuturesKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { + + var limit = 1000 + if options.Limit > 0 { + // default limit == 1000 + limit = options.Limit + } + + log.Infof("querying kline %s %s %v", symbol, interval, options) + + req := e.futuresClient.NewKlinesService(). + Symbol(symbol). + Interval(string(interval)). + Limit(limit) + + if options.StartTime != nil { + req.StartTime(options.StartTime.UnixMilli()) + } + + if options.EndTime != nil { + req.EndTime(options.EndTime.UnixMilli()) + } + + resp, err := req.Do(ctx) + if err != nil { + return nil, err + } + + var kLines []types.KLine + for _, k := range resp { + kLines = append(kLines, types.KLine{ + Exchange: types.ExchangeBinance, + Symbol: symbol, + Interval: interval, + StartTime: types.NewTimeFromUnix(0, k.OpenTime*int64(time.Millisecond)), + EndTime: types.NewTimeFromUnix(0, k.CloseTime*int64(time.Millisecond)), + Open: fixedpoint.MustNewFromString(k.Open), + Close: fixedpoint.MustNewFromString(k.Close), + High: fixedpoint.MustNewFromString(k.High), + Low: fixedpoint.MustNewFromString(k.Low), + Volume: fixedpoint.MustNewFromString(k.Volume), + QuoteVolume: fixedpoint.MustNewFromString(k.QuoteAssetVolume), + TakerBuyBaseAssetVolume: fixedpoint.MustNewFromString(k.TakerBuyBaseAssetVolume), + TakerBuyQuoteAssetVolume: fixedpoint.MustNewFromString(k.TakerBuyQuoteAssetVolume), + LastTradeID: 0, + NumberOfTrades: uint64(k.TradeNum), + Closed: true, + }) + } + + kLines = types.SortKLinesAscending(kLines) + return kLines, nil +} + +func (e *Exchange) queryFuturesTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { + + var remoteTrades []*futures.AccountTrade + req := e.futuresClient.NewListAccountTradeService(). + Symbol(symbol) + if options.Limit > 0 { + req.Limit(int(options.Limit)) + } else { + req.Limit(1000) + } + + // BINANCE uses inclusive last trade ID + if options.LastTradeID > 0 { + req.FromID(int64(options.LastTradeID)) + } + + // The parameter fromId cannot be sent with startTime or endTime. + // Mentioned in binance futures docs + if options.LastTradeID <= 0 { + if options.StartTime != nil && options.EndTime != nil { + if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { + req.StartTime(options.StartTime.UnixMilli()) + req.EndTime(options.EndTime.UnixMilli()) + } else { + req.StartTime(options.StartTime.UnixMilli()) + } + } else if options.EndTime != nil { + req.EndTime(options.EndTime.UnixMilli()) + } + } + + remoteTrades, err = req.Do(ctx) + if err != nil { + return nil, err + } + for _, t := range remoteTrades { + localTrade, err := toGlobalFuturesTrade(*t) + if err != nil { + log.WithError(err).Errorf("can not convert binance futures trade: %+v", t) + continue + } + + trades = append(trades, *localTrade) + } + + trades = types.SortTradesAscending(trades) + return trades, nil +} From ed4d32c59ada3073f899a2be54e69e3946440897 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 18:06:40 +0800 Subject: [PATCH 0665/1392] binance: refactor binance exchange code for futures api --- pkg/exchange/binance/exchange.go | 87 ++++++++++---------------------- pkg/exchange/binance/futures.go | 50 ++++++++++++++++++ 2 files changed, 76 insertions(+), 61 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 96eb12450e..89ff369bdf 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -787,22 +787,7 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, } if e.IsFutures { - req := e.futuresClient.NewListOrdersService().Symbol(symbol) - - if lastOrderID > 0 { - req.OrderID(int64(lastOrderID)) - } else { - req.StartTime(since.UnixNano() / int64(time.Millisecond)) - if until.Sub(since) < 24*time.Hour { - req.EndTime(until.UnixNano() / int64(time.Millisecond)) - } - } - - binanceOrders, err := req.Do(ctx) - if err != nil { - return orders, err - } - return toGlobalFuturesOrders(binanceOrders, false) + return e.queryFuturesClosedOrders(ctx, symbol, since, until, lastOrderID) } // If orderId is set, it will get orders >= that orderId. Otherwise most recent orders are returned. @@ -1012,36 +997,6 @@ func newSpotClientOrderID(originalID string) (clientOrderID string) { return clientOrderID } -// BBGO is a futures broker on Binance -const futuresBrokerID = "gBhMvywy" - -func newFuturesClientOrderID(originalID string) (clientOrderID string) { - if originalID == types.NoClientOrderID { - return "" - } - - prefix := "x-" + futuresBrokerID - prefixLen := len(prefix) - - if originalID != "" { - // try to keep the whole original client order ID if user specifies it. - if prefixLen+len(originalID) > 32 { - return originalID - } - - clientOrderID = prefix + originalID - return clientOrderID - } - - clientOrderID = uuid.New().String() - clientOrderID = prefix + clientOrderID - if len(clientOrderID) > 32 { - return clientOrderID[0:32] - } - - return clientOrderID -} - func (e *Exchange) submitSpotOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { orderType, err := toLocalOrderType(order.Type) if err != nil { @@ -1326,26 +1281,36 @@ func (e *Exchange) DefaultFeeRates() types.ExchangeFee { } } +func (e *Exchange) queryFuturesDepth(ctx context.Context, symbol string) (snapshot types.SliceOrderBook, finalUpdateID int64, err error) { + res, err := e.futuresClient.NewDepthService().Symbol(symbol).Do(ctx) + if err != nil { + return snapshot, finalUpdateID, err + } + + response := &binance.DepthResponse{ + LastUpdateID: res.LastUpdateID, + Bids: res.Bids, + Asks: res.Asks, + } + + return convertDepth(snapshot, symbol, finalUpdateID, response) +} + // QueryDepth query the order book depth of a symbol func (e *Exchange) QueryDepth(ctx context.Context, symbol string) (snapshot types.SliceOrderBook, finalUpdateID int64, err error) { - var response *binance.DepthResponse if e.IsFutures { - res, err := e.futuresClient.NewDepthService().Symbol(symbol).Do(ctx) - if err != nil { - return snapshot, finalUpdateID, err - } - response = &binance.DepthResponse{ - LastUpdateID: res.LastUpdateID, - Bids: res.Bids, - Asks: res.Asks, - } - } else { - response, err = e.client.NewDepthService().Symbol(symbol).Do(ctx) - if err != nil { - return snapshot, finalUpdateID, err - } + return e.queryFuturesDepth(ctx, symbol) } + response, err := e.client.NewDepthService().Symbol(symbol).Do(ctx) + if err != nil { + return snapshot, finalUpdateID, err + } + + return convertDepth(snapshot, symbol, finalUpdateID, response) +} + +func convertDepth(snapshot types.SliceOrderBook, symbol string, finalUpdateID int64, response *binance.DepthResponse) (types.SliceOrderBook, int64, error) { snapshot.Symbol = symbol finalUpdateID = response.LastUpdateID for _, entry := range response.Bids { diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index 0f72573877..9fdc434763 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -6,6 +6,7 @@ import ( "time" "github.com/adshao/go-binance/v2/futures" + "github.com/google/uuid" "go.uber.org/multierr" "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" @@ -13,6 +14,25 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +func (e *Exchange) queryFuturesClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) { + req := e.futuresClient.NewListOrdersService().Symbol(symbol) + + if lastOrderID > 0 { + req.OrderID(int64(lastOrderID)) + } else { + req.StartTime(since.UnixNano() / int64(time.Millisecond)) + if until.Sub(since) < 24*time.Hour { + req.EndTime(until.UnixNano() / int64(time.Millisecond)) + } + } + + binanceOrders, err := req.Do(ctx) + if err != nil { + return orders, err + } + return toGlobalFuturesOrders(binanceOrders, false) +} + func (e *Exchange) TransferFuturesAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { req := e.client2.NewFuturesTransferRequest() req.Asset(asset) @@ -286,3 +306,33 @@ func (e *Exchange) queryFuturesTrades(ctx context.Context, symbol string, option trades = types.SortTradesAscending(trades) return trades, nil } + +// BBGO is a futures broker on Binance +const futuresBrokerID = "gBhMvywy" + +func newFuturesClientOrderID(originalID string) (clientOrderID string) { + if originalID == types.NoClientOrderID { + return "" + } + + prefix := "x-" + futuresBrokerID + prefixLen := len(prefix) + + if originalID != "" { + // try to keep the whole original client order ID if user specifies it. + if prefixLen+len(originalID) > 32 { + return originalID + } + + clientOrderID = prefix + originalID + return clientOrderID + } + + clientOrderID = uuid.New().String() + clientOrderID = prefix + clientOrderID + if len(clientOrderID) > 32 { + return clientOrderID[0:32] + } + + return clientOrderID +} From d359464b2c4b9d7b4e88678094b7ae934c4bfb61 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 18:14:24 +0800 Subject: [PATCH 0666/1392] binance: add default futures fee rate --- pkg/exchange/binance/exchange.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 89ff369bdf..777458be57 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -1275,9 +1275,16 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type // DefaultFeeRates returns the Binance VIP 0 fee schedule // See also https://www.binance.com/en/fee/schedule func (e *Exchange) DefaultFeeRates() types.ExchangeFee { + if e.IsFutures { + return types.ExchangeFee{ + MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.0180), // 0.0180% -USDT with BNB + TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.0360), // 0.0360% -USDT with BNB + } + } + return types.ExchangeFee{ - MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.075), // 0.075% - TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.075), // 0.075% + MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.075), // 0.075% with BNB + TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.075), // 0.075% with BNB } } From cfe47cd53b8efb1d0b1172fa27c4577a20d85605 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 18:14:52 +0800 Subject: [PATCH 0667/1392] binance: add futures fee link --- pkg/exchange/binance/exchange.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 777458be57..32693cb312 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -1274,6 +1274,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type // DefaultFeeRates returns the Binance VIP 0 fee schedule // See also https://www.binance.com/en/fee/schedule +// See futures fee at: https://www.binance.com/en/fee/futureFee func (e *Exchange) DefaultFeeRates() types.ExchangeFee { if e.IsFutures { return types.ExchangeFee{ From 8919bdd21283ee470e9160b0e496da550e305a3f Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 18:36:32 +0800 Subject: [PATCH 0668/1392] update go-binance package --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 10a84c69f6..9e11b06f71 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ go 1.17 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Masterminds/squirrel v1.5.3 - github.com/adshao/go-binance/v2 v2.3.10 + github.com/adshao/go-binance/v2 v2.4.1 github.com/c-bata/goptuna v0.8.1 github.com/c9s/requestgen v1.3.0 github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b diff --git a/go.sum b/go.sum index 294026040a..a925fed026 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,8 @@ github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdc github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/adshao/go-binance/v2 v2.3.10 h1:iWtHD/sQ8GK6r+cSMMdOynpGI/4Q6P5LZtiEHdWOjag= github.com/adshao/go-binance/v2 v2.3.10/go.mod h1:Z3MCnWI0gHC4Rea8TWiF3aN1t4nV9z3CaU/TeHcKsLM= +github.com/adshao/go-binance/v2 v2.4.1 h1:fOZ2tCbN7sgDZvvsawUMjhsOoe40X87JVE4DklIyyyc= +github.com/adshao/go-binance/v2 v2.4.1/go.mod h1:6Qoh+CYcj8U43h4HgT6mqJnsGj4mWZKA/nsj8LN8ZTU= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= From 4bbcb9553d40a37f3106455b8fbc4fa0c6f6aec0 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 18:36:54 +0800 Subject: [PATCH 0669/1392] binanceapi: add FuturesGetPositionRisksRequest --- .../binance/binanceapi/futures_client.go | 33 +++++++++++++++++++ .../futures_get_position_risks_request.go | 17 ++++++++++ 2 files changed, 50 insertions(+) create mode 100644 pkg/exchange/binance/binanceapi/futures_client.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go diff --git a/pkg/exchange/binance/binanceapi/futures_client.go b/pkg/exchange/binance/binanceapi/futures_client.go new file mode 100644 index 0000000000..87155ada42 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_client.go @@ -0,0 +1,33 @@ +package binanceapi + +import ( + "net/url" + + "github.com/c9s/requestgen" +) + +type FuturesRestClient struct { + RestClient +} + +const FuturesRestBaseURL = "https://fapi.binance.com" + +func NewFuturesRestClient(baseURL string) *FuturesRestClient { + if len(baseURL) == 0 { + baseURL = FuturesRestBaseURL + } + + u, err := url.Parse(baseURL) + if err != nil { + panic(err) + } + + return &FuturesRestClient{ + RestClient: RestClient{ + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: DefaultHttpClient, + }, + }, + } +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go b/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go new file mode 100644 index 0000000000..0614420a3a --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go @@ -0,0 +1,17 @@ +package binanceapi + +import "github.com/c9s/requestgen" + +type FuturesPositionRisksResponse struct { +} + +//go:generate requestgen -method GET -url "/fapi/v2/positionRisk" -type FuturesGetPositionRisksRequest -responseType .FuturesPositionRisksResponse +type FuturesGetPositionRisksRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` +} + +func (c *FuturesRestClient) NewGetPositionRisksRequest() *FuturesGetPositionRisksRequest { + return &FuturesGetPositionRisksRequest{client: c} +} From b9d60d8edb42013f5e0419188fa8fc5d298cfee7 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 18:37:04 +0800 Subject: [PATCH 0670/1392] binance: add QueryFuturesPositionRisks method --- pkg/exchange/binance/futures.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index 9fdc434763..ec5523ae25 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -307,6 +307,19 @@ func (e *Exchange) queryFuturesTrades(ctx context.Context, symbol string, option return trades, nil } +func (e *Exchange) QueryFuturesPositionRisks(ctx context.Context, symbol string) error { + req := e.futuresClient.NewGetPositionRiskService() + req.Symbol(symbol) + res, err := req.Do(ctx) + if err != nil { + return err + } + + _ = res + + return nil +} + // BBGO is a futures broker on Binance const futuresBrokerID = "gBhMvywy" From b41f8a8355113681a59ac558ffc9b2a61e67c334 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 22:35:22 +0800 Subject: [PATCH 0671/1392] binance: add FuturesGetIncomeHistoryRequest api support --- .../futures_get_income_history_request.go | 58 +++++ ...s_get_income_history_request_requestgen.go | 212 ++++++++++++++++++ .../futures_get_position_risks_request.go | 19 +- ...s_get_position_risks_request_requestgen.go | 148 ++++++++++++ 4 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 pkg/exchange/binance/binanceapi/futures_get_income_history_request.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_income_history_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_position_risks_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go b/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go new file mode 100644 index 0000000000..544760ac66 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go @@ -0,0 +1,58 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// FuturesIncomeType can be one of the following value: +// TRANSFER, WELCOME_BONUS, REALIZED_PNL, FUNDING_FEE, COMMISSION, INSURANCE_CLEAR, REFERRAL_KICKBACK, COMMISSION_REBATE, +// API_REBATE, CONTEST_REWARD, CROSS_COLLATERAL_TRANSFER, OPTIONS_PREMIUM_FEE, +// OPTIONS_SETTLE_PROFIT, INTERNAL_TRANSFER, AUTO_EXCHANGE, +// DELIVERED_SETTELMENT, COIN_SWAP_DEPOSIT, COIN_SWAP_WITHDRAW, POSITION_LIMIT_INCREASE_FEE +type FuturesIncomeType string + +const ( + FuturesIncomeTransfer FuturesIncomeType = "TRANSFER" + FuturesIncomeWelcomeBonus FuturesIncomeType = "WELCOME_BONUS" + FuturesIncomeFundingFee FuturesIncomeType = "FUNDING_FEE" + FuturesIncomeRealizedPnL FuturesIncomeType = "REALIZED_PNL" + FuturesIncomeCommission FuturesIncomeType = "COMMISSION" + FuturesIncomeReferralKickback FuturesIncomeType = "REFERRAL_KICKBACK" + FuturesIncomeCommissionRebate FuturesIncomeType = "COMMISSION_REBATE" + FuturesIncomeApiRebate FuturesIncomeType = "API_REBATE" + FuturesIncomeContestReward FuturesIncomeType = "CONTEST_REWARD" +) + +type FuturesIncome struct { + Symbol string `json:"symbol"` + IncomeType FuturesIncomeType `json:"incomeType"` + Income fixedpoint.Value `json:"income"` + Asset string `json:"asset"` + Info string `json:"info"` + Time types.MillisecondTimestamp `json:"time"` + TranId string `json:"tranId"` + TradeId string `json:"tradeId"` +} + +//go:generate requestgen -method GET -url "/fapi/v2/positionRisk" -type FuturesGetIncomeHistoryRequest -responseType []FuturesIncome +type FuturesGetIncomeHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` + + incomeType FuturesIncomeType `param:"incomeType"` + + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + + limit *uint64 `param:"limit"` +} + +func (c *FuturesRestClient) NewFuturesGetIncomeHistoryRequest() *FuturesGetIncomeHistoryRequest { + return &FuturesGetIncomeHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_income_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_get_income_history_request_requestgen.go new file mode 100644 index 0000000000..9ff3468a4d --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_income_history_request_requestgen.go @@ -0,0 +1,212 @@ +// Code generated by "requestgen -method GET -url /fapi/v2/positionRisk -type FuturesGetIncomeHistoryRequest -responseType []FuturesIncome"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (f *FuturesGetIncomeHistoryRequest) Symbol(symbol string) *FuturesGetIncomeHistoryRequest { + f.symbol = symbol + return f +} + +func (f *FuturesGetIncomeHistoryRequest) IncomeType(incomeType FuturesIncomeType) *FuturesGetIncomeHistoryRequest { + f.incomeType = incomeType + return f +} + +func (f *FuturesGetIncomeHistoryRequest) StartTime(startTime time.Time) *FuturesGetIncomeHistoryRequest { + f.startTime = &startTime + return f +} + +func (f *FuturesGetIncomeHistoryRequest) EndTime(endTime time.Time) *FuturesGetIncomeHistoryRequest { + f.endTime = &endTime + return f +} + +func (f *FuturesGetIncomeHistoryRequest) Limit(limit uint64) *FuturesGetIncomeHistoryRequest { + f.limit = &limit + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGetIncomeHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGetIncomeHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := f.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check incomeType field -> json key incomeType + incomeType := f.incomeType + + // TEMPLATE check-valid-values + switch incomeType { + case FuturesIncomeTransfer, FuturesIncomeWelcomeBonus, FuturesIncomeFundingFee, FuturesIncomeRealizedPnL, FuturesIncomeCommission, FuturesIncomeReferralKickback, FuturesIncomeCommissionRebate, FuturesIncomeApiRebate, FuturesIncomeContestReward: + params["incomeType"] = incomeType + + default: + return nil, fmt.Errorf("incomeType value %v is invalid", incomeType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of incomeType + params["incomeType"] = incomeType + // check startTime field -> json key startTime + if f.startTime != nil { + startTime := *f.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if f.endTime != nil { + endTime := *f.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if f.limit != nil { + limit := *f.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGetIncomeHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGetIncomeHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGetIncomeHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGetIncomeHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGetIncomeHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGetIncomeHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGetIncomeHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesGetIncomeHistoryRequest) Do(ctx context.Context) ([]FuturesIncome, error) { + + // empty params for GET operation + var params interface{} + query, err := f.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/fapi/v2/positionRisk" + + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []FuturesIncome + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go b/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go index 0614420a3a..83459a8f97 100644 --- a/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go +++ b/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go @@ -2,10 +2,25 @@ package binanceapi import "github.com/c9s/requestgen" -type FuturesPositionRisksResponse struct { +type FuturesPositionRisk struct { + EntryPrice string `json:"entryPrice"` + MarginType string `json:"marginType"` + IsAutoAddMargin string `json:"isAutoAddMargin"` + IsolatedMargin string `json:"isolatedMargin"` + Leverage string `json:"leverage"` + LiquidationPrice string `json:"liquidationPrice"` + MarkPrice string `json:"markPrice"` + MaxNotionalValue string `json:"maxNotionalValue"` + PositionAmt string `json:"positionAmt"` + Notional string `json:"notional"` + IsolatedWallet string `json:"isolatedWallet"` + Symbol string `json:"symbol"` + UnRealizedProfit string `json:"unRealizedProfit"` + PositionSide string `json:"positionSide"` + UpdateTime int `json:"updateTime"` } -//go:generate requestgen -method GET -url "/fapi/v2/positionRisk" -type FuturesGetPositionRisksRequest -responseType .FuturesPositionRisksResponse +//go:generate requestgen -method GET -url "/fapi/v2/positionRisk" -type FuturesGetPositionRisksRequest -responseType []FuturesPositionRisk type FuturesGetPositionRisksRequest struct { client requestgen.AuthenticatedAPIClient diff --git a/pkg/exchange/binance/binanceapi/futures_get_position_risks_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_get_position_risks_request_requestgen.go new file mode 100644 index 0000000000..f17d53dd99 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_position_risks_request_requestgen.go @@ -0,0 +1,148 @@ +// Code generated by "requestgen -method GET -url /fapi/v2/positionRisk -type FuturesGetPositionRisksRequest -responseType []FuturesPositionRisk"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (f *FuturesGetPositionRisksRequest) Symbol(symbol string) *FuturesGetPositionRisksRequest { + f.symbol = symbol + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGetPositionRisksRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGetPositionRisksRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := f.symbol + + // assign parameter of symbol + params["symbol"] = symbol + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGetPositionRisksRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGetPositionRisksRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGetPositionRisksRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGetPositionRisksRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGetPositionRisksRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGetPositionRisksRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGetPositionRisksRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesGetPositionRisksRequest) Do(ctx context.Context) ([]FuturesPositionRisk, error) { + + // empty params for GET operation + var params interface{} + query, err := f.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/fapi/v2/positionRisk" + + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []FuturesPositionRisk + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} From d05cbca6524b29b2496e19c7f9478a306fb77c6f Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 25 Mar 2023 02:09:42 +0800 Subject: [PATCH 0672/1392] binance: fix income history request path --- .../binance/binanceapi/futures_get_income_history_request.go | 2 +- .../futures_get_income_history_request_requestgen.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go b/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go index 544760ac66..aeacdf8890 100644 --- a/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go +++ b/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go @@ -39,7 +39,7 @@ type FuturesIncome struct { TradeId string `json:"tradeId"` } -//go:generate requestgen -method GET -url "/fapi/v2/positionRisk" -type FuturesGetIncomeHistoryRequest -responseType []FuturesIncome +//go:generate requestgen -method GET -url "/fapi/v1/income" -type FuturesGetIncomeHistoryRequest -responseType []FuturesIncome type FuturesGetIncomeHistoryRequest struct { client requestgen.AuthenticatedAPIClient diff --git a/pkg/exchange/binance/binanceapi/futures_get_income_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_get_income_history_request_requestgen.go index 9ff3468a4d..177142773e 100644 --- a/pkg/exchange/binance/binanceapi/futures_get_income_history_request_requestgen.go +++ b/pkg/exchange/binance/binanceapi/futures_get_income_history_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method GET -url /fapi/v2/positionRisk -type FuturesGetIncomeHistoryRequest -responseType []FuturesIncome"; DO NOT EDIT. +// Code generated by "requestgen -method GET -url /fapi/v1/income -type FuturesGetIncomeHistoryRequest -responseType []FuturesIncome"; DO NOT EDIT. package binanceapi @@ -192,7 +192,7 @@ func (f *FuturesGetIncomeHistoryRequest) Do(ctx context.Context) ([]FuturesIncom return nil, err } - apiURL := "/fapi/v2/positionRisk" + apiURL := "/fapi/v1/income" req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) if err != nil { From 281f09ff42e3595682110608d90594653ced53ee Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 25 Mar 2023 02:25:09 +0800 Subject: [PATCH 0673/1392] binance: fix futures user data stream AccountUpdateEvent parsing --- pkg/exchange/binance/parse.go | 56 ++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index edbd2504cb..2af88ebb2f 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -891,16 +891,58 @@ func (e *OrderTradeUpdateEvent) TradeFutures() (*types.Trade, error) { }, nil } -type AccountUpdate struct { - EventReasonType string `json:"m"` - Balances []*futures.Balance `json:"B,omitempty"` - Positions []*futures.AccountPosition `json:"P,omitempty"` -} +type FuturesStreamBalance struct { + Asset string `json:"a"` + WalletBalance fixedpoint.Value `json:"wb"` + CrossWalletBalance fixedpoint.Value `json:"cw"` + BalanceChange fixedpoint.Value `json:"bc"` +} + +type FuturesStreamPosition struct { + Symbol string `json:"s"` + PositionAmount fixedpoint.Value `json:"pa"` + EntryPrice fixedpoint.Value `json:"ep"` + AccumulatedRealizedPnL fixedpoint.Value `json:"cr"` // (Pre-fee) Accumulated Realized PnL + UnrealizedPnL fixedpoint.Value `json:"up"` + MarginType string `json:"mt"` + IsolatedWallet fixedpoint.Value `json:"iw"` + PositionSide string `json:"ps"` +} + +type AccountUpdateEventReasonType string + +const ( + AccountUpdateEventReasonDeposit AccountUpdateEventReasonType = "DEPOSIT" + AccountUpdateEventReasonWithdraw AccountUpdateEventReasonType = "WITHDRAW" + AccountUpdateEventReasonOrder AccountUpdateEventReasonType = "ORDER" + AccountUpdateEventReasonFundingFee AccountUpdateEventReasonType = "FUNDING_FEE" + AccountUpdateEventReasonMarginTransfer AccountUpdateEventReasonType = "MARGIN_TRANSFER" + AccountUpdateEventReasonMarginTypeChange AccountUpdateEventReasonType = "MARGIN_TYPE_CHANGE" + AccountUpdateEventReasonAssetTransfer AccountUpdateEventReasonType = "ASSET_TRANSFER" + AccountUpdateEventReasonAdminDeposit AccountUpdateEventReasonType = "ADMIN_DEPOSIT" + AccountUpdateEventReasonAdminWithdraw AccountUpdateEventReasonType = "ADMIN_WITHDRAW" +) +type AccountUpdate struct { + // m: DEPOSIT WITHDRAW + // ORDER FUNDING_FEE + // WITHDRAW_REJECT ADJUSTMENT + // INSURANCE_CLEAR + // ADMIN_DEPOSIT ADMIN_WITHDRAW + // MARGIN_TRANSFER MARGIN_TYPE_CHANGE + // ASSET_TRANSFER + // OPTIONS_PREMIUM_FEE OPTIONS_SETTLE_PROFIT + // AUTO_EXCHANGE + // COIN_SWAP_DEPOSIT COIN_SWAP_WITHDRAW + EventReasonType AccountUpdateEventReasonType `json:"m"` + Balances []FuturesStreamBalance `json:"B,omitempty"` + Positions []FuturesStreamPosition `json:"P,omitempty"` +} + +// AccountUpdateEvent is only used in the futures user data stream type AccountUpdateEvent struct { EventBase - Transaction int64 `json:"T"` - + Transaction int64 `json:"T"` AccountUpdate AccountUpdate `json:"a"` } From f34c72eba0d5191be43ff94f9186883f2b843eb2 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 25 Mar 2023 02:39:44 +0800 Subject: [PATCH 0674/1392] binance: add margin call event support --- pkg/exchange/binance/parse.go | 82 ++++++++++++++++------- pkg/exchange/binance/stream.go | 39 +++++------ pkg/exchange/binance/stream_callbacks.go | 84 ++++++++++++++---------- 3 files changed, 124 insertions(+), 81 deletions(-) diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index 2af88ebb2f..074eec74a9 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -15,6 +15,11 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +type EventBase struct { + Event string `json:"e"` // event name + Time int64 `json:"E"` // event time +} + /* executionReport @@ -314,22 +319,40 @@ func parseWebSocketEvent(message []byte) (interface{}, error) { case "depthUpdate": return parseDepthEvent(val) - case "markPriceUpdate": - var event MarkPriceUpdateEvent + case "listenKeyExpired": + var event ListenKeyExpired err = json.Unmarshal([]byte(message), &event) return &event, err - case "listenKeyExpired": - var event ListenKeyExpired + case "trade": + var event MarketTradeEvent err = json.Unmarshal([]byte(message), &event) return &event, err - // Binance futures data -------------- + case "aggTrade": + var event AggTradeEvent + err = json.Unmarshal([]byte(message), &event) + return &event, err + + } + + // futures stream + switch eventType { + + // futures market data stream + // ======================================================== case "continuousKline": var event ContinuousKLineEvent err = json.Unmarshal([]byte(message), &event) return &event, err + case "markPriceUpdate": + var event MarkPriceUpdateEvent + err = json.Unmarshal([]byte(message), &event) + return &event, err + + // futures user data stream + // ======================================================== case "ORDER_TRADE_UPDATE": var event OrderTradeUpdateEvent err = json.Unmarshal([]byte(message), &event) @@ -347,13 +370,8 @@ func parseWebSocketEvent(message []byte) (interface{}, error) { err = json.Unmarshal([]byte(message), &event) return &event, err - case "trade": - var event MarketTradeEvent - err = json.Unmarshal([]byte(message), &event) - return &event, err - - case "aggTrade": - var event AggTradeEvent + case "MARGIN_CALL": + var event MarginCallEvent err = json.Unmarshal([]byte(message), &event) return &event, err @@ -939,6 +957,22 @@ type AccountUpdate struct { Positions []FuturesStreamPosition `json:"P,omitempty"` } +type MarginCallEvent struct { + EventBase + + CrossWalletBalance fixedpoint.Value `json:"cw"` + P []struct { + Symbol string `json:"s"` + PositionSide string `json:"ps"` + PositionAmount fixedpoint.Value `json:"pa"` + MarginType string `json:"mt"` + IsolatedWallet fixedpoint.Value `json:"iw"` + MarkPrice fixedpoint.Value `json:"mp"` + UnrealizedPnL fixedpoint.Value `json:"up"` + MaintenanceMarginRequired fixedpoint.Value `json:"mm"` + } `json:"p"` // Position(s) of Margin Call +} + // AccountUpdateEvent is only used in the futures user data stream type AccountUpdateEvent struct { EventBase @@ -946,21 +980,23 @@ type AccountUpdateEvent struct { AccountUpdate AccountUpdate `json:"a"` } -type AccountConfig struct { - Symbol string `json:"s"` - Leverage fixedpoint.Value `json:"l"` -} - type AccountConfigUpdateEvent struct { EventBase Transaction int64 `json:"T"` - AccountConfig AccountConfig `json:"ac"` -} - -type EventBase struct { - Event string `json:"e"` // event - Time int64 `json:"E"` + // When the leverage of a trade pair changes, + // the payload will contain the object ac to represent the account configuration of the trade pair, + // where s represents the specific trade pair and l represents the leverage + AccountConfig struct { + Symbol string `json:"s"` + Leverage fixedpoint.Value `json:"l"` + } `json:"ac"` + + // When the user Multi-Assets margin mode changes the payload will contain the object ai representing the user account configuration, + // where j represents the user Multi-Assets margin mode + MarginModeConfig struct { + MultiAssetsMode bool `json:"j"` + } `json:"ai"` } type BookTickerEvent struct { diff --git a/pkg/exchange/binance/stream.go b/pkg/exchange/binance/stream.go index 2a1b39dfcb..9cf5d27b20 100644 --- a/pkg/exchange/binance/stream.go +++ b/pkg/exchange/binance/stream.go @@ -46,12 +46,8 @@ type Stream struct { kLineEventCallbacks []func(e *KLineEvent) kLineClosedEventCallbacks []func(e *KLineEvent) - markPriceUpdateEventCallbacks []func(e *MarkPriceUpdateEvent) - marketTradeEventCallbacks []func(e *MarketTradeEvent) - aggTradeEventCallbacks []func(e *AggTradeEvent) - - continuousKLineEventCallbacks []func(e *ContinuousKLineEvent) - continuousKLineClosedEventCallbacks []func(e *ContinuousKLineEvent) + marketTradeEventCallbacks []func(e *MarketTradeEvent) + aggTradeEventCallbacks []func(e *AggTradeEvent) balanceUpdateEventCallbacks []func(event *BalanceUpdateEvent) outboundAccountInfoEventCallbacks []func(event *OutboundAccountInfoEvent) @@ -59,12 +55,19 @@ type Stream struct { executionReportEventCallbacks []func(event *ExecutionReportEvent) bookTickerEventCallbacks []func(event *BookTickerEvent) + // futures market data stream + markPriceUpdateEventCallbacks []func(e *MarkPriceUpdateEvent) + continuousKLineEventCallbacks []func(e *ContinuousKLineEvent) + continuousKLineClosedEventCallbacks []func(e *ContinuousKLineEvent) + + // futures user data stream event callbacks orderTradeUpdateEventCallbacks []func(e *OrderTradeUpdateEvent) accountUpdateEventCallbacks []func(e *AccountUpdateEvent) accountConfigUpdateEventCallbacks []func(e *AccountConfigUpdateEvent) + marginCallEventCallbacks []func(e *MarginCallEvent) + listenKeyExpiredCallbacks []func(e *ListenKeyExpired) - listenKeyExpiredCallbacks []func(e *ListenKeyExpired) - + // depthBuffers is used for storing the depth info depthBuffers map[string]*depth.Buffer } @@ -123,10 +126,12 @@ func NewStream(ex *Exchange, client *binance.Client, futuresClient *futures.Clie stream.OnMarketTradeEvent(stream.handleMarketTradeEvent) stream.OnAggTradeEvent(stream.handleAggTradeEvent) + // Futures User Data Stream + // =================================== // Event type ACCOUNT_UPDATE from user data stream updates Balance and FuturesPosition. - stream.OnAccountUpdateEvent(stream.handleAccountUpdateEvent) - stream.OnAccountConfigUpdateEvent(stream.handleAccountConfigUpdateEvent) stream.OnOrderTradeUpdateEvent(stream.handleOrderTradeUpdateEvent) + // =================================== + stream.OnDisconnect(stream.handleDisconnect) stream.OnConnect(stream.handleConnect) stream.OnListenKeyExpired(func(e *ListenKeyExpired) { @@ -246,18 +251,6 @@ func (s *Stream) handleOutboundAccountPositionEvent(e *OutboundAccountPositionEv s.EmitBalanceSnapshot(snapshot) } -func (s *Stream) handleAccountUpdateEvent(e *AccountUpdateEvent) { - futuresPositionSnapshot := toGlobalFuturesPositions(e.AccountUpdate.Positions) - s.EmitFuturesPositionSnapshot(futuresPositionSnapshot) - - balanceSnapshot := toGlobalFuturesBalance(e.AccountUpdate.Balances) - s.EmitBalanceSnapshot(balanceSnapshot) -} - -// TODO: emit account config leverage updates -func (s *Stream) handleAccountConfigUpdateEvent(e *AccountConfigUpdateEvent) { -} - func (s *Stream) handleOrderTradeUpdateEvent(e *OrderTradeUpdateEvent) { switch e.OrderTrade.CurrentExecutionType { @@ -381,6 +374,8 @@ func (s *Stream) dispatchEvent(e interface{}) { case *ListenKeyExpired: s.EmitListenKeyExpired(e) + case *MarginCallEvent: + } } diff --git a/pkg/exchange/binance/stream_callbacks.go b/pkg/exchange/binance/stream_callbacks.go index d335ccff99..a90b4f668c 100644 --- a/pkg/exchange/binance/stream_callbacks.go +++ b/pkg/exchange/binance/stream_callbacks.go @@ -34,16 +34,6 @@ func (s *Stream) EmitKLineClosedEvent(e *KLineEvent) { } } -func (s *Stream) OnMarkPriceUpdateEvent(cb func(e *MarkPriceUpdateEvent)) { - s.markPriceUpdateEventCallbacks = append(s.markPriceUpdateEventCallbacks, cb) -} - -func (s *Stream) EmitMarkPriceUpdateEvent(e *MarkPriceUpdateEvent) { - for _, cb := range s.markPriceUpdateEventCallbacks { - cb(e) - } -} - func (s *Stream) OnMarketTradeEvent(cb func(e *MarketTradeEvent)) { s.marketTradeEventCallbacks = append(s.marketTradeEventCallbacks, cb) } @@ -64,26 +54,6 @@ func (s *Stream) EmitAggTradeEvent(e *AggTradeEvent) { } } -func (s *Stream) OnContinuousKLineEvent(cb func(e *ContinuousKLineEvent)) { - s.continuousKLineEventCallbacks = append(s.continuousKLineEventCallbacks, cb) -} - -func (s *Stream) EmitContinuousKLineEvent(e *ContinuousKLineEvent) { - for _, cb := range s.continuousKLineEventCallbacks { - cb(e) - } -} - -func (s *Stream) OnContinuousKLineClosedEvent(cb func(e *ContinuousKLineEvent)) { - s.continuousKLineClosedEventCallbacks = append(s.continuousKLineClosedEventCallbacks, cb) -} - -func (s *Stream) EmitContinuousKLineClosedEvent(e *ContinuousKLineEvent) { - for _, cb := range s.continuousKLineClosedEventCallbacks { - cb(e) - } -} - func (s *Stream) OnBalanceUpdateEvent(cb func(event *BalanceUpdateEvent)) { s.balanceUpdateEventCallbacks = append(s.balanceUpdateEventCallbacks, cb) } @@ -134,6 +104,36 @@ func (s *Stream) EmitBookTickerEvent(event *BookTickerEvent) { } } +func (s *Stream) OnMarkPriceUpdateEvent(cb func(e *MarkPriceUpdateEvent)) { + s.markPriceUpdateEventCallbacks = append(s.markPriceUpdateEventCallbacks, cb) +} + +func (s *Stream) EmitMarkPriceUpdateEvent(e *MarkPriceUpdateEvent) { + for _, cb := range s.markPriceUpdateEventCallbacks { + cb(e) + } +} + +func (s *Stream) OnContinuousKLineEvent(cb func(e *ContinuousKLineEvent)) { + s.continuousKLineEventCallbacks = append(s.continuousKLineEventCallbacks, cb) +} + +func (s *Stream) EmitContinuousKLineEvent(e *ContinuousKLineEvent) { + for _, cb := range s.continuousKLineEventCallbacks { + cb(e) + } +} + +func (s *Stream) OnContinuousKLineClosedEvent(cb func(e *ContinuousKLineEvent)) { + s.continuousKLineClosedEventCallbacks = append(s.continuousKLineClosedEventCallbacks, cb) +} + +func (s *Stream) EmitContinuousKLineClosedEvent(e *ContinuousKLineEvent) { + for _, cb := range s.continuousKLineClosedEventCallbacks { + cb(e) + } +} + func (s *Stream) OnOrderTradeUpdateEvent(cb func(e *OrderTradeUpdateEvent)) { s.orderTradeUpdateEventCallbacks = append(s.orderTradeUpdateEventCallbacks, cb) } @@ -164,6 +164,16 @@ func (s *Stream) EmitAccountConfigUpdateEvent(e *AccountConfigUpdateEvent) { } } +func (s *Stream) OnMarginCallEvent(cb func(e *MarginCallEvent)) { + s.marginCallEventCallbacks = append(s.marginCallEventCallbacks, cb) +} + +func (s *Stream) EmitMarginCallEvent(e *MarginCallEvent) { + for _, cb := range s.marginCallEventCallbacks { + cb(e) + } +} + func (s *Stream) OnListenKeyExpired(cb func(e *ListenKeyExpired)) { s.listenKeyExpiredCallbacks = append(s.listenKeyExpiredCallbacks, cb) } @@ -181,16 +191,10 @@ type StreamEventHub interface { OnKLineClosedEvent(cb func(e *KLineEvent)) - OnMarkPriceUpdateEvent(cb func(e *MarkPriceUpdateEvent)) - OnMarketTradeEvent(cb func(e *MarketTradeEvent)) OnAggTradeEvent(cb func(e *AggTradeEvent)) - OnContinuousKLineEvent(cb func(e *ContinuousKLineEvent)) - - OnContinuousKLineClosedEvent(cb func(e *ContinuousKLineEvent)) - OnBalanceUpdateEvent(cb func(event *BalanceUpdateEvent)) OnOutboundAccountInfoEvent(cb func(event *OutboundAccountInfoEvent)) @@ -201,11 +205,19 @@ type StreamEventHub interface { OnBookTickerEvent(cb func(event *BookTickerEvent)) + OnMarkPriceUpdateEvent(cb func(e *MarkPriceUpdateEvent)) + + OnContinuousKLineEvent(cb func(e *ContinuousKLineEvent)) + + OnContinuousKLineClosedEvent(cb func(e *ContinuousKLineEvent)) + OnOrderTradeUpdateEvent(cb func(e *OrderTradeUpdateEvent)) OnAccountUpdateEvent(cb func(e *AccountUpdateEvent)) OnAccountConfigUpdateEvent(cb func(e *AccountConfigUpdateEvent)) + OnMarginCallEvent(cb func(e *MarginCallEvent)) + OnListenKeyExpired(cb func(e *ListenKeyExpired)) } From 0f49f9fbe59701a10ef787ed43dd4733b3c5d6d9 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 25 Mar 2023 02:48:27 +0800 Subject: [PATCH 0675/1392] xfunding: add e.AccountUpdate.EventReasonType switch case --- pkg/strategy/xfunding/strategy.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 77e662d73e..78648cac2f 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -332,9 +332,22 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } }) - s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { - // s.queryAndDetectPremiumIndex(ctx, binanceFutures) - })) + // s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) {})) + + if binanceStream, ok := s.futuresSession.UserDataStream.(*binance.Stream); ok { + binanceStream.OnAccountUpdateEvent(func(e *binance.AccountUpdateEvent) { + log.Infof("onAccountUpdateEvent: %+v", e) + switch e.AccountUpdate.EventReasonType { + + case binance.AccountUpdateEventReasonDeposit: + + case binance.AccountUpdateEventReasonWithdraw: + + case binance.AccountUpdateEventReasonFundingFee: + + } + }) + } go func() { ticker := time.NewTicker(10 * time.Second) From 300506f9f956b6a90eb6ac234b4ccb324e0f2a78 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 25 Mar 2023 02:53:55 +0800 Subject: [PATCH 0676/1392] xfunding: fix critical section for usedQuoteInvestment --- pkg/strategy/xfunding/strategy.go | 45 ++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 78648cac2f..01f6f4b996 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -332,7 +332,9 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } }) - // s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) {})) + s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + s.queryAndDetectPremiumIndex(ctx, binanceFutures) + })) if binanceStream, ok := s.futuresSession.UserDataStream.(*binance.Stream); ok { binanceStream.OnAccountUpdateEvent(func(e *binance.AccountUpdateEvent) { @@ -359,17 +361,25 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return case <-ticker.C: - s.queryAndDetectPremiumIndex(ctx, binanceFutures) - s.sync(ctx) + s.syncSpotAccount(ctx) } } }() - // TODO: use go routine and time.Ticker to trigger spot sync and futures sync - /* - s.spotSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { - })) - */ + go func() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + s.syncFuturesAccount(ctx) + } + } + }() return nil } @@ -388,14 +398,21 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture } } -func (s *Strategy) sync(ctx context.Context) { +func (s *Strategy) syncSpotAccount(ctx context.Context) { switch s.getPositionState() { case PositionOpening: s.increaseSpotPosition(ctx) + case PositionClosing: + s.syncSpotPosition(ctx) + } +} + +func (s *Strategy) syncFuturesAccount(ctx context.Context) { + switch s.getPositionState() { + case PositionOpening: s.syncFuturesPosition(ctx) case PositionClosing: s.reduceFuturesPosition(ctx) - s.syncSpotPosition(ctx) } } @@ -635,8 +652,10 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { } s.mu.Lock() - defer s.mu.Unlock() - if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + usedQuoteInvestment := s.State.UsedQuoteInvestment + s.mu.Unlock() + + if usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { // stop increase the position s.setPositionState(PositionReady) @@ -653,7 +672,7 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { return } - leftQuota := s.QuoteInvestment.Sub(s.State.UsedQuoteInvestment) + leftQuota := s.QuoteInvestment.Sub(usedQuoteInvestment) orderPrice := ticker.Buy orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuota).Div(orderPrice) From 0c6e9496b31e9a6c5b6cfd74581b7a431ddef9b7 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 25 Mar 2023 02:56:45 +0800 Subject: [PATCH 0677/1392] xfunding: use early return --- pkg/strategy/xfunding/strategy.go | 38 +++++++++++++++++-------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 01f6f4b996..df8edfb01c 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -718,28 +718,32 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) bool { switch s.getPositionState() { case PositionClosed: - if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { - log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", - fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) - - s.startOpeningPosition(types.PositionShort, premiumIndex.Time) - return true + if fundingRate.Compare(s.ShortFundingRate.High) < 0 { + return false } + log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", + fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) + + s.startOpeningPosition(types.PositionShort, premiumIndex.Time) + return true + case PositionReady: - if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { - log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", - fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) - - holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) - if holdingPeriod < time.Duration(s.MinHoldingPeriod) { - log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod.Duration()) - return false - } + if fundingRate.Compare(s.ShortFundingRate.Low) > 0 { + return false + } + + log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", + fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) - s.startClosingPosition() - return true + holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) + if holdingPeriod < time.Duration(s.MinHoldingPeriod) { + log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod.Duration()) + return false } + + s.startClosingPosition() + return true } return false From 01799cfc4e47c1c52096427023862f48455837e5 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 25 Mar 2023 02:58:06 +0800 Subject: [PATCH 0678/1392] binanceapi: update FuturesPositionRisk field types --- .../futures_get_position_risks_request.go | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go b/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go index 83459a8f97..56efd19ce5 100644 --- a/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go +++ b/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go @@ -1,23 +1,28 @@ package binanceapi -import "github.com/c9s/requestgen" +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) type FuturesPositionRisk struct { - EntryPrice string `json:"entryPrice"` - MarginType string `json:"marginType"` - IsAutoAddMargin string `json:"isAutoAddMargin"` - IsolatedMargin string `json:"isolatedMargin"` - Leverage string `json:"leverage"` - LiquidationPrice string `json:"liquidationPrice"` - MarkPrice string `json:"markPrice"` - MaxNotionalValue string `json:"maxNotionalValue"` - PositionAmt string `json:"positionAmt"` - Notional string `json:"notional"` - IsolatedWallet string `json:"isolatedWallet"` - Symbol string `json:"symbol"` - UnRealizedProfit string `json:"unRealizedProfit"` - PositionSide string `json:"positionSide"` - UpdateTime int `json:"updateTime"` + EntryPrice string `json:"entryPrice"` + MarginType string `json:"marginType"` + IsAutoAddMargin string `json:"isAutoAddMargin"` + IsolatedMargin string `json:"isolatedMargin"` + Leverage fixedpoint.Value `json:"leverage"` + LiquidationPrice fixedpoint.Value `json:"liquidationPrice"` + MarkPrice fixedpoint.Value `json:"markPrice"` + MaxNotionalValue fixedpoint.Value `json:"maxNotionalValue"` + PositionAmount fixedpoint.Value `json:"positionAmt"` + Notional fixedpoint.Value `json:"notional"` + IsolatedWallet string `json:"isolatedWallet"` + Symbol string `json:"symbol"` + UnRealizedProfit fixedpoint.Value `json:"unRealizedProfit"` + PositionSide string `json:"positionSide"` + UpdateTime types.MillisecondTimestamp `json:"updateTime"` } //go:generate requestgen -method GET -url "/fapi/v2/positionRisk" -type FuturesGetPositionRisksRequest -responseType []FuturesPositionRisk From fc3e59b3ef8908627da297ab7a1dbc1187a376de Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 00:17:12 +0800 Subject: [PATCH 0679/1392] binance: move queryFuturesDepth to futures.go --- pkg/exchange/binance/exchange.go | 15 --------------- pkg/exchange/binance/futures.go | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 32693cb312..2670b37392 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -1289,21 +1289,6 @@ func (e *Exchange) DefaultFeeRates() types.ExchangeFee { } } -func (e *Exchange) queryFuturesDepth(ctx context.Context, symbol string) (snapshot types.SliceOrderBook, finalUpdateID int64, err error) { - res, err := e.futuresClient.NewDepthService().Symbol(symbol).Do(ctx) - if err != nil { - return snapshot, finalUpdateID, err - } - - response := &binance.DepthResponse{ - LastUpdateID: res.LastUpdateID, - Bids: res.Bids, - Asks: res.Asks, - } - - return convertDepth(snapshot, symbol, finalUpdateID, response) -} - // QueryDepth query the order book depth of a symbol func (e *Exchange) QueryDepth(ctx context.Context, symbol string) (snapshot types.SliceOrderBook, finalUpdateID int64, err error) { if e.IsFutures { diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index ec5523ae25..b66cbfa399 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/adshao/go-binance/v2" "github.com/adshao/go-binance/v2/futures" "github.com/google/uuid" "go.uber.org/multierr" @@ -349,3 +350,18 @@ func newFuturesClientOrderID(originalID string) (clientOrderID string) { return clientOrderID } + +func (e *Exchange) queryFuturesDepth(ctx context.Context, symbol string) (snapshot types.SliceOrderBook, finalUpdateID int64, err error) { + res, err := e.futuresClient.NewDepthService().Symbol(symbol).Do(ctx) + if err != nil { + return snapshot, finalUpdateID, err + } + + response := &binance.DepthResponse{ + LastUpdateID: res.LastUpdateID, + Bids: res.Bids, + Asks: res.Asks, + } + + return convertDepth(snapshot, symbol, finalUpdateID, response) +} From 8ddf248d50e60bf34ba0a0f295aba5ef21b4a6fd Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 00:19:31 +0800 Subject: [PATCH 0680/1392] binance: initialize the new futures client --- pkg/exchange/binance/exchange.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 2670b37392..fdde15815c 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -88,6 +88,8 @@ type Exchange struct { // client2 is a newer version of the binance api client implemented by ourselves. client2 *binanceapi.RestClient + + futuresClient2 *binanceapi.FuturesRestClient } var timeSetterOnce sync.Once @@ -111,13 +113,15 @@ func New(key, secret string) *Exchange { } client2 := binanceapi.NewClient(client.BaseURL) + futuresClient2 := binanceapi.NewFuturesRestClient(futuresClient.BaseURL) ex := &Exchange{ - key: key, - secret: secret, - client: client, - futuresClient: futuresClient, - client2: client2, + key: key, + secret: secret, + client: client, + futuresClient: futuresClient, + client2: client2, + futuresClient2: futuresClient2, } if len(key) > 0 && len(secret) > 0 { From f50999b7800dff1d54b355d88c0117d15f55fe14 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 00:22:42 +0800 Subject: [PATCH 0681/1392] binance: add QueryFuturesIncomeHistory --- pkg/exchange/binance/futures.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index b66cbfa399..f6bcd13011 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -365,3 +365,15 @@ func (e *Exchange) queryFuturesDepth(ctx context.Context, symbol string) (snapsh return convertDepth(snapshot, symbol, finalUpdateID, response) } + +// QueryFuturesIncomeHistory queries the income history on the binance futures account +// This is more binance futures specific API, the convert function is not designed yet. +// TODO: consider other futures platforms and design the common data structure for this +func (e *Exchange) QueryFuturesIncomeHistory(ctx context.Context, symbol string, startTime, endTime time.Time) ([]binanceapi.FuturesIncome, error) { + req := e.futuresClient2.NewFuturesGetIncomeHistoryRequest() + req.Symbol(symbol) + req.StartTime(startTime) + req.EndTime(endTime) + resp, err := req.Do(ctx) + return resp, err +} From 12ec3fd87fbe8d89cae478189d797cfb86cb6e21 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 00:23:25 +0800 Subject: [PATCH 0682/1392] binance: add incomeType parameter --- pkg/exchange/binance/futures.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index f6bcd13011..3374a79d59 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -369,9 +369,10 @@ func (e *Exchange) queryFuturesDepth(ctx context.Context, symbol string) (snapsh // QueryFuturesIncomeHistory queries the income history on the binance futures account // This is more binance futures specific API, the convert function is not designed yet. // TODO: consider other futures platforms and design the common data structure for this -func (e *Exchange) QueryFuturesIncomeHistory(ctx context.Context, symbol string, startTime, endTime time.Time) ([]binanceapi.FuturesIncome, error) { +func (e *Exchange) QueryFuturesIncomeHistory(ctx context.Context, symbol string, incomeType binanceapi.FuturesIncomeType, startTime, endTime time.Time) ([]binanceapi.FuturesIncome, error) { req := e.futuresClient2.NewFuturesGetIncomeHistoryRequest() req.Symbol(symbol) + req.IncomeType(incomeType) req.StartTime(startTime) req.EndTime(endTime) resp, err := req.Do(ctx) From 265b69a0ee1ec3bafc781e4219c0405bd4f0a7cd Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 00:53:29 +0800 Subject: [PATCH 0683/1392] batch: add funding fee batch query --- pkg/exchange/batch/funding_fee.go | 41 +++++++++++++++++++++++++++++++ pkg/exchange/binance/futures.go | 12 ++++++--- 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 pkg/exchange/batch/funding_fee.go diff --git a/pkg/exchange/batch/funding_fee.go b/pkg/exchange/batch/funding_fee.go new file mode 100644 index 0000000000..a7c5ce3ee3 --- /dev/null +++ b/pkg/exchange/batch/funding_fee.go @@ -0,0 +1,41 @@ +package batch + +import ( + "context" + "time" + + "golang.org/x/time/rate" + + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" + "github.com/c9s/bbgo/pkg/types" +) + +type BinanceFuturesIncomeHistoryService interface { + QueryFuturesIncomeHistory(ctx context.Context, symbol string, incomeType binanceapi.FuturesIncomeType, startTime, endTime *time.Time) ([]binanceapi.FuturesIncome, error) +} + +type FuturesFundingFeeBatchQuery struct { + BinanceFuturesIncomeHistoryService +} + +func (e *FuturesFundingFeeBatchQuery) Query(ctx context.Context, symbol string, startTime, endTime time.Time) (c chan types.MarginInterest, errC chan error) { + query := &AsyncTimeRangedBatchQuery{ + Type: types.MarginInterest{}, + Limiter: rate.NewLimiter(rate.Every(3*time.Second), 1), + JumpIfEmpty: time.Hour * 24 * 30, + Q: func(startTime, endTime time.Time) (interface{}, error) { + return e.QueryFuturesIncomeHistory(ctx, symbol, binanceapi.FuturesIncomeFundingFee, &startTime, &endTime) + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(binanceapi.FuturesIncome).Time) + }, + ID: func(obj interface{}) string { + interest := obj.(binanceapi.FuturesIncome) + return interest.Time.String() + }, + } + + c = make(chan types.MarginInterest, 100) + errC = query.Query(ctx, c, startTime, endTime) + return c, errC +} diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index 3374a79d59..224a185999 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -369,12 +369,18 @@ func (e *Exchange) queryFuturesDepth(ctx context.Context, symbol string) (snapsh // QueryFuturesIncomeHistory queries the income history on the binance futures account // This is more binance futures specific API, the convert function is not designed yet. // TODO: consider other futures platforms and design the common data structure for this -func (e *Exchange) QueryFuturesIncomeHistory(ctx context.Context, symbol string, incomeType binanceapi.FuturesIncomeType, startTime, endTime time.Time) ([]binanceapi.FuturesIncome, error) { +func (e *Exchange) QueryFuturesIncomeHistory(ctx context.Context, symbol string, incomeType binanceapi.FuturesIncomeType, startTime, endTime *time.Time) ([]binanceapi.FuturesIncome, error) { req := e.futuresClient2.NewFuturesGetIncomeHistoryRequest() req.Symbol(symbol) req.IncomeType(incomeType) - req.StartTime(startTime) - req.EndTime(endTime) + if startTime != nil { + req.StartTime(*startTime) + } + + if endTime != nil { + req.EndTime(*endTime) + } + resp, err := req.Do(ctx) return resp, err } From 6ea399dc8e7ab899fccfd480bf2cb75a76ba0c56 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 00:53:43 +0800 Subject: [PATCH 0684/1392] all: rename types.MarginHistory to types.MarginHistoryService --- pkg/cmd/margin.go | 12 ++++++------ pkg/exchange/batch/margin_interest.go | 2 +- pkg/exchange/batch/margin_liquidation.go | 2 +- pkg/exchange/batch/margin_loan.go | 2 +- pkg/exchange/batch/margin_repay.go | 2 +- pkg/service/margin.go | 10 +++++----- pkg/service/sync.go | 4 ++-- pkg/types/margin.go | 4 ++-- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pkg/cmd/margin.go b/pkg/cmd/margin.go index 1ad608f90d..542a905ddf 100644 --- a/pkg/cmd/margin.go +++ b/pkg/cmd/margin.go @@ -88,9 +88,9 @@ var marginLoansCmd = &cobra.Command{ return errors.New("session is not set") } - marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistory) + marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistoryService) if !ok { - return fmt.Errorf("exchange %s does not support MarginHistory service", selectedSession.ExchangeName) + return fmt.Errorf("exchange %s does not support MarginHistoryService service", selectedSession.ExchangeName) } now := time.Now() @@ -127,9 +127,9 @@ var marginRepaysCmd = &cobra.Command{ return errors.New("session is not set") } - marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistory) + marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistoryService) if !ok { - return fmt.Errorf("exchange %s does not support MarginHistory service", selectedSession.ExchangeName) + return fmt.Errorf("exchange %s does not support MarginHistoryService service", selectedSession.ExchangeName) } now := time.Now() @@ -166,9 +166,9 @@ var marginInterestsCmd = &cobra.Command{ return errors.New("session is not set") } - marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistory) + marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistoryService) if !ok { - return fmt.Errorf("exchange %s does not support MarginHistory service", selectedSession.ExchangeName) + return fmt.Errorf("exchange %s does not support MarginHistoryService service", selectedSession.ExchangeName) } now := time.Now() diff --git a/pkg/exchange/batch/margin_interest.go b/pkg/exchange/batch/margin_interest.go index 4e8224bc9e..5a9929c428 100644 --- a/pkg/exchange/batch/margin_interest.go +++ b/pkg/exchange/batch/margin_interest.go @@ -10,7 +10,7 @@ import ( ) type MarginInterestBatchQuery struct { - types.MarginHistory + types.MarginHistoryService } func (e *MarginInterestBatchQuery) Query(ctx context.Context, asset string, startTime, endTime time.Time) (c chan types.MarginInterest, errC chan error) { diff --git a/pkg/exchange/batch/margin_liquidation.go b/pkg/exchange/batch/margin_liquidation.go index 3726d18913..be325acf5d 100644 --- a/pkg/exchange/batch/margin_liquidation.go +++ b/pkg/exchange/batch/margin_liquidation.go @@ -11,7 +11,7 @@ import ( ) type MarginLiquidationBatchQuery struct { - types.MarginHistory + types.MarginHistoryService } func (e *MarginLiquidationBatchQuery) Query(ctx context.Context, startTime, endTime time.Time) (c chan types.MarginLiquidation, errC chan error) { diff --git a/pkg/exchange/batch/margin_loan.go b/pkg/exchange/batch/margin_loan.go index a32c7ea15e..7c8fd75175 100644 --- a/pkg/exchange/batch/margin_loan.go +++ b/pkg/exchange/batch/margin_loan.go @@ -11,7 +11,7 @@ import ( ) type MarginLoanBatchQuery struct { - types.MarginHistory + types.MarginHistoryService } func (e *MarginLoanBatchQuery) Query(ctx context.Context, asset string, startTime, endTime time.Time) (c chan types.MarginLoan, errC chan error) { diff --git a/pkg/exchange/batch/margin_repay.go b/pkg/exchange/batch/margin_repay.go index a30ea12085..eb7cb32329 100644 --- a/pkg/exchange/batch/margin_repay.go +++ b/pkg/exchange/batch/margin_repay.go @@ -11,7 +11,7 @@ import ( ) type MarginRepayBatchQuery struct { - types.MarginHistory + types.MarginHistoryService } func (e *MarginRepayBatchQuery) Query(ctx context.Context, asset string, startTime, endTime time.Time) (c chan types.MarginRepay, errC chan error) { diff --git a/pkg/service/margin.go b/pkg/service/margin.go index 563167c0db..595638a1c8 100644 --- a/pkg/service/margin.go +++ b/pkg/service/margin.go @@ -17,7 +17,7 @@ type MarginService struct { } func (s *MarginService) Sync(ctx context.Context, ex types.Exchange, asset string, startTime time.Time) error { - api, ok := ex.(types.MarginHistory) + api, ok := ex.(types.MarginHistoryService) if !ok { return nil } @@ -38,7 +38,7 @@ func (s *MarginService) Sync(ctx context.Context, ex types.Exchange, asset strin Type: types.MarginLoan{}, BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { query := &batch.MarginLoanBatchQuery{ - MarginHistory: api, + MarginHistoryService: api, } return query.Query(ctx, asset, startTime, endTime) }, @@ -55,7 +55,7 @@ func (s *MarginService) Sync(ctx context.Context, ex types.Exchange, asset strin Type: types.MarginRepay{}, BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { query := &batch.MarginRepayBatchQuery{ - MarginHistory: api, + MarginHistoryService: api, } return query.Query(ctx, asset, startTime, endTime) }, @@ -72,7 +72,7 @@ func (s *MarginService) Sync(ctx context.Context, ex types.Exchange, asset strin Type: types.MarginInterest{}, BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { query := &batch.MarginInterestBatchQuery{ - MarginHistory: api, + MarginHistoryService: api, } return query.Query(ctx, asset, startTime, endTime) }, @@ -90,7 +90,7 @@ func (s *MarginService) Sync(ctx context.Context, ex types.Exchange, asset strin Type: types.MarginLiquidation{}, BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { query := &batch.MarginLiquidationBatchQuery{ - MarginHistory: api, + MarginHistoryService: api, } return query.Query(ctx, startTime, endTime) }, diff --git a/pkg/service/sync.go b/pkg/service/sync.go index aaf757ddb2..34010c5c5a 100644 --- a/pkg/service/sync.go +++ b/pkg/service/sync.go @@ -49,8 +49,8 @@ func (s *SyncService) SyncSessionSymbols(ctx context.Context, exchange types.Exc } func (s *SyncService) SyncMarginHistory(ctx context.Context, exchange types.Exchange, startTime time.Time, assets ...string) error { - if _, implemented := exchange.(types.MarginHistory); !implemented { - log.Debugf("exchange %T does not support types.MarginHistory", exchange) + if _, implemented := exchange.(types.MarginHistoryService); !implemented { + log.Debugf("exchange %T does not support types.MarginHistoryService", exchange) return nil } diff --git a/pkg/types/margin.go b/pkg/types/margin.go index fbec9e2a0f..9d68049ec7 100644 --- a/pkg/types/margin.go +++ b/pkg/types/margin.go @@ -105,8 +105,8 @@ type MarginLiquidation struct { UpdatedTime Time `json:"updatedTime" db:"time"` } -// MarginHistory provides the service of querying loan history and repay history -type MarginHistory interface { +// MarginHistoryService provides the service of querying loan history and repay history +type MarginHistoryService interface { QueryLoanHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]MarginLoan, error) QueryRepayHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]MarginRepay, error) QueryLiquidationHistory(ctx context.Context, startTime, endTime *time.Time) ([]MarginLiquidation, error) From 111e435a0aecdfedc74e1856e0db36ba12a52861 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 00:55:56 +0800 Subject: [PATCH 0685/1392] batch: rename BinanceFuturesIncomeBatchQuery --- pkg/exchange/batch/funding_fee.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/batch/funding_fee.go b/pkg/exchange/batch/funding_fee.go index a7c5ce3ee3..98c1453f0e 100644 --- a/pkg/exchange/batch/funding_fee.go +++ b/pkg/exchange/batch/funding_fee.go @@ -14,17 +14,17 @@ type BinanceFuturesIncomeHistoryService interface { QueryFuturesIncomeHistory(ctx context.Context, symbol string, incomeType binanceapi.FuturesIncomeType, startTime, endTime *time.Time) ([]binanceapi.FuturesIncome, error) } -type FuturesFundingFeeBatchQuery struct { +type BinanceFuturesIncomeBatchQuery struct { BinanceFuturesIncomeHistoryService } -func (e *FuturesFundingFeeBatchQuery) Query(ctx context.Context, symbol string, startTime, endTime time.Time) (c chan types.MarginInterest, errC chan error) { +func (e *BinanceFuturesIncomeBatchQuery) Query(ctx context.Context, symbol string, incomeType binanceapi.FuturesIncomeType, startTime, endTime time.Time) (c chan types.MarginInterest, errC chan error) { query := &AsyncTimeRangedBatchQuery{ Type: types.MarginInterest{}, Limiter: rate.NewLimiter(rate.Every(3*time.Second), 1), JumpIfEmpty: time.Hour * 24 * 30, Q: func(startTime, endTime time.Time) (interface{}, error) { - return e.QueryFuturesIncomeHistory(ctx, symbol, binanceapi.FuturesIncomeFundingFee, &startTime, &endTime) + return e.QueryFuturesIncomeHistory(ctx, symbol, incomeType, &startTime, &endTime) }, T: func(obj interface{}) time.Time { return time.Time(obj.(binanceapi.FuturesIncome).Time) @@ -35,7 +35,7 @@ func (e *FuturesFundingFeeBatchQuery) Query(ctx context.Context, symbol string, }, } - c = make(chan types.MarginInterest, 100) + c = make(chan binanceapi.FuturesIncome, 100) errC = query.Query(ctx, c, startTime, endTime) return c, errC } From c6cedde8c936559843c3f796f98f4f31a7dcef97 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 00:56:24 +0800 Subject: [PATCH 0686/1392] batch: fix binance query return type --- pkg/exchange/batch/funding_fee.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/exchange/batch/funding_fee.go b/pkg/exchange/batch/funding_fee.go index 98c1453f0e..3d9941894b 100644 --- a/pkg/exchange/batch/funding_fee.go +++ b/pkg/exchange/batch/funding_fee.go @@ -18,7 +18,7 @@ type BinanceFuturesIncomeBatchQuery struct { BinanceFuturesIncomeHistoryService } -func (e *BinanceFuturesIncomeBatchQuery) Query(ctx context.Context, symbol string, incomeType binanceapi.FuturesIncomeType, startTime, endTime time.Time) (c chan types.MarginInterest, errC chan error) { +func (e *BinanceFuturesIncomeBatchQuery) Query(ctx context.Context, symbol string, incomeType binanceapi.FuturesIncomeType, startTime, endTime time.Time) (c chan binanceapi.FuturesIncome, errC chan error) { query := &AsyncTimeRangedBatchQuery{ Type: types.MarginInterest{}, Limiter: rate.NewLimiter(rate.Every(3*time.Second), 1), From c176e2df5f7a7434f4f099de4f59cf7686ca588a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 01:02:31 +0800 Subject: [PATCH 0687/1392] xfunding: move moving average config out --- pkg/strategy/xfunding/strategy.go | 39 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index df8edfb01c..b39026ddbb 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -34,6 +34,23 @@ const ( PositionClosing ) +type MovingAverageConfig struct { + Interval types.Interval `json:"interval"` + // MovingAverageType is the moving average indicator type that we want to use, + // it could be SMA or EWMA + MovingAverageType string `json:"movingAverageType"` + + // MovingAverageInterval is the interval of k-lines for the moving average indicator to calculate, + // it could be "1m", "5m", "1h" and so on. note that, the moving averages are calculated from + // the k-line data we subscribed + // MovingAverageInterval types.Interval `json:"movingAverageInterval"` + // + // // MovingAverageWindow is the number of the window size of the moving average indicator. + // // The number of k-lines in the window. generally used window sizes are 7, 25 and 99 in the TradingView. + // MovingAverageWindow int `json:"movingAverageWindow"` + MovingAverageIntervalWindow types.IntervalWindow `json:"movingAverageIntervalWindow"` +} + var log = logrus.WithField("strategy", ID) func init() { @@ -89,28 +106,6 @@ type Strategy struct { Low fixedpoint.Value `json:"low"` } `json:"shortFundingRate"` - SupportDetection []struct { - Interval types.Interval `json:"interval"` - // MovingAverageType is the moving average indicator type that we want to use, - // it could be SMA or EWMA - MovingAverageType string `json:"movingAverageType"` - - // MovingAverageInterval is the interval of k-lines for the moving average indicator to calculate, - // it could be "1m", "5m", "1h" and so on. note that, the moving averages are calculated from - // the k-line data we subscribed - // MovingAverageInterval types.Interval `json:"movingAverageInterval"` - // - // // MovingAverageWindow is the number of the window size of the moving average indicator. - // // The number of k-lines in the window. generally used window sizes are 7, 25 and 99 in the TradingView. - // MovingAverageWindow int `json:"movingAverageWindow"` - - MovingAverageIntervalWindow types.IntervalWindow `json:"movingAverageIntervalWindow"` - - MinVolume fixedpoint.Value `json:"minVolume"` - - MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` - } `json:"supportDetection"` - SpotSession string `json:"spotSession"` FuturesSession string `json:"futuresSession"` Reset bool `json:"reset"` From 5c21445b1547923ea18da5e3eee291aa269354e7 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 01:03:07 +0800 Subject: [PATCH 0688/1392] xfunding: comment unused code --- pkg/strategy/xfunding/strategy.go | 47 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index b39026ddbb..954b872836 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "sync" "time" @@ -187,31 +186,29 @@ func (s *Strategy) InstanceID() string { } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - standardIndicatorSet := session.StandardIndicatorSet(s.Symbol) - - var ma types.Float64Indicator - for _, detection := range s.SupportDetection { - - switch strings.ToLower(detection.MovingAverageType) { - case "sma": - ma = standardIndicatorSet.SMA(types.IntervalWindow{ - Interval: detection.MovingAverageIntervalWindow.Interval, - Window: detection.MovingAverageIntervalWindow.Window, - }) - case "ema", "ewma": - ma = standardIndicatorSet.EWMA(types.IntervalWindow{ - Interval: detection.MovingAverageIntervalWindow.Interval, - Window: detection.MovingAverageIntervalWindow.Window, - }) - default: - ma = standardIndicatorSet.EWMA(types.IntervalWindow{ - Interval: detection.MovingAverageIntervalWindow.Interval, - Window: detection.MovingAverageIntervalWindow.Window, - }) + // standardIndicatorSet := session.StandardIndicatorSet(s.Symbol) + /* + var ma types.Float64Indicator + for _, detection := range s.SupportDetection { + switch strings.ToLower(detection.MovingAverageType) { + case "sma": + ma = standardIndicatorSet.SMA(types.IntervalWindow{ + Interval: detection.MovingAverageIntervalWindow.Interval, + Window: detection.MovingAverageIntervalWindow.Window, + }) + case "ema", "ewma": + ma = standardIndicatorSet.EWMA(types.IntervalWindow{ + Interval: detection.MovingAverageIntervalWindow.Interval, + Window: detection.MovingAverageIntervalWindow.Window, + }) + default: + ma = standardIndicatorSet.EWMA(types.IntervalWindow{ + Interval: detection.MovingAverageIntervalWindow.Interval, + Window: detection.MovingAverageIntervalWindow.Window, + }) + } } - } - _ = ma - + */ return nil } From 23746678b4668b35dcfcc3d16aba3276db014a14 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 01:07:50 +0800 Subject: [PATCH 0689/1392] xfunding: initialize NeutralPosition --- pkg/strategy/xfunding/strategy.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 954b872836..43eac9aac9 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -109,9 +109,21 @@ type Strategy struct { FuturesSession string `json:"futuresSession"` Reset bool `json:"reset"` - ProfitStats *types.ProfitStats `persistence:"profit_stats"` - SpotPosition *types.Position `persistence:"spot_position"` - FuturesPosition *types.Position `persistence:"futures_position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + + // SpotPosition is used for the spot position (usually long position) + // so that we know how much spot we have bought and the average cost of the spot. + SpotPosition *types.Position `persistence:"spot_position"` + + // FuturesPosition is used for the futures position + // this position is the reverse side of the spot position, when spot position is long, then the futures position will be short. + // but the base quantity should be the same as the spot position + FuturesPosition *types.Position `persistence:"futures_position"` + + // NeutralPosition is used for sharing spot/futures position + // when creating the spot position and futures position, there will be a spread between the spot position and the futures position. + // this neutral position can calculate the spread cost between these two positions + NeutralPosition *types.Position `persistence:"neutral_position"` State *State `persistence:"state"` @@ -241,12 +253,16 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.ProfitStats = types.NewProfitStats(s.Market) } + if s.SpotPosition == nil || s.Reset { + s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) + } + if s.FuturesPosition == nil || s.Reset { s.FuturesPosition = types.NewPositionFromMarket(s.futuresMarket) } - if s.SpotPosition == nil || s.Reset { - s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) + if s.NeutralPosition == nil || s.Reset { + s.NeutralPosition = types.NewPositionFromMarket(s.futuresMarket) } if s.State == nil || s.Reset { From 22d339cb4149ae3e6988e254701583a2a49a54a3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 01:16:54 +0800 Subject: [PATCH 0690/1392] xfunding: check binance type and return error --- pkg/strategy/xfunding/strategy.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 43eac9aac9..edb0ce4325 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -52,6 +52,8 @@ type MovingAverageConfig struct { var log = logrus.WithField("strategy", ID) +var errNotBinanceExchange = errors.New("not binance exchange, currently only support binance exchange") + func init() { // Register the pointer of the strategy struct, // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) @@ -277,9 +279,14 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order log.Infof("loaded spot position: %s", s.SpotPosition.String()) log.Infof("loaded futures position: %s", s.FuturesPosition.String()) - binanceFutures := s.futuresSession.Exchange.(*binance.Exchange) - binanceSpot := s.spotSession.Exchange.(*binance.Exchange) - _ = binanceSpot + binanceFutures, ok := s.futuresSession.Exchange.(*binance.Exchange) + if !ok { + return errNotBinanceExchange + } + binanceSpot, ok := s.spotSession.Exchange.(*binance.Exchange) + if !ok { + return errNotBinanceExchange + } s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { From e41df7e321eade31df9eb8dd4d8a7773661914bb Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 01:21:20 +0800 Subject: [PATCH 0691/1392] xfunding: add wip list --- pkg/strategy/xfunding/strategy.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index edb0ce4325..3786d232c1 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -17,6 +17,14 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +// WIP: +// - track fee token price for cost +// - buy enough BNB before creating positions +// - transfer the rest BNB into the futures account +// - add slack notification support +// - use neutral position to calculate the position cost +// - customize profit stats for this funding fee strategy + const ID = "xfunding" // Position State Transitions: @@ -290,6 +298,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + s.NeutralPosition.AddTrade(trade) + // we act differently on the spot account // when opening a position, we place orders on the spot account first, then the futures account, // and we need to accumulate the used quote amount @@ -331,6 +341,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) s.futuresOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + s.NeutralPosition.AddTrade(trade) + if s.positionType != types.PositionShort { return } From 78c73e4514dad871045725f1de9de2ee1a842146 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 01:32:47 +0800 Subject: [PATCH 0692/1392] bbgo: check e.disableNotify for profit stats --- pkg/bbgo/order_executor_general.go | 6 ++++-- pkg/strategy/xfunding/profitstats.go | 1 + pkg/strategy/xfunding/strategy.go | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 pkg/strategy/xfunding/profitstats.go diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index db97ae7e03..daba2999ca 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -155,8 +155,10 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { profitStats.AddProfit(*profit) - Notify(profit) - Notify(profitStats) + if !e.disableNotify { + Notify(profit) + Notify(profitStats) + } }) } diff --git a/pkg/strategy/xfunding/profitstats.go b/pkg/strategy/xfunding/profitstats.go new file mode 100644 index 0000000000..80467a4ce6 --- /dev/null +++ b/pkg/strategy/xfunding/profitstats.go @@ -0,0 +1 @@ +package xfunding diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 3786d232c1..1745353935 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -843,5 +843,6 @@ func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.Exch orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) + orderExecutor.BindProfitStats(s.ProfitStats) return orderExecutor } From f127a530b7ea83e111aab08854563fedb0b277f1 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 01:33:52 +0800 Subject: [PATCH 0693/1392] xfunding: bind profit stats --- pkg/strategy/xfunding/strategy.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 1745353935..42a8d7adc2 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -837,9 +837,6 @@ func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.Exch orderExecutor.SetMaxRetries(0) orderExecutor.BindEnvironment(s.Environment) orderExecutor.Bind() - orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { - s.ProfitStats.AddTrade(trade) - }) orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) From ba0dd68be04f6d1ff307ccfba4fc966696dbda14 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 01:54:39 +0800 Subject: [PATCH 0694/1392] xfunding: callcate funding fee --- pkg/strategy/xfunding/profitstats.go | 31 +++++++++++++++++++++ pkg/strategy/xfunding/strategy.go | 41 +++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/xfunding/profitstats.go b/pkg/strategy/xfunding/profitstats.go index 80467a4ce6..c72ad2a072 100644 --- a/pkg/strategy/xfunding/profitstats.go +++ b/pkg/strategy/xfunding/profitstats.go @@ -1 +1,32 @@ package xfunding + +import ( + "fmt" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type FundingFee struct { + Asset string `json:"asset"` + Amount fixedpoint.Value `json:"amount"` +} + +type ProfitStats struct { + *types.ProfitStats + + FundingFeeCurrency string `json:"fundingFeeCurrency"` + TotalFundingFee fixedpoint.Value `json:"totalFundingFee"` + FundingFeeRecords []FundingFee `json:"fundingFeeRecords"` +} + +func (s *ProfitStats) AddFundingFee(fee FundingFee) error { + s.FundingFeeRecords = append(s.FundingFeeRecords, fee) + s.TotalFundingFee = s.TotalFundingFee.Add(fee.Amount) + if s.FundingFeeCurrency == "" { + s.FundingFeeCurrency = fee.Asset + } else if s.FundingFeeCurrency != fee.Asset { + return fmt.Errorf("unexpected error, funding fee currency is not matched, given: %s, wanted: %s", fee.Asset, s.FundingFeeCurrency) + } + return nil +} diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 42a8d7adc2..d650cb4e24 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -119,7 +119,7 @@ type Strategy struct { FuturesSession string `json:"futuresSession"` Reset bool `json:"reset"` - ProfitStats *types.ProfitStats `persistence:"profit_stats"` + ProfitStats *ProfitStats `persistence:"profit_stats"` // SpotPosition is used for the spot position (usually long position) // so that we know how much spot we have bought and the average cost of the spot. @@ -260,7 +260,12 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } if s.ProfitStats == nil || s.Reset { - s.ProfitStats = types.NewProfitStats(s.Market) + s.ProfitStats = &ProfitStats{ + ProfitStats: types.NewProfitStats(s.Market), + + // when receiving funding fee, the funding fee asset is the quote currency of that market. + FundingFeeCurrency: s.futuresMarket.QuoteCurrency, + } } if s.SpotPosition == nil || s.Reset { @@ -373,7 +378,35 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order case binance.AccountUpdateEventReasonWithdraw: case binance.AccountUpdateEventReasonFundingFee: - + // EventBase:{ + // Event:ACCOUNT_UPDATE + // Time:1679760000932 + // } + // Transaction:1679760000927 + // AccountUpdate:{ + // EventReasonType:FUNDING_FEE + // Balances:[{ + // Asset:USDT + // WalletBalance:56.64251742 + // CrossWalletBalance:56.64251742 + // BalanceChange:-0.00037648 + // }] + // } + // } + for _, b := range e.AccountUpdate.Balances { + if b.Asset != s.ProfitStats.FundingFeeCurrency { + continue + } + + err := s.ProfitStats.AddFundingFee(FundingFee{ + Asset: b.Asset, + Amount: b.BalanceChange, + }) + if err != nil { + log.WithError(err).Error("unable to add funding fee to profitStats") + } + } + bbgo.Sync(ctx, s) } }) } @@ -840,6 +873,6 @@ func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.Exch orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) - orderExecutor.BindProfitStats(s.ProfitStats) + orderExecutor.BindProfitStats(s.ProfitStats.ProfitStats) return orderExecutor } From 425952d76cecde6333febcb07bd5645fdc360aa8 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 01:55:33 +0800 Subject: [PATCH 0695/1392] xfunding: log collected funding fee --- pkg/strategy/xfunding/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index d650cb4e24..9d0d572796 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -406,6 +406,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order log.WithError(err).Error("unable to add funding fee to profitStats") } } + + log.Infof("total collected funding fee: %f %s", s.ProfitStats.TotalFundingFee.Float64(), s.ProfitStats.FundingFeeCurrency) bbgo.Sync(ctx, s) } }) From 6349566ce9e5d279f53766fc286eda53fbd29a36 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 02:07:54 +0800 Subject: [PATCH 0696/1392] config: add document to the xfunding options --- config/xfunding.yaml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 2b65cd1670..5b31b47a06 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -30,11 +30,28 @@ crossExchangeStrategies: - xfunding: spotSession: binance futuresSession: binance_futures + + ## symbol is the symbol name of the spot market and the futures market + ## todo: provide option to separate the futures market symbol symbol: ETHUSDT + + ## interval is the interval for checking futures premium and the funding rate + interval: 1m + + ## leverage is the leverage of the reverse futures position size. + ## for example, you can buy 1 BTC and short 3 BTC in the futures account with 3x leverage. leverage: 1.0 + + ## incrementalQuoteQuantity is the quote quantity per maker order when creating the positions + ## when in BTC-USDT 20 means 20 USDT, each buy order will hold 20 USDT quote amount. incrementalQuoteQuantity: 20 + + ## quoteInvestment is how much you want to invest to create your position. + ## for example, when 10k USDT is given as the quote investment, and the average executed price of your position is around BTC 18k + ## you will be holding around 0.555555 BTC quoteInvestment: 50 + ## shortFundingRate is the funding rate range you want to create your position shortFundingRate: ## when funding rate is higher than this high value, the strategy will start buying spot and opening a short position high: 0.001% @@ -42,4 +59,4 @@ crossExchangeStrategies: low: -0.01% ## reset will reset the spot/futures positions, the transfer stats and the position state. - # reset: true + reset: true From ff35fd06c4ea1c803b1d2930d7c585771c65e52a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 02:09:21 +0800 Subject: [PATCH 0697/1392] xfunding: pull out interval option --- pkg/strategy/xfunding/strategy.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 9d0d572796..3f32b1eac4 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -94,7 +94,9 @@ type Strategy struct { Environment *bbgo.Environment // These fields will be filled from the config file (it translates YAML to JSON) - Symbol string `json:"symbol"` + Symbol string `json:"symbol"` + Interval types.Interval `json:"interval"` + Market types.Market `json:"-"` // Leverage is the leverage of the futures position @@ -158,13 +160,8 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { spotSession := sessions[s.SpotSession] futuresSession := sessions[s.FuturesSession] - spotSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: types.Interval1m, - }) - - futuresSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: types.Interval1m, - }) + spotSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + futuresSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {} @@ -178,6 +175,10 @@ func (s *Strategy) Defaults() error { s.MinHoldingPeriod = types.Duration(3 * 24 * time.Hour) } + if s.Interval == "" { + s.Interval = types.Interval1m + } + s.positionType = types.PositionShort return nil @@ -364,7 +365,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } }) - s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { s.queryAndDetectPremiumIndex(ctx, binanceFutures) })) From a6b47fda7290026fc988afcf1ed33ae5418204ec Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 02:12:00 +0800 Subject: [PATCH 0698/1392] xfunding: check funding fee and record txn id --- pkg/strategy/xfunding/profitstats.go | 8 ++++++-- pkg/strategy/xfunding/strategy.go | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/xfunding/profitstats.go b/pkg/strategy/xfunding/profitstats.go index c72ad2a072..04c98a65a5 100644 --- a/pkg/strategy/xfunding/profitstats.go +++ b/pkg/strategy/xfunding/profitstats.go @@ -10,6 +10,7 @@ import ( type FundingFee struct { Asset string `json:"asset"` Amount fixedpoint.Value `json:"amount"` + Txn int64 `json:"txn"` } type ProfitStats struct { @@ -18,15 +19,18 @@ type ProfitStats struct { FundingFeeCurrency string `json:"fundingFeeCurrency"` TotalFundingFee fixedpoint.Value `json:"totalFundingFee"` FundingFeeRecords []FundingFee `json:"fundingFeeRecords"` + LastFundingFeeTxn int64 `json:"lastFundingFeeTxn"` } func (s *ProfitStats) AddFundingFee(fee FundingFee) error { - s.FundingFeeRecords = append(s.FundingFeeRecords, fee) - s.TotalFundingFee = s.TotalFundingFee.Add(fee.Amount) if s.FundingFeeCurrency == "" { s.FundingFeeCurrency = fee.Asset } else if s.FundingFeeCurrency != fee.Asset { return fmt.Errorf("unexpected error, funding fee currency is not matched, given: %s, wanted: %s", fee.Asset, s.FundingFeeCurrency) } + + s.FundingFeeRecords = append(s.FundingFeeRecords, fee) + s.TotalFundingFee = s.TotalFundingFee.Add(fee.Amount) + s.LastFundingFeeTxn = fee.Txn return nil } diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 3f32b1eac4..a88b866dca 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -402,6 +402,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order err := s.ProfitStats.AddFundingFee(FundingFee{ Asset: b.Asset, Amount: b.BalanceChange, + Txn: e.Transaction, }) if err != nil { log.WithError(err).Error("unable to add funding fee to profitStats") From ac33b5a8785b68c7481ce210211de51d99d40523 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 02:13:22 +0800 Subject: [PATCH 0699/1392] xfunding: check duplicated funding fee txn --- pkg/strategy/xfunding/profitstats.go | 4 ++++ pkg/strategy/xfunding/strategy.go | 2 ++ 2 files changed, 6 insertions(+) diff --git a/pkg/strategy/xfunding/profitstats.go b/pkg/strategy/xfunding/profitstats.go index 04c98a65a5..ae56b1331c 100644 --- a/pkg/strategy/xfunding/profitstats.go +++ b/pkg/strategy/xfunding/profitstats.go @@ -29,6 +29,10 @@ func (s *ProfitStats) AddFundingFee(fee FundingFee) error { return fmt.Errorf("unexpected error, funding fee currency is not matched, given: %s, wanted: %s", fee.Asset, s.FundingFeeCurrency) } + if s.LastFundingFeeTxn == fee.Txn { + return errDuplicatedFundingFeeTxnId + } + s.FundingFeeRecords = append(s.FundingFeeRecords, fee) s.TotalFundingFee = s.TotalFundingFee.Add(fee.Amount) s.LastFundingFeeTxn = fee.Txn diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index a88b866dca..5d9b3fc709 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -62,6 +62,8 @@ var log = logrus.WithField("strategy", ID) var errNotBinanceExchange = errors.New("not binance exchange, currently only support binance exchange") +var errDuplicatedFundingFeeTxnId = errors.New("duplicated funding fee txn id") + func init() { // Register the pointer of the strategy struct, // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) From f4a35132e8b9860781365f118db01441888223f1 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 02:16:23 +0800 Subject: [PATCH 0700/1392] xfunding: add trades to s.NeutralPosition --- pkg/strategy/xfunding/strategy.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 5d9b3fc709..c4f5589cd8 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -306,8 +306,6 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - s.NeutralPosition.AddTrade(trade) - // we act differently on the spot account // when opening a position, we place orders on the spot account first, then the futures account, // and we need to accumulate the used quote amount @@ -349,8 +347,6 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) s.futuresOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - s.NeutralPosition.AddTrade(trade) - if s.positionType != types.PositionShort { return } @@ -879,6 +875,12 @@ func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.Exch orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) + orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _ fixedpoint.Value, _ fixedpoint.Value) { + if profit, netProfit, madeProfit := s.NeutralPosition.AddTrade(trade); madeProfit { + _ = profit + _ = netProfit + } + }) orderExecutor.BindProfitStats(s.ProfitStats.ProfitStats) return orderExecutor } From e5d2db0f72d1e0ddd2aa5dca0a936cbba2dcbfe6 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 02:32:21 +0800 Subject: [PATCH 0701/1392] xfunding: customize netural position profit --- pkg/strategy/xfunding/strategy.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index c4f5589cd8..57c8f9f252 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -876,11 +876,12 @@ func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.Exch bbgo.Sync(ctx, s) }) orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _ fixedpoint.Value, _ fixedpoint.Value) { + s.ProfitStats.AddTrade(trade) + if profit, netProfit, madeProfit := s.NeutralPosition.AddTrade(trade); madeProfit { - _ = profit - _ = netProfit + p := s.NeutralPosition.NewProfit(trade, profit, netProfit) + s.ProfitStats.AddProfit(p) } }) - orderExecutor.BindProfitStats(s.ProfitStats.ProfitStats) return orderExecutor } From 36836c7c7938a991ab362ac4d08d148b3bee6021 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 14:42:13 +0800 Subject: [PATCH 0702/1392] xfunding: add funding fee time --- pkg/strategy/xfunding/profitstats.go | 4 ++++ pkg/strategy/xfunding/strategy.go | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/xfunding/profitstats.go b/pkg/strategy/xfunding/profitstats.go index ae56b1331c..93e3b6df9f 100644 --- a/pkg/strategy/xfunding/profitstats.go +++ b/pkg/strategy/xfunding/profitstats.go @@ -2,6 +2,7 @@ package xfunding import ( "fmt" + "time" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" @@ -11,6 +12,7 @@ type FundingFee struct { Asset string `json:"asset"` Amount fixedpoint.Value `json:"amount"` Txn int64 `json:"txn"` + Time time.Time `json:"time"` } type ProfitStats struct { @@ -20,6 +22,7 @@ type ProfitStats struct { TotalFundingFee fixedpoint.Value `json:"totalFundingFee"` FundingFeeRecords []FundingFee `json:"fundingFeeRecords"` LastFundingFeeTxn int64 `json:"lastFundingFeeTxn"` + LastFundingFeeTime time.Time `json:"lastFundingFeeTime"` } func (s *ProfitStats) AddFundingFee(fee FundingFee) error { @@ -36,5 +39,6 @@ func (s *ProfitStats) AddFundingFee(fee FundingFee) error { s.FundingFeeRecords = append(s.FundingFeeRecords, fee) s.TotalFundingFee = s.TotalFundingFee.Add(fee.Amount) s.LastFundingFeeTxn = fee.Txn + s.LastFundingFeeTime = fee.Time return nil } diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 57c8f9f252..125a4e6cb2 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -369,13 +369,9 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if binanceStream, ok := s.futuresSession.UserDataStream.(*binance.Stream); ok { binanceStream.OnAccountUpdateEvent(func(e *binance.AccountUpdateEvent) { - log.Infof("onAccountUpdateEvent: %+v", e) switch e.AccountUpdate.EventReasonType { - case binance.AccountUpdateEventReasonDeposit: - case binance.AccountUpdateEventReasonWithdraw: - case binance.AccountUpdateEventReasonFundingFee: // EventBase:{ // Event:ACCOUNT_UPDATE @@ -397,10 +393,12 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order continue } + txnTime := time.UnixMilli(e.Time) err := s.ProfitStats.AddFundingFee(FundingFee{ Asset: b.Asset, Amount: b.BalanceChange, Txn: e.Transaction, + Time: txnTime, }) if err != nil { log.WithError(err).Error("unable to add funding fee to profitStats") From a3f96871e27ba322a3656826d530b4224b78c9af Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 14:44:18 +0800 Subject: [PATCH 0703/1392] xfunding: pull out newState constructor --- pkg/strategy/xfunding/strategy.go | 34 +++++++++++++++++-------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 125a4e6cb2..1df919991e 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -82,6 +82,15 @@ type State struct { UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` } +func newState() *State { + return &State{ + PositionState: PositionClosed, + PendingBaseTransfer: fixedpoint.Zero, + TotalBaseTransfer: fixedpoint.Zero, + UsedQuoteInvestment: fixedpoint.Zero, + } +} + func (s *State) Reset() { s.PositionState = PositionClosed s.PendingBaseTransfer = fixedpoint.Zero @@ -246,6 +255,15 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.spotMarket, _ = s.spotSession.Market(s.Symbol) s.futuresMarket, _ = s.futuresSession.Market(s.Symbol) + binanceFutures, ok := s.futuresSession.Exchange.(*binance.Exchange) + if !ok { + return errNotBinanceExchange + } + binanceSpot, ok := s.spotSession.Exchange.(*binance.Exchange) + if !ok { + return errNotBinanceExchange + } + // adjust QuoteInvestment if b, ok := s.spotSession.Account.Balance(s.spotMarket.QuoteCurrency); ok { originalQuoteInvestment := s.QuoteInvestment @@ -284,26 +302,12 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } if s.State == nil || s.Reset { - s.State = &State{ - PositionState: PositionClosed, - PendingBaseTransfer: fixedpoint.Zero, - TotalBaseTransfer: fixedpoint.Zero, - UsedQuoteInvestment: fixedpoint.Zero, - } + s.State = newState() } log.Infof("loaded spot position: %s", s.SpotPosition.String()) log.Infof("loaded futures position: %s", s.FuturesPosition.String()) - binanceFutures, ok := s.futuresSession.Exchange.(*binance.Exchange) - if !ok { - return errNotBinanceExchange - } - binanceSpot, ok := s.spotSession.Exchange.(*binance.Exchange) - if !ok { - return errNotBinanceExchange - } - s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { // we act differently on the spot account From 4d1f69130001fc3bd896fb261d597bc5256519ad Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 14:54:27 +0800 Subject: [PATCH 0704/1392] xfunding: add syncFundingFeeRecords method --- config/xfunding.yaml | 2 +- pkg/strategy/xfunding/strategy.go | 48 +++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 5b31b47a06..259b7f78df 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -59,4 +59,4 @@ crossExchangeStrategies: low: -0.01% ## reset will reset the spot/futures positions, the transfer stats and the position state. - reset: true + # reset: true diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 1df919991e..f80341592d 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -9,7 +9,9 @@ import ( "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/exchange/batch" "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/util/backoff" @@ -157,6 +159,8 @@ type Strategy struct { spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor spotMarket, futuresMarket types.Market + binanceFutures, binanceSpot *binance.Exchange + // positionType is the futures position type // currently we only support short position for the positive funding rate positionType types.PositionType @@ -255,11 +259,13 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.spotMarket, _ = s.spotSession.Market(s.Symbol) s.futuresMarket, _ = s.futuresSession.Market(s.Symbol) - binanceFutures, ok := s.futuresSession.Exchange.(*binance.Exchange) + var ok bool + s.binanceFutures, ok = s.futuresSession.Exchange.(*binance.Exchange) if !ok { return errNotBinanceExchange } - binanceSpot, ok := s.spotSession.Exchange.(*binance.Exchange) + + s.binanceSpot, ok = s.spotSession.Exchange.(*binance.Exchange) if !ok { return errNotBinanceExchange } @@ -283,9 +289,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if s.ProfitStats == nil || s.Reset { s.ProfitStats = &ProfitStats{ ProfitStats: types.NewProfitStats(s.Market), - // when receiving funding fee, the funding fee asset is the quote currency of that market. FundingFeeCurrency: s.futuresMarket.QuoteCurrency, + TotalFundingFee: fixedpoint.Zero, + FundingFeeRecords: nil, } } @@ -307,6 +314,12 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order log.Infof("loaded spot position: %s", s.SpotPosition.String()) log.Infof("loaded futures position: %s", s.FuturesPosition.String()) + log.Infof("loaded neutral position: %s", s.NeutralPosition.String()) + + // sync funding fee txns + if !s.ProfitStats.LastFundingFeeTime.IsZero() { + s.syncFundingFeeRecords(ctx, s.ProfitStats.LastFundingFeeTime) + } s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { @@ -334,7 +347,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order // if we have trade, try to query the balance and transfer the balance to the futures wallet account // TODO: handle missing trades here. If the process crashed during the transfer, how to recover? if err := backoff.RetryGeneral(ctx, func() error { - return s.transferIn(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade) + return s.transferIn(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, trade) }); err != nil { log.WithError(err).Errorf("spot-to-futures transfer in retry failed") return @@ -358,7 +371,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order switch s.getPositionState() { case PositionClosing: if err := backoff.RetryGeneral(ctx, func() error { - return s.transferOut(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade) + return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, trade) }); err != nil { log.WithError(err).Errorf("spot-to-futures transfer in retry failed") return @@ -368,7 +381,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order }) s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - s.queryAndDetectPremiumIndex(ctx, binanceFutures) + s.queryAndDetectPremiumIndex(ctx, s.binanceFutures) })) if binanceStream, ok := s.futuresSession.UserDataStream.(*binance.Stream); ok { @@ -448,6 +461,29 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return nil } +func (s *Strategy) syncFundingFeeRecords(ctx context.Context, since time.Time) { + now := time.Now() + q := batch.BinanceFuturesIncomeBatchQuery{ + BinanceFuturesIncomeHistoryService: s.binanceFutures, + } + + dataC, errC := q.Query(ctx, s.Symbol, binanceapi.FuturesIncomeFundingFee, since, now) + for { + select { + case <-ctx.Done(): + return + + case income := <-dataC: + log.Infof("income: %+v", income) + + case err := <-errC: + log.WithError(err).Errorf("unable to query futures income history") + return + + } + } +} + func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFutures *binance.Exchange) { premiumIndex, err := binanceFutures.QueryPremiumIndex(ctx, s.Symbol) if err != nil { From 75cbe10128bfe90e403d8b094e0fb2c213b7cf0a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 15:03:23 +0800 Subject: [PATCH 0705/1392] binance: call auth on futuresClient2 --- pkg/exchange/binance/exchange.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index fdde15815c..43d7554afe 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -126,6 +126,7 @@ func New(key, secret string) *Exchange { if len(key) > 0 && len(secret) > 0 { client2.Auth(key, secret) + futuresClient2.Auth(key, secret) ctx := context.Background() go timeSetterOnce.Do(func() { From cadd3f07950461d55eb66c9822a2aa69caa02bc1 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 15:03:39 +0800 Subject: [PATCH 0706/1392] binanceapi: fix binance futures get income history query --- .../binance/binanceapi/futures_get_income_history_request.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go b/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go index aeacdf8890..dbb7cee6bd 100644 --- a/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go +++ b/pkg/exchange/binance/binanceapi/futures_get_income_history_request.go @@ -35,7 +35,7 @@ type FuturesIncome struct { Asset string `json:"asset"` Info string `json:"info"` Time types.MillisecondTimestamp `json:"time"` - TranId string `json:"tranId"` + TranId int64 `json:"tranId"` TradeId string `json:"tradeId"` } From 88514e8bd95abbe4d0264d59411cc2ba91b7c17a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 15:04:12 +0800 Subject: [PATCH 0707/1392] xfunding: call syncFundingFeeRecords to sync funding fee records --- pkg/strategy/xfunding/strategy.go | 32 +++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index f80341592d..28789c5bc3 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -321,6 +321,9 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.syncFundingFeeRecords(ctx, s.ProfitStats.LastFundingFeeTime) } + // TEST CODE: + // s.syncFundingFeeRecords(ctx, time.Now().Add(-3*24*time.Hour)) + s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { // we act differently on the spot account @@ -463,6 +466,11 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order func (s *Strategy) syncFundingFeeRecords(ctx context.Context, since time.Time) { now := time.Now() + + log.Infof("syncing funding fee records from the income history query: %s <=> %s", since, now) + + defer log.Infof("sync funding fee records done") + q := batch.BinanceFuturesIncomeBatchQuery{ BinanceFuturesIncomeHistoryService: s.binanceFutures, } @@ -473,10 +481,30 @@ func (s *Strategy) syncFundingFeeRecords(ctx context.Context, since time.Time) { case <-ctx.Done(): return - case income := <-dataC: + case income, ok := <-dataC: + if !ok { + return + } + log.Infof("income: %+v", income) + switch income.IncomeType { + case binanceapi.FuturesIncomeFundingFee: + err := s.ProfitStats.AddFundingFee(FundingFee{ + Asset: income.Asset, + Amount: income.Income, + Txn: income.TranId, + Time: income.Time.Time(), + }) + if err != nil { + log.WithError(err).Errorf("can not add funding fee record to ProfitStats") + } + } + + case err, ok := <-errC: + if !ok { + return + } - case err := <-errC: log.WithError(err).Errorf("unable to query futures income history") return From 81799f2c49081bcef3f4a8d3f00091f002dfb01a Mon Sep 17 00:00:00 2001 From: chiahung Date: Mon, 27 Mar 2023 16:03:06 +0800 Subject: [PATCH 0708/1392] FIX: end batch query if start > end --- pkg/exchange/batch/time_range_query.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/pkg/exchange/batch/time_range_query.go b/pkg/exchange/batch/time_range_query.go index 852b23e8f6..7b8490c59d 100644 --- a/pkg/exchange/batch/time_range_query.go +++ b/pkg/exchange/batch/time_range_query.go @@ -72,16 +72,11 @@ func (q *AsyncTimeRangedBatchQuery) Query(ctx context.Context, ch interface{}, s if listLen == 0 { if q.JumpIfEmpty > 0 { - startTime2 := startTime.Add(q.JumpIfEmpty) - if startTime2.After(endTime) { - startTime = endTime - endTime = startTime2 - } else { - startTime = startTime.Add(q.JumpIfEmpty) + startTime = startTime.Add(q.JumpIfEmpty) + if startTime.Before(endTime) { + log.Debugf("batch querying %T: empty records jump to %s", q.Type, startTime) + continue } - - log.Debugf("batch querying %T: empty records jump to %s", q.Type, startTime) - continue } log.Debugf("batch querying %T: empty records, query is completed", q.Type) From 43c4ecc9dad14c3ada42184103af233b6ee73f80 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 16:45:25 +0800 Subject: [PATCH 0709/1392] binance: add MultiAssetsMode related apis --- ...utures_change_multi_assets_mode_request.go | 20 +++ ...ge_multi_assets_mode_request_requestgen.go | 135 ++++++++++++++++++ .../futures_get_multi_assets_mode_request.go | 18 +++ ...et_multi_assets_mode_request_requestgen.go | 135 ++++++++++++++++++ 4 files changed, 308 insertions(+) create mode 100644 pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request.go create mode 100644 pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request.go b/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request.go new file mode 100644 index 0000000000..31b1859df5 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request.go @@ -0,0 +1,20 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" +) + +// Code 200 == success +type FuturesChangeMultiAssetsModeResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +//go:generate requestgen -method POST -url "/fapi/v1/multiAssetsMargin" -type FuturesChangeMultiAssetsModeRequest -responseType FuturesChangeMultiAssetsModeResponse +type FuturesChangeMultiAssetsModeRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *FuturesRestClient) NewFuturesChangeMultiAssetsModeRequest() *FuturesChangeMultiAssetsModeRequest { + return &FuturesChangeMultiAssetsModeRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request_requestgen.go new file mode 100644 index 0000000000..0dabd6c60c --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method POST -url /fapi/v1/multiAssetsMargin -type FuturesChangeMultiAssetsModeRequest -responseType FuturesChangeMultiAssetsModeResponse"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesChangeMultiAssetsModeRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesChangeMultiAssetsModeRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesChangeMultiAssetsModeRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesChangeMultiAssetsModeRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesChangeMultiAssetsModeRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesChangeMultiAssetsModeRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesChangeMultiAssetsModeRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesChangeMultiAssetsModeRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesChangeMultiAssetsModeRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesChangeMultiAssetsModeRequest) Do(ctx context.Context) (*FuturesChangeMultiAssetsModeResponse, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/fapi/v1/multiAssetsMargin" + + req, err := f.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse FuturesChangeMultiAssetsModeResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request.go b/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request.go new file mode 100644 index 0000000000..f52013bb7b --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request.go @@ -0,0 +1,18 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" +) + +type FuturesMultiAssetsModeResponse struct { + MultiAssetsMargin bool `json:"multiAssetsMargin"` +} + +//go:generate requestgen -method POST -url "/fapi/v1/multiAssetsMargin" -type FuturesGetMultiAssetsModeRequest -responseType FuturesMultiAssetsModeResponse +type FuturesGetMultiAssetsModeRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *FuturesRestClient) NewFuturesGetMultiAssetsModeRequest() *FuturesGetMultiAssetsModeRequest { + return &FuturesGetMultiAssetsModeRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request_requestgen.go new file mode 100644 index 0000000000..e957136bcf --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method POST -url /fapi/v1/multiAssetsMargin -type FuturesGetMultiAssetsModeRequest -responseType FuturesMultiAssetsModeResponse"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGetMultiAssetsModeRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGetMultiAssetsModeRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGetMultiAssetsModeRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGetMultiAssetsModeRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGetMultiAssetsModeRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGetMultiAssetsModeRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGetMultiAssetsModeRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGetMultiAssetsModeRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGetMultiAssetsModeRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesGetMultiAssetsModeRequest) Do(ctx context.Context) (*FuturesMultiAssetsModeResponse, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/fapi/v1/multiAssetsMargin" + + req, err := f.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse FuturesMultiAssetsModeResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} From 18d8d63b021b98965d8c80950a5f7ca6bd84492a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 16:59:15 +0800 Subject: [PATCH 0710/1392] binance: add and fix multi assets mode --- ...utures_change_multi_assets_mode_request.go | 9 +++++++ ...ge_multi_assets_mode_request_requestgen.go | 27 +++++++++++++++++-- .../futures_get_multi_assets_mode_request.go | 2 +- ...et_multi_assets_mode_request_requestgen.go | 4 +-- pkg/exchange/binance/futures.go | 4 +++ pkg/strategy/xfunding/strategy.go | 27 +++++++++++++++++++ 6 files changed, 68 insertions(+), 5 deletions(-) diff --git a/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request.go b/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request.go index 31b1859df5..eea4a983f6 100644 --- a/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request.go +++ b/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request.go @@ -4,6 +4,13 @@ import ( "github.com/c9s/requestgen" ) +type MultiAssetsMarginMode string + +const ( + MultiAssetsMarginModeOn MultiAssetsMarginMode = "true" + MultiAssetsMarginModeOff MultiAssetsMarginMode = "false" +) + // Code 200 == success type FuturesChangeMultiAssetsModeResponse struct { Code int `json:"code"` @@ -13,6 +20,8 @@ type FuturesChangeMultiAssetsModeResponse struct { //go:generate requestgen -method POST -url "/fapi/v1/multiAssetsMargin" -type FuturesChangeMultiAssetsModeRequest -responseType FuturesChangeMultiAssetsModeResponse type FuturesChangeMultiAssetsModeRequest struct { client requestgen.AuthenticatedAPIClient + + multiAssetsMargin MultiAssetsMarginMode `param:"multiAssetsMargin"` } func (c *FuturesRestClient) NewFuturesChangeMultiAssetsModeRequest() *FuturesChangeMultiAssetsModeRequest { diff --git a/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request_requestgen.go index 0dabd6c60c..b145487e82 100644 --- a/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request_requestgen.go +++ b/pkg/exchange/binance/binanceapi/futures_change_multi_assets_mode_request_requestgen.go @@ -11,6 +11,11 @@ import ( "regexp" ) +func (f *FuturesChangeMultiAssetsModeRequest) MultiAssetsMargin(multiAssetsMargin MultiAssetsMarginMode) *FuturesChangeMultiAssetsModeRequest { + f.multiAssetsMargin = multiAssetsMargin + return f +} + // GetQueryParameters builds and checks the query parameters and returns url.Values func (f *FuturesChangeMultiAssetsModeRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} @@ -26,6 +31,22 @@ func (f *FuturesChangeMultiAssetsModeRequest) GetQueryParameters() (url.Values, // GetParameters builds and checks the parameters and return the result in a map object func (f *FuturesChangeMultiAssetsModeRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} + // check multiAssetsMargin field -> json key multiAssetsMargin + multiAssetsMargin := f.multiAssetsMargin + + // TEMPLATE check-valid-values + switch multiAssetsMargin { + case MultiAssetsMarginModeOn, MultiAssetsMarginModeOff: + params["multiAssetsMargin"] = multiAssetsMargin + + default: + return nil, fmt.Errorf("multiAssetsMargin value %v is invalid", multiAssetsMargin) + + } + // END TEMPLATE check-valid-values + + // assign parameter of multiAssetsMargin + params["multiAssetsMargin"] = multiAssetsMargin return params, nil } @@ -111,8 +132,10 @@ func (f *FuturesChangeMultiAssetsModeRequest) GetSlugsMap() (map[string]string, func (f *FuturesChangeMultiAssetsModeRequest) Do(ctx context.Context) (*FuturesChangeMultiAssetsModeResponse, error) { - // no body params - var params interface{} + params, err := f.GetParameters() + if err != nil { + return nil, err + } query := url.Values{} apiURL := "/fapi/v1/multiAssetsMargin" diff --git a/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request.go b/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request.go index f52013bb7b..1ea85fb98a 100644 --- a/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request.go +++ b/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request.go @@ -8,7 +8,7 @@ type FuturesMultiAssetsModeResponse struct { MultiAssetsMargin bool `json:"multiAssetsMargin"` } -//go:generate requestgen -method POST -url "/fapi/v1/multiAssetsMargin" -type FuturesGetMultiAssetsModeRequest -responseType FuturesMultiAssetsModeResponse +//go:generate requestgen -method GET -url "/fapi/v1/multiAssetsMargin" -type FuturesGetMultiAssetsModeRequest -responseType FuturesMultiAssetsModeResponse type FuturesGetMultiAssetsModeRequest struct { client requestgen.AuthenticatedAPIClient } diff --git a/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request_requestgen.go index e957136bcf..83b1f8979a 100644 --- a/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request_requestgen.go +++ b/pkg/exchange/binance/binanceapi/futures_get_multi_assets_mode_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method POST -url /fapi/v1/multiAssetsMargin -type FuturesGetMultiAssetsModeRequest -responseType FuturesMultiAssetsModeResponse"; DO NOT EDIT. +// Code generated by "requestgen -method GET -url /fapi/v1/multiAssetsMargin -type FuturesGetMultiAssetsModeRequest -responseType FuturesMultiAssetsModeResponse"; DO NOT EDIT. package binanceapi @@ -117,7 +117,7 @@ func (f *FuturesGetMultiAssetsModeRequest) Do(ctx context.Context) (*FuturesMult apiURL := "/fapi/v1/multiAssetsMargin" - req, err := f.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) if err != nil { return nil, err } diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index 224a185999..bf2b28031f 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -366,6 +366,10 @@ func (e *Exchange) queryFuturesDepth(ctx context.Context, symbol string) (snapsh return convertDepth(snapshot, symbol, finalUpdateID, response) } +func (e *Exchange) GetFuturesClient() *binanceapi.FuturesRestClient { + return e.futuresClient2 +} + // QueryFuturesIncomeHistory queries the income history on the binance futures account // This is more binance futures specific API, the convert function is not designed yet. // TODO: consider other futures platforms and design the common data structure for this diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 28789c5bc3..23d3eead75 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -270,6 +270,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return errNotBinanceExchange } + if err := s.checkAndFixMarginMode(ctx); err != nil { + return err + } + // adjust QuoteInvestment if b, ok := s.spotSession.Account.Balance(s.spotMarket.QuoteCurrency); ok { originalQuoteInvestment := s.QuoteInvestment @@ -951,3 +955,26 @@ func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.Exch }) return orderExecutor } + +func (s *Strategy) checkAndFixMarginMode(ctx context.Context) error { + futuresClient := s.binanceFutures.GetFuturesClient() + req := futuresClient.NewFuturesGetMultiAssetsModeRequest() + resp, err := req.Do(ctx) + if err != nil { + return err + } + + if resp.MultiAssetsMargin { + return nil + } + + fixReq := futuresClient.NewFuturesChangeMultiAssetsModeRequest() + fixReq.MultiAssetsMargin(binanceapi.MultiAssetsMarginModeOn) + fixResp, err := fixReq.Do(ctx) + if err != nil { + return err + } + + log.Infof("changeMultiAssetsMode response: %+v", fixResp) + return nil +} From aa6feed272ae543b751eeacf9ad2ebf2cf8de0e7 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 17:08:34 +0800 Subject: [PATCH 0711/1392] binance: add FuturesChangeInitialLeverageRequest api --- ...futures_change_initial_leverage_request.go | 25 +++ ...nge_initial_leverage_request_requestgen.go | 157 ++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request.go create mode 100644 pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request.go b/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request.go new file mode 100644 index 0000000000..1d3cb2ce7b --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request.go @@ -0,0 +1,25 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type FuturesChangeInitialLeverageResponse struct { + Leverage int `json:"leverage"` + MaxNotionalValue fixedpoint.Value `json:"maxNotionalValue"` + Symbol string `json:"symbol"` +} + +//go:generate requestgen -method POST -url "/fapi/v1/multiAssetsMargin" -type FuturesChangeInitialLeverageRequest -responseType FuturesChangeInitialLeverageResponse +type FuturesChangeInitialLeverageRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` + leverage int `param:"leverage"` +} + +func (c *FuturesRestClient) NewFuturesChangeInitialLeverageRequest() *FuturesChangeInitialLeverageRequest { + return &FuturesChangeInitialLeverageRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request_requestgen.go new file mode 100644 index 0000000000..cb23945dde --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request_requestgen.go @@ -0,0 +1,157 @@ +// Code generated by "requestgen -method POST -url /fapi/v1/multiAssetsMargin -type FuturesChangeInitialLeverageRequest -responseType FuturesChangeInitialLeverageResponse"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (f *FuturesChangeInitialLeverageRequest) Symbol(symbol string) *FuturesChangeInitialLeverageRequest { + f.symbol = symbol + return f +} + +func (f *FuturesChangeInitialLeverageRequest) Leverage(leverage int) *FuturesChangeInitialLeverageRequest { + f.leverage = leverage + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesChangeInitialLeverageRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesChangeInitialLeverageRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := f.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check leverage field -> json key leverage + leverage := f.leverage + + // assign parameter of leverage + params["leverage"] = leverage + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesChangeInitialLeverageRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesChangeInitialLeverageRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesChangeInitialLeverageRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesChangeInitialLeverageRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesChangeInitialLeverageRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesChangeInitialLeverageRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesChangeInitialLeverageRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesChangeInitialLeverageRequest) Do(ctx context.Context) (*FuturesChangeInitialLeverageResponse, error) { + + params, err := f.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/fapi/v1/multiAssetsMargin" + + req, err := f.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse FuturesChangeInitialLeverageResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} From 38ba567558b52bc64026f1194172c4102b74c97f Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 17:14:29 +0800 Subject: [PATCH 0712/1392] xfunding: fix and call FuturesChangeInitialLeverageRequest --- .../futures_change_initial_leverage_request.go | 2 +- ...ange_initial_leverage_request_requestgen.go | 4 ++-- pkg/strategy/xfunding/strategy.go | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request.go b/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request.go index 1d3cb2ce7b..e31b932df1 100644 --- a/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request.go +++ b/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request.go @@ -12,7 +12,7 @@ type FuturesChangeInitialLeverageResponse struct { Symbol string `json:"symbol"` } -//go:generate requestgen -method POST -url "/fapi/v1/multiAssetsMargin" -type FuturesChangeInitialLeverageRequest -responseType FuturesChangeInitialLeverageResponse +//go:generate requestgen -method POST -url "/fapi/v1/leverage" -type FuturesChangeInitialLeverageRequest -responseType FuturesChangeInitialLeverageResponse type FuturesChangeInitialLeverageRequest struct { client requestgen.AuthenticatedAPIClient diff --git a/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request_requestgen.go index cb23945dde..c6a1eb6954 100644 --- a/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request_requestgen.go +++ b/pkg/exchange/binance/binanceapi/futures_change_initial_leverage_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method POST -url /fapi/v1/multiAssetsMargin -type FuturesChangeInitialLeverageRequest -responseType FuturesChangeInitialLeverageResponse"; DO NOT EDIT. +// Code generated by "requestgen -method POST -url /fapi/v1/leverage -type FuturesChangeInitialLeverageRequest -responseType FuturesChangeInitialLeverageResponse"; DO NOT EDIT. package binanceapi @@ -137,7 +137,7 @@ func (f *FuturesChangeInitialLeverageRequest) Do(ctx context.Context) (*FuturesC } query := url.Values{} - apiURL := "/fapi/v1/multiAssetsMargin" + apiURL := "/fapi/v1/leverage" req, err := f.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) if err != nil { diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 23d3eead75..0e23944cc7 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -274,6 +274,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return err } + if err := s.setInitialLeverage(ctx); err != nil { + return err + } + // adjust QuoteInvestment if b, ok := s.spotSession.Account.Balance(s.spotMarket.QuoteCurrency); ok { originalQuoteInvestment := s.QuoteInvestment @@ -956,6 +960,20 @@ func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.Exch return orderExecutor } +func (s *Strategy) setInitialLeverage(ctx context.Context) error { + futuresClient := s.binanceFutures.GetFuturesClient() + req := futuresClient.NewFuturesChangeInitialLeverageRequest() + req.Symbol(s.Symbol) + req.Leverage(s.Leverage.Int() + 1) + resp, err := req.Do(ctx) + if err != nil { + return err + } + + log.Infof("adjusted initial leverage: %+v", resp) + return nil +} + func (s *Strategy) checkAndFixMarginMode(ctx context.Context) error { futuresClient := s.binanceFutures.GetFuturesClient() req := futuresClient.NewFuturesGetMultiAssetsModeRequest() From 38778ff7569a36332843dd413ecdfd50c3388d6b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 17:46:54 +0800 Subject: [PATCH 0713/1392] bbgo: fix order executor ClosePosition for order executor --- pkg/bbgo/order_executor_general.go | 18 +++++----- .../futures_get_position_risks_request.go | 4 +-- pkg/strategy/xfunding/strategy.go | 35 ++++++++++++++++++- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index daba2999ca..6c2bd9d570 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -498,20 +498,22 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix defer atomic.StoreInt64(&e.closing, 0) if e.session.Futures { // Futures: Use base qty in e.position - submitOrder.Quantity = e.position.GetBase().Abs() - submitOrder.ReduceOnly = true + if percentage.Compare(fixedpoint.One) == 0 { + submitOrder.ClosePosition = true + submitOrder.Quantity = fixedpoint.Zero + } else { + submitOrder.Quantity = e.position.GetBase().Abs() + submitOrder.ReduceOnly = true + } + if e.position.IsLong() { submitOrder.Side = types.SideTypeSell } else if e.position.IsShort() { submitOrder.Side = types.SideTypeBuy } else { - submitOrder.Side = types.SideTypeSelf - submitOrder.Quantity = fixedpoint.Zero + return fmt.Errorf("unexpected position side: %+v", e.position) } - if submitOrder.Quantity.IsZero() { - return fmt.Errorf("no position to close: %+v", submitOrder) - } } else { // Spot and spot margin // check base balance and adjust the close position order if e.position.IsLong() { @@ -535,7 +537,7 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix tagStr := strings.Join(tags, ",") submitOrder.Tag = tagStr - Notify("Closing %s position %s with tags: %v", e.symbol, percentage.Percentage(), tagStr) + Notify("Closing %s position %s with tags: %s", e.symbol, percentage.Percentage(), tagStr) _, err := e.SubmitOrders(ctx, *submitOrder) return err diff --git a/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go b/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go index 56efd19ce5..18fe8da5b1 100644 --- a/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go +++ b/pkg/exchange/binance/binanceapi/futures_get_position_risks_request.go @@ -8,7 +8,7 @@ import ( ) type FuturesPositionRisk struct { - EntryPrice string `json:"entryPrice"` + EntryPrice fixedpoint.Value `json:"entryPrice"` MarginType string `json:"marginType"` IsAutoAddMargin string `json:"isAutoAddMargin"` IsolatedMargin string `json:"isolatedMargin"` @@ -32,6 +32,6 @@ type FuturesGetPositionRisksRequest struct { symbol string `param:"symbol"` } -func (c *FuturesRestClient) NewGetPositionRisksRequest() *FuturesGetPositionRisksRequest { +func (c *FuturesRestClient) NewFuturesGetPositionRisksRequest() *FuturesGetPositionRisksRequest { return &FuturesGetPositionRisksRequest{client: c} } diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 0e23944cc7..d8bf80b54d 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -132,7 +132,11 @@ type Strategy struct { SpotSession string `json:"spotSession"` FuturesSession string `json:"futuresSession"` - Reset bool `json:"reset"` + + // Reset your position info + Reset bool `json:"reset"` + + CloseFuturesPosition bool `json:"closeFuturesPosition"` ProfitStats *ProfitStats `persistence:"profit_stats"` @@ -278,6 +282,14 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return err } + if err := s.checkPositionRisks(ctx); err != nil { + return err + } + + if s.CloseFuturesPosition && s.Reset { + return errors.New("reset and closeFuturesPosition can not be used together") + } + // adjust QuoteInvestment if b, ok := s.spotSession.Account.Balance(s.spotMarket.QuoteCurrency); ok { originalQuoteInvestment := s.QuoteInvestment @@ -395,6 +407,14 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.queryAndDetectPremiumIndex(ctx, s.binanceFutures) })) + s.futuresSession.UserDataStream.OnStart(func() { + if s.CloseFuturesPosition { + if err := s.futuresOrderExecutor.ClosePosition(ctx, fixedpoint.One); err != nil { + log.WithError(err).Errorf("close position error") + } + } + }) + if binanceStream, ok := s.futuresSession.UserDataStream.(*binance.Stream); ok { binanceStream.OnAccountUpdateEvent(func(e *binance.AccountUpdateEvent) { switch e.AccountUpdate.EventReasonType { @@ -996,3 +1016,16 @@ func (s *Strategy) checkAndFixMarginMode(ctx context.Context) error { log.Infof("changeMultiAssetsMode response: %+v", fixResp) return nil } + +func (s *Strategy) checkPositionRisks(ctx context.Context) error { + futuresClient := s.binanceFutures.GetFuturesClient() + req := futuresClient.NewFuturesGetPositionRisksRequest() + req.Symbol(s.Symbol) + resp, err := req.Do(ctx) + if err != nil { + return err + } + + log.Infof("positions: %+v", resp) + return nil +} From 8257c4ffbe0d17bdedb3435946af1887061da919 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 18:28:25 +0800 Subject: [PATCH 0714/1392] xfunding: fix ClosePosition call for futures --- config/xfunding.yaml | 2 ++ pkg/bbgo/order_executor_general.go | 9 ++------- pkg/exchange/binance/futures.go | 21 ++++++++++++++------- pkg/strategy/xfunding/strategy.go | 29 ++++++++++++++++++----------- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 259b7f78df..efada033d0 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -60,3 +60,5 @@ crossExchangeStrategies: ## reset will reset the spot/futures positions, the transfer stats and the position state. # reset: true + + # closeFuturesPosition: true diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 6c2bd9d570..a845ef4999 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -498,13 +498,8 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix defer atomic.StoreInt64(&e.closing, 0) if e.session.Futures { // Futures: Use base qty in e.position - if percentage.Compare(fixedpoint.One) == 0 { - submitOrder.ClosePosition = true - submitOrder.Quantity = fixedpoint.Zero - } else { - submitOrder.Quantity = e.position.GetBase().Abs() - submitOrder.ReduceOnly = true - } + submitOrder.Quantity = e.position.GetBase().Abs() + submitOrder.ReduceOnly = true if e.position.IsLong() { submitOrder.Side = types.SideTypeSell diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index bf2b28031f..bd70e3bdbe 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -129,8 +129,13 @@ func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrd req := e.futuresClient.NewCreateOrderService(). Symbol(order.Symbol). Type(orderType). - Side(futures.SideType(order.Side)). - ReduceOnly(order.ReduceOnly) + Side(futures.SideType(order.Side)) + + if order.ReduceOnly { + req.ReduceOnly(order.ReduceOnly) + } else if order.ClosePosition { + req.ClosePosition(order.ClosePosition) + } clientOrderID := newFuturesClientOrderID(order.ClientOrderID) if len(clientOrderID) > 0 { @@ -140,11 +145,13 @@ func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrd // use response result format req.NewOrderResponseType(futures.NewOrderRespTypeRESULT) - if order.Market.Symbol != "" { - req.Quantity(order.Market.FormatQuantity(order.Quantity)) - } else { - // TODO report error - req.Quantity(order.Quantity.FormatString(8)) + if !order.ClosePosition { + if order.Market.Symbol != "" { + req.Quantity(order.Market.FormatQuantity(order.Quantity)) + } else { + // TODO report error + req.Quantity(order.Quantity.FormatString(8)) + } } // set price field for limit orders diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index d8bf80b54d..66c167c604 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -282,14 +282,6 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return err } - if err := s.checkPositionRisks(ctx); err != nil { - return err - } - - if s.CloseFuturesPosition && s.Reset { - return errors.New("reset and closeFuturesPosition can not be used together") - } - // adjust QuoteInvestment if b, ok := s.spotSession.Account.Balance(s.spotMarket.QuoteCurrency); ok { originalQuoteInvestment := s.QuoteInvestment @@ -332,6 +324,14 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.State = newState() } + if err := s.checkAndRestorePositionRisks(ctx); err != nil { + return err + } + + if s.CloseFuturesPosition && s.Reset { + return errors.New("reset and closeFuturesPosition can not be used together") + } + log.Infof("loaded spot position: %s", s.SpotPosition.String()) log.Infof("loaded futures position: %s", s.FuturesPosition.String()) log.Infof("loaded neutral position: %s", s.NeutralPosition.String()) @@ -1017,15 +1017,22 @@ func (s *Strategy) checkAndFixMarginMode(ctx context.Context) error { return nil } -func (s *Strategy) checkPositionRisks(ctx context.Context) error { +func (s *Strategy) checkAndRestorePositionRisks(ctx context.Context) error { futuresClient := s.binanceFutures.GetFuturesClient() req := futuresClient.NewFuturesGetPositionRisksRequest() req.Symbol(s.Symbol) - resp, err := req.Do(ctx) + positionRisks, err := req.Do(ctx) if err != nil { return err } - log.Infof("positions: %+v", resp) + for _, positionRisk := range positionRisks { + if positionRisk.Symbol == s.Symbol { + s.FuturesPosition.Base = positionRisk.PositionAmount + s.FuturesPosition.AverageCost = positionRisk.EntryPrice + } + } + + log.Infof("positions: %+v", positionRisks) return nil } From c437837210b13a689298235643b1cf119bc0f59b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 21:36:29 +0800 Subject: [PATCH 0715/1392] xfunding: improve checkAndRestorePositionRisks --- pkg/strategy/xfunding/strategy.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 66c167c604..b42c65f594 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -1027,12 +1027,21 @@ func (s *Strategy) checkAndRestorePositionRisks(ctx context.Context) error { } for _, positionRisk := range positionRisks { - if positionRisk.Symbol == s.Symbol { - s.FuturesPosition.Base = positionRisk.PositionAmount - s.FuturesPosition.AverageCost = positionRisk.EntryPrice + if positionRisk.Symbol != s.Symbol { + continue } + + if positionRisk.PositionAmount.IsZero() || positionRisk.EntryPrice.IsZero() { + continue + } + + s.FuturesPosition.Base = positionRisk.PositionAmount + s.FuturesPosition.AverageCost = positionRisk.EntryPrice + log.Infof("restored futures position from positionRisk: base=%s, average_cost=%s, position_risk=%+v", + s.FuturesPosition.Base.String(), + s.FuturesPosition.AverageCost.String(), + positionRisk) } - log.Infof("positions: %+v", positionRisks) return nil } From 4d59edc3d17d783e7d69219baa123520a0cc3f6b Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 16:09:29 +0800 Subject: [PATCH 0716/1392] xfunding: add funding fee slack attachment support --- config/xfunding.yaml | 2 +- pkg/strategy/xfunding/fundingfee.go | 29 ++++++++++++++++++++++++++++ pkg/strategy/xfunding/profitstats.go | 7 ------- 3 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 pkg/strategy/xfunding/fundingfee.go diff --git a/config/xfunding.yaml b/config/xfunding.yaml index efada033d0..e18576c9ab 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -54,7 +54,7 @@ crossExchangeStrategies: ## shortFundingRate is the funding rate range you want to create your position shortFundingRate: ## when funding rate is higher than this high value, the strategy will start buying spot and opening a short position - high: 0.001% + high: 0.0005% ## when funding rate is lower than this low value, the strategy will start closing futures position and sell the spot low: -0.01% diff --git a/pkg/strategy/xfunding/fundingfee.go b/pkg/strategy/xfunding/fundingfee.go new file mode 100644 index 0000000000..a07fad0c6e --- /dev/null +++ b/pkg/strategy/xfunding/fundingfee.go @@ -0,0 +1,29 @@ +package xfunding + +import ( + "fmt" + "time" + + "github.com/slack-go/slack" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/style" +) + +type FundingFee struct { + Asset string `json:"asset"` + Amount fixedpoint.Value `json:"amount"` + Txn int64 `json:"txn"` + Time time.Time `json:"time"` +} + +func (f *FundingFee) SlackAttachment() slack.Attachment { + return slack.Attachment{ + Title: "Funding Fee " + fmt.Sprintf("%s %s", style.PnLSignString(f.Amount), f.Asset), + Color: style.PnLColor(f.Amount), + // Pretext: "", + // Text: text, + Fields: []slack.AttachmentField{}, + Footer: fmt.Sprintf("Transation ID: %d Transaction Time %s", f.Txn, f.Time.Format(time.RFC822)), + } +} diff --git a/pkg/strategy/xfunding/profitstats.go b/pkg/strategy/xfunding/profitstats.go index 93e3b6df9f..c8eb58af82 100644 --- a/pkg/strategy/xfunding/profitstats.go +++ b/pkg/strategy/xfunding/profitstats.go @@ -8,13 +8,6 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -type FundingFee struct { - Asset string `json:"asset"` - Amount fixedpoint.Value `json:"amount"` - Txn int64 `json:"txn"` - Time time.Time `json:"time"` -} - type ProfitStats struct { *types.ProfitStats From 6f961556d7ffb4a54e1efdcb57201bc883cb7a76 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 16:12:22 +0800 Subject: [PATCH 0717/1392] xfunding: add SlackAttachment method support on profit stats --- pkg/strategy/xfunding/profitstats.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/strategy/xfunding/profitstats.go b/pkg/strategy/xfunding/profitstats.go index c8eb58af82..6ae2780b51 100644 --- a/pkg/strategy/xfunding/profitstats.go +++ b/pkg/strategy/xfunding/profitstats.go @@ -4,7 +4,10 @@ import ( "fmt" "time" + "github.com/slack-go/slack" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/style" "github.com/c9s/bbgo/pkg/types" ) @@ -18,6 +21,20 @@ type ProfitStats struct { LastFundingFeeTime time.Time `json:"lastFundingFeeTime"` } +func (s *ProfitStats) SlackAttachment() slack.Attachment { + var fields []slack.AttachmentField + var totalProfit = fmt.Sprintf("Total Funding Fee Profit: %s %s", style.PnLSignString(s.TotalFundingFee), s.FundingFeeCurrency) + + return slack.Attachment{ + Title: totalProfit, + Color: style.PnLColor(s.TotalFundingFee), + // Pretext: "", + // Text: text, + Fields: fields, + Footer: fmt.Sprintf("Last Funding Fee Transation ID: %d Last Funding Fee Time %s", s.LastFundingFeeTxn, s.LastFundingFeeTime.Format(time.RFC822)), + } +} + func (s *ProfitStats) AddFundingFee(fee FundingFee) error { if s.FundingFeeCurrency == "" { s.FundingFeeCurrency = fee.Asset From 1e7afbc0c844f74a8ad7f4d22298306f072a3958 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 16:24:33 +0800 Subject: [PATCH 0718/1392] xfunding: fix position ready set call --- config/xfunding.yaml | 2 +- pkg/strategy/xfunding/strategy.go | 67 ++++++++++++++++--------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index e18576c9ab..972d3a13e2 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -54,7 +54,7 @@ crossExchangeStrategies: ## shortFundingRate is the funding rate range you want to create your position shortFundingRate: ## when funding rate is higher than this high value, the strategy will start buying spot and opening a short position - high: 0.0005% + high: 0.0001% ## when funding rate is lower than this low value, the strategy will start closing futures position and sell the spot low: -0.01% diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index b42c65f594..e718abde6c 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -332,6 +332,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return errors.New("reset and closeFuturesPosition can not be used together") } + log.Infof("state: %+v", s.State) log.Infof("loaded spot position: %s", s.SpotPosition.String()) log.Infof("loaded futures position: %s", s.FuturesPosition.String()) log.Infof("loaded neutral position: %s", s.NeutralPosition.String()) @@ -672,40 +673,46 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { s.State.TotalBaseTransfer.Mul(s.Leverage)) // if - futures position < max futures position, increase it - if futuresBase.Neg().Compare(maxFuturesBasePosition) < 0 { - orderPrice := ticker.Sell - diffQuantity := maxFuturesBasePosition.Sub(futuresBase.Neg()) + if futuresBase.Neg().Compare(maxFuturesBasePosition) >= 0 { + s.setPositionState(PositionReady) - if diffQuantity.Sign() < 0 { - log.Errorf("unexpected negative position diff: %s", diffQuantity.String()) - return - } + // DEBUG CODE - triggering closing position automatically + // s.startClosingPosition() + return + } - log.Infof("position diff quantity: %s", diffQuantity.String()) + orderPrice := ticker.Sell + diffQuantity := maxFuturesBasePosition.Sub(futuresBase.Neg()) - orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) - orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) - if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { - log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) - return - } + if diffQuantity.Sign() < 0 { + log.Errorf("unexpected negative position diff: %s", diffQuantity.String()) + return + } - createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Quantity: orderQuantity, - Price: orderPrice, - Market: s.futuresMarket, - }) + log.Infof("position diff quantity: %s", diffQuantity.String()) - if err != nil { - log.WithError(err).Errorf("can not submit order") - return - } + orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) + orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) + return + } - log.Infof("created orders: %+v", createdOrders) + createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.futuresMarket, + }) + + if err != nil { + log.WithError(err).Errorf("can not submit order") + return } + + log.Infof("created orders: %+v", createdOrders) } func (s *Strategy) syncSpotPosition(ctx context.Context) { @@ -812,11 +819,7 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { s.mu.Unlock() if usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - // stop increase the position - s.setPositionState(PositionReady) - - // DEBUG CODE - triggering closing position automatically - // s.startClosingPosition() + // stop increase the stop position return } From eeda500a90d190b4d650767c62613edab77a3174 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 16:29:06 +0800 Subject: [PATCH 0719/1392] xfunding: pull out handleAccountUpdate handler --- pkg/strategy/xfunding/strategy.go | 82 ++++++++++++++++--------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index e718abde6c..ece48980e3 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -418,45 +418,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if binanceStream, ok := s.futuresSession.UserDataStream.(*binance.Stream); ok { binanceStream.OnAccountUpdateEvent(func(e *binance.AccountUpdateEvent) { - switch e.AccountUpdate.EventReasonType { - case binance.AccountUpdateEventReasonDeposit: - case binance.AccountUpdateEventReasonWithdraw: - case binance.AccountUpdateEventReasonFundingFee: - // EventBase:{ - // Event:ACCOUNT_UPDATE - // Time:1679760000932 - // } - // Transaction:1679760000927 - // AccountUpdate:{ - // EventReasonType:FUNDING_FEE - // Balances:[{ - // Asset:USDT - // WalletBalance:56.64251742 - // CrossWalletBalance:56.64251742 - // BalanceChange:-0.00037648 - // }] - // } - // } - for _, b := range e.AccountUpdate.Balances { - if b.Asset != s.ProfitStats.FundingFeeCurrency { - continue - } - - txnTime := time.UnixMilli(e.Time) - err := s.ProfitStats.AddFundingFee(FundingFee{ - Asset: b.Asset, - Amount: b.BalanceChange, - Txn: e.Transaction, - Time: txnTime, - }) - if err != nil { - log.WithError(err).Error("unable to add funding fee to profitStats") - } - } - - log.Infof("total collected funding fee: %f %s", s.ProfitStats.TotalFundingFee.Float64(), s.ProfitStats.FundingFeeCurrency) - bbgo.Sync(ctx, s) - } + s.handleAccountUpdate(ctx, e) }) } @@ -493,6 +455,48 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return nil } +func (s *Strategy) handleAccountUpdate(ctx context.Context, e *binance.AccountUpdateEvent) { + switch e.AccountUpdate.EventReasonType { + case binance.AccountUpdateEventReasonDeposit: + case binance.AccountUpdateEventReasonWithdraw: + case binance.AccountUpdateEventReasonFundingFee: + // EventBase:{ + // Event:ACCOUNT_UPDATE + // Time:1679760000932 + // } + // Transaction:1679760000927 + // AccountUpdate:{ + // EventReasonType:FUNDING_FEE + // Balances:[{ + // Asset:USDT + // WalletBalance:56.64251742 + // CrossWalletBalance:56.64251742 + // BalanceChange:-0.00037648 + // }] + // } + // } + for _, b := range e.AccountUpdate.Balances { + if b.Asset != s.ProfitStats.FundingFeeCurrency { + continue + } + + txnTime := time.UnixMilli(e.Time) + err := s.ProfitStats.AddFundingFee(FundingFee{ + Asset: b.Asset, + Amount: b.BalanceChange, + Txn: e.Transaction, + Time: txnTime, + }) + if err != nil { + log.WithError(err).Error("unable to add funding fee to profitStats") + } + } + + log.Infof("total collected funding fee: %f %s", s.ProfitStats.TotalFundingFee.Float64(), s.ProfitStats.FundingFeeCurrency) + bbgo.Sync(ctx, s) + } +} + func (s *Strategy) syncFundingFeeRecords(ctx context.Context, since time.Time) { now := time.Now() From 0f88309d9e5bea75563500bdadaafe37dfc1faab Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 16:31:25 +0800 Subject: [PATCH 0720/1392] xfunding: add notifications --- pkg/strategy/xfunding/strategy.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index ece48980e3..30c4e96350 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -481,19 +481,25 @@ func (s *Strategy) handleAccountUpdate(ctx context.Context, e *binance.AccountUp } txnTime := time.UnixMilli(e.Time) - err := s.ProfitStats.AddFundingFee(FundingFee{ + fee := FundingFee{ Asset: b.Asset, Amount: b.BalanceChange, Txn: e.Transaction, Time: txnTime, - }) + } + err := s.ProfitStats.AddFundingFee(fee) if err != nil { log.WithError(err).Error("unable to add funding fee to profitStats") + continue } + + bbgo.Notify(fee) } log.Infof("total collected funding fee: %f %s", s.ProfitStats.TotalFundingFee.Float64(), s.ProfitStats.FundingFeeCurrency) bbgo.Sync(ctx, s) + + bbgo.Notify(s.ProfitStats) } } @@ -680,6 +686,10 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { if futuresBase.Neg().Compare(maxFuturesBasePosition) >= 0 { s.setPositionState(PositionReady) + bbgo.Notify("SpotPosition", s.SpotPosition) + bbgo.Notify("FuturesPosition", s.FuturesPosition) + bbgo.Notify("NeutralPosition", s.NeutralPosition) + // DEBUG CODE - triggering closing position automatically // s.startClosingPosition() return From 16cb68ac3ebdac713263528fc650d156d53557b9 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 17:22:16 +0800 Subject: [PATCH 0721/1392] xfunding: change dust quantity info log to warn log --- pkg/strategy/xfunding/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 30c4e96350..56ff768334 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -708,7 +708,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { - log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) + log.Warnf("unexpected dust quantity, skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) return } From ce0b73b6e44f7a44a3e66dd2238121649e9e0d6c Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 17:44:48 +0800 Subject: [PATCH 0722/1392] xfunding: calculate max minQuantity from spot market and future market --- pkg/strategy/xfunding/strategy.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 56ff768334..b79cafb810 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -168,6 +168,8 @@ type Strategy struct { // positionType is the futures position type // currently we only support short position for the positive funding rate positionType types.PositionType + + minQuantity fixedpoint.Value } func (s *Strategy) ID() string { @@ -308,6 +310,9 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } } + // common min quantity + s.minQuantity = fixedpoint.Max(s.futuresMarket.MinQuantity, s.spotMarket.MinQuantity) + if s.SpotPosition == nil || s.Reset { s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) } @@ -607,7 +612,7 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { if futuresBase.Compare(fixedpoint.Zero) < 0 { orderPrice := ticker.Buy orderQuantity := futuresBase.Abs() - orderQuantity = fixedpoint.Max(orderQuantity, s.futuresMarket.MinQuantity) + orderQuantity = fixedpoint.Max(orderQuantity, s.minQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) @@ -705,7 +710,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { log.Infof("position diff quantity: %s", diffQuantity.String()) - orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) + orderQuantity := fixedpoint.Max(diffQuantity, s.minQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { log.Warnf("unexpected dust quantity, skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) @@ -852,7 +857,7 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { log.Infof("initial spot order quantity %s", orderQuantity.String()) - orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) + orderQuantity = fixedpoint.Max(orderQuantity, s.minQuantity) orderQuantity = s.spotMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) if s.spotMarket.IsDustQuantity(orderQuantity, orderPrice) { From 088a36a1698149c07c105ee9d98fa215dfc1a4bf Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 17:49:33 +0800 Subject: [PATCH 0723/1392] xfunding: refactor transferIn --- pkg/strategy/xfunding/strategy.go | 2 +- pkg/strategy/xfunding/transfer.go | 19 ++++++------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index b79cafb810..ee06a6985a 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -376,7 +376,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order // if we have trade, try to query the balance and transfer the balance to the futures wallet account // TODO: handle missing trades here. If the process crashed during the transfer, how to recover? if err := backoff.RetryGeneral(ctx, func() error { - return s.transferIn(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, trade) + return s.transferIn(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, trade.Quantity) }); err != nil { log.WithError(err).Errorf("spot-to-futures transfer in retry failed") return diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 12bcd2b6b7..dd53d159a7 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -77,27 +77,20 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency return nil } -func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, currency string, trade types.Trade) error { - - // base asset needs BUY trades - if trade.Side == types.SideTypeSell { - return nil - } - +func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, asset string, quantity fixedpoint.Value) error { balances, err := s.spotSession.Exchange.QueryAccountBalances(ctx) if err != nil { return err } - b, ok := balances[currency] + b, ok := balances[asset] if !ok { - return fmt.Errorf("%s balance not found", currency) + return fmt.Errorf("%s balance not found", asset) } // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - quantity := trade.Quantity if b.Available.Compare(quantity) < 0 { - log.Infof("adding to pending base transfer: %s %s", quantity, currency) + log.Infof("adding to pending base transfer: %s %s", quantity, asset) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } @@ -113,8 +106,8 @@ func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, currency amount = fixedpoint.Min(rest, amount) - log.Infof("transfering in futures account asset %s %s", amount, currency) - if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferIn); err != nil { + log.Infof("transfering in futures account asset %s %s", amount, asset) + if err := ex.TransferFuturesAccountAsset(ctx, asset, amount, types.TransferIn); err != nil { return err } From bc6ee59add65655aa08b6a2153a222a2a3f1ee3b Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 26 Mar 2023 17:51:08 +0800 Subject: [PATCH 0724/1392] xfunding: refactor transferOut with trade quantity --- pkg/strategy/xfunding/strategy.go | 2 +- pkg/strategy/xfunding/transfer.go | 24 +++++++++--------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index ee06a6985a..f0ae55da91 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -400,7 +400,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order switch s.getPositionState() { case PositionClosing: if err := backoff.RetryGeneral(ctx, func() error { - return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, trade) + return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, trade.Quantity) }); err != nil { log.WithError(err).Errorf("spot-to-futures transfer in retry failed") return diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index dd53d159a7..9d45307fca 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -13,33 +13,27 @@ type FuturesTransfer interface { QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) } -func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency string, trade types.Trade) error { - // base asset needs BUY trades - if trade.Side != types.SideTypeBuy { - return nil - } - +func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset string, tradeQuantity fixedpoint.Value) error { // if transfer done if s.State.TotalBaseTransfer.IsZero() { return nil } // de-leverage and get the collateral base quantity for transfer - quantity := trade.Quantity - quantity = quantity.Div(s.Leverage) + quantity := tradeQuantity.Div(s.Leverage) balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) if err != nil { - log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), currency, s.State.PendingBaseTransfer.String()) + log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return err } - b, ok := balances[currency] + b, ok := balances[asset] if !ok { - log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), currency, s.State.PendingBaseTransfer.String()) + log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) - return fmt.Errorf("%s balance not found", currency) + return fmt.Errorf("%s balance not found", asset) } // add the previous pending base transfer and the current trade quantity @@ -53,7 +47,7 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here if amount.IsZero() { - log.Infof("adding to pending base transfer: %s %s + %s ", quantity.String(), currency, s.State.PendingBaseTransfer.String()) + log.Infof("adding to pending base transfer: %s %s + %s ", quantity.String(), asset, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } @@ -64,8 +58,8 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency // if s.State.TotalBaseTransfer.Compare(collateralBase) - log.Infof("transfering out futures account asset %s %s", amount, currency) - if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferOut); err != nil { + log.Infof("transfering out futures account asset %s %s", amount, asset) + if err := ex.TransferFuturesAccountAsset(ctx, asset, amount, types.TransferOut); err != nil { return err } From a2fdc997419c7ad9e27e0b4e0c9a3256798a9d7a Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 27 Mar 2023 13:34:24 +0800 Subject: [PATCH 0725/1392] xfunding: notify position ready --- config/xfunding.yaml | 2 +- pkg/strategy/xfunding/strategy.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 972d3a13e2..68f343982d 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -44,7 +44,7 @@ crossExchangeStrategies: ## incrementalQuoteQuantity is the quote quantity per maker order when creating the positions ## when in BTC-USDT 20 means 20 USDT, each buy order will hold 20 USDT quote amount. - incrementalQuoteQuantity: 20 + incrementalQuoteQuantity: 10 ## quoteInvestment is how much you want to invest to create your position. ## for example, when 10k USDT is given as the quote investment, and the average executed price of your position is around BTC 18k diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index f0ae55da91..839d8dbb06 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -691,6 +691,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { if futuresBase.Neg().Compare(maxFuturesBasePosition) >= 0 { s.setPositionState(PositionReady) + bbgo.Notify("Position Ready") bbgo.Notify("SpotPosition", s.SpotPosition) bbgo.Notify("FuturesPosition", s.FuturesPosition) bbgo.Notify("NeutralPosition", s.NeutralPosition) From 117b5198ec5ff4d62d07169e37896df6c9afdeda Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 21:43:36 +0800 Subject: [PATCH 0726/1392] xfunding: introduce resetTransfer method to reset the futures transfer --- pkg/strategy/xfunding/strategy.go | 5 +++++ pkg/strategy/xfunding/transfer.go | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 839d8dbb06..d7e17faee7 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -418,7 +418,12 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if err := s.futuresOrderExecutor.ClosePosition(ctx, fixedpoint.One); err != nil { log.WithError(err).Errorf("close position error") } + + if err := s.resetTransfer(ctx, s.binanceSpot, s.spotMarket.BaseCurrency); err != nil { + log.WithError(err).Errorf("transfer error") + } } + }) if binanceStream, ok := s.futuresSession.UserDataStream.(*binance.Stream); ok { diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 9d45307fca..e5cf4de76f 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -13,6 +13,21 @@ type FuturesTransfer interface { QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) } +func (s *Strategy) resetTransfer(ctx context.Context, ex FuturesTransfer, asset string) error { + balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) + if err != nil { + return err + } + + b, ok := balances[asset] + if !ok { + return fmt.Errorf("%s balance not found", asset) + } + + log.Infof("transfering out futures account asset %s %s", b.Available, asset) + return ex.TransferFuturesAccountAsset(ctx, asset, b.Available, types.TransferOut) +} + func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset string, tradeQuantity fixedpoint.Value) error { // if transfer done if s.State.TotalBaseTransfer.IsZero() { From 1383eb0401cb4661ec61ce7b78a80bc34bbdcf80 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 21:44:48 +0800 Subject: [PATCH 0727/1392] xfunding: resetTransfer should also reset the transfer stats --- pkg/strategy/xfunding/transfer.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index e5cf4de76f..86e76c4b5f 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -25,7 +25,15 @@ func (s *Strategy) resetTransfer(ctx context.Context, ex FuturesTransfer, asset } log.Infof("transfering out futures account asset %s %s", b.Available, asset) - return ex.TransferFuturesAccountAsset(ctx, asset, b.Available, types.TransferOut) + + err = ex.TransferFuturesAccountAsset(ctx, asset, b.Available, types.TransferOut) + if err != nil { + return err + } + + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero + return nil } func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset string, tradeQuantity fixedpoint.Value) error { From d0566e23ec390329ae74a4789250bdd84e5c19ff Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 21:46:15 +0800 Subject: [PATCH 0728/1392] xfunding: log submit failed orders --- pkg/strategy/xfunding/strategy.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index d7e17faee7..9253c5de9a 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -624,7 +624,7 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { return } - createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + submitOrder := types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimitMaker, @@ -632,10 +632,11 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { Price: orderPrice, Market: s.futuresMarket, ReduceOnly: true, - }) + } + createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, submitOrder) if err != nil { - log.WithError(err).Errorf("can not submit order") + log.WithError(err).Errorf("can not submit futures order: %+v", submitOrder) return } @@ -723,17 +724,18 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } - createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + submitOrder := types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimitMaker, Quantity: orderQuantity, Price: orderPrice, Market: s.futuresMarket, - }) + } + createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, submitOrder) if err != nil { - log.WithError(err).Errorf("can not submit order") + log.WithError(err).Errorf("can not submit spot order: %+v", submitOrder) return } From 866443d89f1d543a49b0ade871753a7f976f24e2 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 21:48:10 +0800 Subject: [PATCH 0729/1392] xfunding: only do transfer when the available balance is not zero --- pkg/strategy/xfunding/transfer.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 86e76c4b5f..d11128772d 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -21,7 +21,11 @@ func (s *Strategy) resetTransfer(ctx context.Context, ex FuturesTransfer, asset b, ok := balances[asset] if !ok { - return fmt.Errorf("%s balance not found", asset) + return nil + } + + if b.Available.IsZero() { + return nil } log.Infof("transfering out futures account asset %s %s", b.Available, asset) From 86c5ba603e52df48d7afb01ced49d24defd0b700 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 22:25:54 +0800 Subject: [PATCH 0730/1392] binanceapi: add get futures balance api --- .../futures_get_account_balance_request.go | 29 ++++ ..._get_account_balance_request_requestgen.go | 135 ++++++++++++++++++ pkg/exchange/binance/futures.go | 3 + 3 files changed, 167 insertions(+) create mode 100644 pkg/exchange/binance/binanceapi/futures_get_account_balance_request.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_account_balance_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/futures_get_account_balance_request.go b/pkg/exchange/binance/binanceapi/futures_get_account_balance_request.go new file mode 100644 index 0000000000..1e7776fedb --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_account_balance_request.go @@ -0,0 +1,29 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type FuturesBalance struct { + AccountAlias string `json:"accountAlias"` + Asset string `json:"asset"` + Balance fixedpoint.Value `json:"balance"` + CrossWalletBalance fixedpoint.Value `json:"crossWalletBalance"` + CrossUnPnl fixedpoint.Value `json:"crossUnPnl"` + AvailableBalance fixedpoint.Value `json:"availableBalance"` + MaxWithdrawAmount fixedpoint.Value `json:"maxWithdrawAmount"` + MarginAvailable bool `json:"marginAvailable"` + UpdateTime types.MillisecondTimestamp `json:"updateTime"` +} + +//go:generate requestgen -method GET -url "/fapi/v2/balance" -type FuturesGetAccountBalanceRequest -responseType []FuturesBalance +type FuturesGetAccountBalanceRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *FuturesRestClient) NewFuturesGetAccountBalanceRequest() *FuturesGetAccountBalanceRequest { + return &FuturesGetAccountBalanceRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_account_balance_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_get_account_balance_request_requestgen.go new file mode 100644 index 0000000000..8aca6d656c --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_account_balance_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method GET -url /fapi/v2/balance -type FuturesGetAccountBalanceRequest -responseType []FuturesBalance"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGetAccountBalanceRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGetAccountBalanceRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGetAccountBalanceRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGetAccountBalanceRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGetAccountBalanceRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGetAccountBalanceRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGetAccountBalanceRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGetAccountBalanceRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGetAccountBalanceRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesGetAccountBalanceRequest) Do(ctx context.Context) ([]FuturesBalance, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/fapi/v2/balance" + + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []FuturesBalance + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index bd70e3bdbe..7302c1ab4c 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -67,6 +67,9 @@ func (e *Exchange) QueryFuturesAccount(ctx context.Context) (*types.Account, err if err != nil { return nil, err } + + // req := e.futuresClient2.NewFuturesGetAccountBalanceRequest() + accountBalances, err := e.futuresClient.NewGetBalanceService().Do(ctx) if err != nil { return nil, err From 321425709ae8103a5d7061aa8d56da5f25dbe07c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 22:45:40 +0800 Subject: [PATCH 0731/1392] binance: use requestgen api to query futures balances --- .../futures_get_account_balance_request.go | 28 +++++++++++++------ pkg/exchange/binance/futures.go | 19 +++++++------ pkg/types/balance.go | 2 ++ 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/pkg/exchange/binance/binanceapi/futures_get_account_balance_request.go b/pkg/exchange/binance/binanceapi/futures_get_account_balance_request.go index 1e7776fedb..cb5e0a3978 100644 --- a/pkg/exchange/binance/binanceapi/futures_get_account_balance_request.go +++ b/pkg/exchange/binance/binanceapi/futures_get_account_balance_request.go @@ -8,15 +8,25 @@ import ( ) type FuturesBalance struct { - AccountAlias string `json:"accountAlias"` - Asset string `json:"asset"` - Balance fixedpoint.Value `json:"balance"` - CrossWalletBalance fixedpoint.Value `json:"crossWalletBalance"` - CrossUnPnl fixedpoint.Value `json:"crossUnPnl"` - AvailableBalance fixedpoint.Value `json:"availableBalance"` - MaxWithdrawAmount fixedpoint.Value `json:"maxWithdrawAmount"` - MarginAvailable bool `json:"marginAvailable"` - UpdateTime types.MillisecondTimestamp `json:"updateTime"` + AccountAlias string `json:"accountAlias"` + Asset string `json:"asset"` + + // Balance - wallet balance + Balance fixedpoint.Value `json:"balance"` + + CrossWalletBalance fixedpoint.Value `json:"crossWalletBalance"` + + // CrossUnPnL unrealized profit of crossed positions + CrossUnPnl fixedpoint.Value `json:"crossUnPnl"` + + AvailableBalance fixedpoint.Value `json:"availableBalance"` + + // MaxWithdrawAmount - maximum amount for transfer out + MaxWithdrawAmount fixedpoint.Value `json:"maxWithdrawAmount"` + + // MarginAvailable - whether the asset can be used as margin in Multi-Assets mode + MarginAvailable bool `json:"marginAvailable"` + UpdateTime types.MillisecondTimestamp `json:"updateTime"` } //go:generate requestgen -method GET -url "/fapi/v2/balance" -type FuturesGetAccountBalanceRequest -responseType []FuturesBalance diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index 7302c1ab4c..f6266e7a24 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -68,22 +68,23 @@ func (e *Exchange) QueryFuturesAccount(ctx context.Context) (*types.Account, err return nil, err } - // req := e.futuresClient2.NewFuturesGetAccountBalanceRequest() - - accountBalances, err := e.futuresClient.NewGetBalanceService().Do(ctx) + req := e.futuresClient2.NewFuturesGetAccountBalanceRequest() + accountBalances, err := req.Do(ctx) if err != nil { return nil, err } var balances = map[string]types.Balance{} for _, b := range accountBalances { - balanceAvailable := fixedpoint.Must(fixedpoint.NewFromString(b.AvailableBalance)) - balanceTotal := fixedpoint.Must(fixedpoint.NewFromString(b.Balance)) - unrealizedPnl := fixedpoint.Must(fixedpoint.NewFromString(b.CrossUnPnl)) + // The futures account balance is much different from the spot balance: + // - Balance is the actual balance of the asset + // - AvailableBalance is the available margin balance (can be used as notional) + // - CrossWalletBalance (this will be meaningful when using isolated margin) balances[b.Asset] = types.Balance{ - Currency: b.Asset, - Available: balanceAvailable, - Locked: balanceTotal.Sub(balanceAvailable.Sub(unrealizedPnl)), + Currency: b.Asset, + Available: b.AvailableBalance, // AvailableBalance here is the available margin, like how much quantity/notional you can SHORT/LONG, not what you can withdraw + Locked: b.Balance.Sub(b.AvailableBalance.Sub(b.CrossUnPnl)), // FIXME: AvailableBalance is the available margin balance, it could be re-calculated by the current formula. + MaxWithdrawAmount: b.MaxWithdrawAmount, } } diff --git a/pkg/types/balance.go b/pkg/types/balance.go index 94c7bc47b0..e0c0255f73 100644 --- a/pkg/types/balance.go +++ b/pkg/types/balance.go @@ -25,6 +25,8 @@ type Balance struct { // NetAsset = (Available + Locked) - Borrowed - Interest NetAsset fixedpoint.Value `json:"net,omitempty"` + + MaxWithdrawAmount fixedpoint.Value `json:"maxWithdrawAmount,omitempty"` } func (b Balance) Add(b2 Balance) Balance { From 0c9e0649c61f70af9019a3ffb2015bfdb423ff28 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 22:49:34 +0800 Subject: [PATCH 0732/1392] xfunding: use b.MaxWithdrawAmount instead of b.Available --- pkg/strategy/xfunding/strategy.go | 3 +++ pkg/strategy/xfunding/transfer.go | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 9253c5de9a..e432074e31 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -136,6 +136,7 @@ type Strategy struct { // Reset your position info Reset bool `json:"reset"` + // CloseFuturesPosition can be enabled to close the futures position and then transfer the collateral asset back to the spot account. CloseFuturesPosition bool `json:"closeFuturesPosition"` ProfitStats *ProfitStats `persistence:"profit_stats"` @@ -1011,6 +1012,8 @@ func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.Exch } func (s *Strategy) setInitialLeverage(ctx context.Context) error { + log.Infof("setting futures leverage to %d", s.Leverage.Int()+1) + futuresClient := s.binanceFutures.GetFuturesClient() req := futuresClient.NewFuturesChangeInitialLeverageRequest() req.Symbol(s.Symbol) diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index d11128772d..433c7c5cf9 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -24,13 +24,14 @@ func (s *Strategy) resetTransfer(ctx context.Context, ex FuturesTransfer, asset return nil } - if b.Available.IsZero() { + amount := b.MaxWithdrawAmount + if amount.IsZero() { return nil } - log.Infof("transfering out futures account asset %s %s", b.Available, asset) + log.Infof("transfering out futures account asset %s %s", amount, asset) - err = ex.TransferFuturesAccountAsset(ctx, asset, b.Available, types.TransferOut) + err = ex.TransferFuturesAccountAsset(ctx, asset, amount, types.TransferOut) if err != nil { return err } @@ -67,7 +68,7 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset st amount := s.State.PendingBaseTransfer.Add(quantity) // try to transfer more if we enough balance - amount = fixedpoint.Min(amount, b.Available) + amount = fixedpoint.Min(amount, b.MaxWithdrawAmount) // we can only transfer the rest quota (total base transfer) amount = fixedpoint.Min(s.State.TotalBaseTransfer, amount) @@ -110,7 +111,7 @@ func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, asset str } // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(quantity) < 0 { + if b.MaxWithdrawAmount.Compare(quantity) < 0 { log.Infof("adding to pending base transfer: %s %s", quantity, asset) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil From 7e2688b8c72b7044568abb3b6de9d1734badf638 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 22:54:54 +0800 Subject: [PATCH 0733/1392] xfunding: cancel open orders before closing the futures position --- pkg/strategy/xfunding/strategy.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index e432074e31..ddde207c01 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -416,6 +416,17 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresSession.UserDataStream.OnStart(func() { if s.CloseFuturesPosition { + + openOrders, err := s.futuresSession.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("query open orders error") + } else { + // canceling open orders + if err = s.futuresSession.Exchange.CancelOrders(ctx, openOrders...); err != nil { + log.WithError(err).Errorf("query open orders error") + } + } + if err := s.futuresOrderExecutor.ClosePosition(ctx, fixedpoint.One); err != nil { log.WithError(err).Errorf("close position error") } From 0efb56c43e2a6c25720afced3bf30687d1c95421 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 22:55:40 +0800 Subject: [PATCH 0734/1392] xfunding: also reset the quote balance transfer --- pkg/strategy/xfunding/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index ddde207c01..4798aa8e09 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -434,6 +434,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if err := s.resetTransfer(ctx, s.binanceSpot, s.spotMarket.BaseCurrency); err != nil { log.WithError(err).Errorf("transfer error") } + + if err := s.resetTransfer(ctx, s.binanceSpot, s.spotMarket.QuoteCurrency); err != nil { + log.WithError(err).Errorf("transfer error") + } } }) From 7c975da575ae11b4ab03a204143bdc46f0b1b6a1 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 23:05:31 +0800 Subject: [PATCH 0735/1392] xfunding: fix position sync bug --- pkg/strategy/xfunding/strategy.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 4798aa8e09..b5ae0b539f 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -703,12 +703,19 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } log.Infof("calculated futures account quote value = %s", quoteValue.String()) + if quoteValue.IsZero() { + return + } // max futures base position (without negative sign) maxFuturesBasePosition := fixedpoint.Min( spotBase.Mul(s.Leverage), s.State.TotalBaseTransfer.Mul(s.Leverage)) + if maxFuturesBasePosition.IsZero() { + return + } + // if - futures position < max futures position, increase it if futuresBase.Neg().Compare(maxFuturesBasePosition) >= 0 { s.setPositionState(PositionReady) From 6c550c55fa264cb230a2ce008e159584d1e5cdb5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 23:09:37 +0800 Subject: [PATCH 0736/1392] xfunding: fix spot transfer --- pkg/strategy/xfunding/transfer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 433c7c5cf9..3de3540aa2 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -111,7 +111,7 @@ func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, asset str } // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.MaxWithdrawAmount.Compare(quantity) < 0 { + if b.Available.Compare(quantity) < 0 { log.Infof("adding to pending base transfer: %s %s", quantity, asset) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil From 69af9e03ea5fb379cc7927a9633e9ef3f5f93a5c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Mar 2023 00:13:02 +0800 Subject: [PATCH 0737/1392] xfunding: fix funding fee notification --- pkg/strategy/xfunding/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index b5ae0b539f..4656015f14 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -519,7 +519,7 @@ func (s *Strategy) handleAccountUpdate(ctx context.Context, e *binance.AccountUp continue } - bbgo.Notify(fee) + bbgo.Notify(&fee) } log.Infof("total collected funding fee: %f %s", s.ProfitStats.TotalFundingFee.Float64(), s.ProfitStats.FundingFeeCurrency) From b18d4da402ad72f954c0d553b52c358aa9c6db6b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Mar 2023 00:44:57 +0800 Subject: [PATCH 0738/1392] binance: fix/improve order trade event parsing --- pkg/exchange/binance/parse.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index 074eec74a9..eb3bbd6eca 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -793,8 +793,8 @@ type OrderTrade struct { CommissionAmount fixedpoint.Value `json:"n"` CommissionAsset string `json:"N"` - OrderTradeTime int64 `json:"T"` - TradeId int64 `json:"t"` + OrderTradeTime types.MillisecondTimestamp `json:"T"` + TradeId int64 `json:"t"` BidsNotional string `json:"b"` AskNotional string `json:"a"` @@ -867,7 +867,6 @@ func (e *OrderTradeUpdateEvent) OrderFutures() (*types.Order, error) { return nil, errors.New("execution report type is not for futures order") } - orderCreationTime := time.Unix(0, e.OrderTrade.OrderTradeTime*int64(time.Millisecond)) return &types.Order{ Exchange: types.ExchangeBinance, SubmitOrder: types.SubmitOrder{ @@ -882,7 +881,8 @@ func (e *OrderTradeUpdateEvent) OrderFutures() (*types.Order, error) { OrderID: uint64(e.OrderTrade.OrderId), Status: toGlobalFuturesOrderStatus(futures.OrderStatusType(e.OrderTrade.CurrentOrderStatus)), ExecutedQuantity: e.OrderTrade.OrderFilledAccumulatedQuantity, - CreationTime: types.Time(orderCreationTime), + CreationTime: types.Time(e.OrderTrade.OrderTradeTime.Time()), // FIXME: find the correct field for creation time + UpdateTime: types.Time(e.OrderTrade.OrderTradeTime.Time()), }, nil } @@ -891,7 +891,6 @@ func (e *OrderTradeUpdateEvent) TradeFutures() (*types.Trade, error) { return nil, errors.New("execution report is not a futures trade") } - tt := time.Unix(0, e.OrderTrade.OrderTradeTime*int64(time.Millisecond)) return &types.Trade{ ID: uint64(e.OrderTrade.TradeId), Exchange: types.ExchangeBinance, @@ -903,7 +902,7 @@ func (e *OrderTradeUpdateEvent) TradeFutures() (*types.Trade, error) { QuoteQuantity: e.OrderTrade.LastFilledPrice.Mul(e.OrderTrade.OrderLastFilledQuantity), IsBuyer: e.OrderTrade.Side == "BUY", IsMaker: e.OrderTrade.IsMaker, - Time: types.Time(tt), + Time: types.Time(e.OrderTrade.OrderTradeTime.Time()), Fee: e.OrderTrade.CommissionAmount, FeeCurrency: e.OrderTrade.CommissionAsset, }, nil From 4b9e3f2302b7647229cf74333d72f9b445562536 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Mar 2023 00:46:41 +0800 Subject: [PATCH 0739/1392] xfunding: send positions to slack when start up --- pkg/strategy/xfunding/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 4656015f14..99d75d9a82 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -343,6 +343,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order log.Infof("loaded futures position: %s", s.FuturesPosition.String()) log.Infof("loaded neutral position: %s", s.NeutralPosition.String()) + bbgo.Notify("Spot Position", s.SpotPosition) + bbgo.Notify("Futures Position", s.FuturesPosition) + bbgo.Notify("Neutral Position", s.NeutralPosition) + // sync funding fee txns if !s.ProfitStats.LastFundingFeeTime.IsZero() { s.syncFundingFeeRecords(ctx, s.ProfitStats.LastFundingFeeTime) From bb47fb35324d103fa7b7602242dffb0aa50c3974 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Mar 2023 01:33:55 +0800 Subject: [PATCH 0740/1392] binance: fix parse tests --- pkg/exchange/binance/parse_test.go | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/pkg/exchange/binance/parse_test.go b/pkg/exchange/binance/parse_test.go index 0d83664ce1..92f7228cf6 100644 --- a/pkg/exchange/binance/parse_test.go +++ b/pkg/exchange/binance/parse_test.go @@ -3,10 +3,12 @@ package binance import ( "regexp" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) var jsCommentTrimmer = regexp.MustCompile("(?m)//.*$") @@ -391,20 +393,20 @@ func TestParseOrderFuturesUpdate(t *testing.T) { assert.True(t, ok) assert.NotNil(t, orderTradeEvent) - assert.Equal(t, orderTradeEvent.OrderTrade.Symbol, "BTCUSDT") - assert.Equal(t, orderTradeEvent.OrderTrade.Side, "SELL") - assert.Equal(t, orderTradeEvent.OrderTrade.ClientOrderID, "x-NSUYEBKMe60cf610-f5c7-49a4-9c1") - assert.Equal(t, orderTradeEvent.OrderTrade.OrderType, "MARKET") - assert.Equal(t, orderTradeEvent.Time, int64(1639933384763)) - assert.Equal(t, orderTradeEvent.OrderTrade.OrderTradeTime, int64(1639933384755)) - assert.Equal(t, orderTradeEvent.OrderTrade.OriginalQuantity, fixedpoint.MustNewFromString("0.001")) - assert.Equal(t, orderTradeEvent.OrderTrade.OrderLastFilledQuantity, fixedpoint.MustNewFromString("0.001")) - assert.Equal(t, orderTradeEvent.OrderTrade.OrderFilledAccumulatedQuantity, fixedpoint.MustNewFromString("0.001")) - assert.Equal(t, orderTradeEvent.OrderTrade.CurrentExecutionType, "TRADE") - assert.Equal(t, orderTradeEvent.OrderTrade.CurrentOrderStatus, "FILLED") - assert.Equal(t, orderTradeEvent.OrderTrade.LastFilledPrice, fixedpoint.MustNewFromString("47202.40")) - assert.Equal(t, orderTradeEvent.OrderTrade.OrderId, int64(38541728873)) - assert.Equal(t, orderTradeEvent.OrderTrade.TradeId, int64(1741505949)) + assert.Equal(t, "BTCUSDT", orderTradeEvent.OrderTrade.Symbol) + assert.Equal(t, "SELL", orderTradeEvent.OrderTrade.Side) + assert.Equal(t, "x-NSUYEBKMe60cf610-f5c7-49a4-9c1", orderTradeEvent.OrderTrade.ClientOrderID) + assert.Equal(t, "MARKET", orderTradeEvent.OrderTrade.OrderType) + assert.Equal(t, int64(1639933384763), orderTradeEvent.Time) + assert.Equal(t, types.MillisecondTimestamp(time.UnixMilli(1639933384755)), orderTradeEvent.OrderTrade.OrderTradeTime) + assert.Equal(t, fixedpoint.MustNewFromString("0.001"), orderTradeEvent.OrderTrade.OriginalQuantity) + assert.Equal(t, fixedpoint.MustNewFromString("0.001"), orderTradeEvent.OrderTrade.OrderLastFilledQuantity) + assert.Equal(t, fixedpoint.MustNewFromString("0.001"), orderTradeEvent.OrderTrade.OrderFilledAccumulatedQuantity) + assert.Equal(t, "TRADE", orderTradeEvent.OrderTrade.CurrentExecutionType) + assert.Equal(t, "FILLED", orderTradeEvent.OrderTrade.CurrentOrderStatus) + assert.Equal(t, fixedpoint.MustNewFromString("47202.40"), orderTradeEvent.OrderTrade.LastFilledPrice) + assert.Equal(t, int64(38541728873), orderTradeEvent.OrderTrade.OrderId) + assert.Equal(t, int64(1741505949), orderTradeEvent.OrderTrade.TradeId) orderUpdate, err := orderTradeEvent.OrderFutures() assert.NoError(t, err) From 5b09ad671cb5d8aa6640ac2eacddc40f8318b531 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 3 Apr 2023 00:11:21 +0800 Subject: [PATCH 0741/1392] max: fix max order group id --- pkg/exchange/max/exchange.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 9863bfecec..69e3cb5f90 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -492,8 +492,7 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (cr ClientOrderID(clientOrderID) if o.GroupID > 0 { - // TODO: MAX API only support 0 ~ 2^31-1 (2147483647) - req.GroupID(strconv.FormatUint(uint64(o.GroupID), 10)) + req.GroupID(strconv.FormatUint(uint64(o.GroupID%math.MaxInt32), 10)) } switch o.Type { From 45ca3eb962223d7cc8fe99ae2a41631609f528e5 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 3 Apr 2023 00:13:04 +0800 Subject: [PATCH 0742/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index c8f86f90d3..58d1e68ecc 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -60,4 +60,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 4d6ee00c88..71137208d7 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -43,4 +43,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 807af1d6d9..45a01c79ed 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -52,4 +52,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index 86f5efdb4c..3d2dbe60ca 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -42,4 +42,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index 33347ca691..e7e42df6d5 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -41,4 +41,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index 15117c9c8f..0ae96aa8c6 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -51,4 +51,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index 78e9aa7117..089e1da0df 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -43,4 +43,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index e8d0b330e6..e1902b59ca 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -50,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 0727e3a193..7c4b0f7111 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -44,4 +44,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index ae10a63de5..7537ff6603 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -47,4 +47,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index be954a0731..9ac68d94e1 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -44,4 +44,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index 347aca129d..9dc1635cb9 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -43,4 +43,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index 031983372f..c247bfd45b 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -40,4 +40,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index f733599759..2df0cf06cc 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -43,4 +43,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index fe3223548b..5701486fbc 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -43,4 +43,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index 91bad8bcd0..3deadf1069 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -43,4 +43,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index b2896f321e..c81a96ae71 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -42,4 +42,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index 3f980b451e..efb4456ede 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -46,4 +46,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index ae8c309306..5b872156c1 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -44,4 +44,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index c98929544c..0329b3f958 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -42,4 +42,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index 6414e0ca61..ebfe58cf58 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -51,4 +51,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index ec6adf8364..e8f0a3f8de 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -53,4 +53,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index da45e54018..5f4192ee6e 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -48,4 +48,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index 20c8715f07..5e7773f28a 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -44,4 +44,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index 62700541bb..0fde24263e 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -44,4 +44,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 50be5401e3..d2907b3fe3 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -42,4 +42,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 131aa464f8..0e3f42d3e5 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -44,4 +44,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index a0d19c6d38..a9e4a05f94 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -42,4 +42,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index c55dbdf189..95979a7ee0 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -41,4 +41,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Mar-2023 +###### Auto generated by spf13/cobra on 3-Apr-2023 From 3328e0453c500ecf8691b0704d023454e0282c6c Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 3 Apr 2023 00:13:04 +0800 Subject: [PATCH 0743/1392] bump version to v1.45.0 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 7efd9e3c48..1c7d792778 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.44.1-7d91fd01-dev" +const Version = "v1.45.0-5b09ad67-dev" -const VersionGitRef = "7d91fd01" +const VersionGitRef = "5b09ad67" diff --git a/pkg/version/version.go b/pkg/version/version.go index 441ce99c19..84730a3c6a 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.44.1-7d91fd01" +const Version = "v1.45.0-5b09ad67" -const VersionGitRef = "7d91fd01" +const VersionGitRef = "5b09ad67" From f54fe281422db5bc6add696cba36e6cfd861f5af Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 3 Apr 2023 00:13:04 +0800 Subject: [PATCH 0744/1392] add v1.45.0 release note --- doc/release/v1.45.0.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 doc/release/v1.45.0.md diff --git a/doc/release/v1.45.0.md b/doc/release/v1.45.0.md new file mode 100644 index 0000000000..ab1ddbd3a6 --- /dev/null +++ b/doc/release/v1.45.0.md @@ -0,0 +1,23 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.44.1...main) + + - [#1134](https://github.com/c9s/bbgo/pull/1134): strategy: [xfunding] fix binance api and add initial setup for futures account + - [#1133](https://github.com/c9s/bbgo/pull/1133): FIX: end batch query if start > end + - [#1132](https://github.com/c9s/bbgo/pull/1132): strategy: xfunding: add profit stats and collect funding fee info + - [#1131](https://github.com/c9s/bbgo/pull/1131): strategy: xfunding: improve sync goroutine, add mutex lock, fix binance websocket message parsing ... + - [#1127](https://github.com/c9s/bbgo/pull/1127): feature: strategy: xfunding + - [#1125](https://github.com/c9s/bbgo/pull/1125): FEATURE: [grid2] using dnum + - [#1129](https://github.com/c9s/bbgo/pull/1129): strategy: fixedmaker: replenish on start + - [#1126](https://github.com/c9s/bbgo/pull/1126): FIX: max: move submitOrderLimiter to the exchange wide var + - [#1124](https://github.com/c9s/bbgo/pull/1124): FEATURE: emit grid error when failed to recover or open grid + - [#1122](https://github.com/c9s/bbgo/pull/1122): strategy: fixedmaker: clamp skew + - [#1123](https://github.com/c9s/bbgo/pull/1123): exits/trailingstop: fix typo + - [#1119](https://github.com/c9s/bbgo/pull/1119): FEATURE: make PinOrderMap's key from string to Pin + - [#1120](https://github.com/c9s/bbgo/pull/1120): REFACTOR: [grid2] pull out backoff cancel all to cancelAllOrdersUntilSuccessful + - [#1117](https://github.com/c9s/bbgo/pull/1117): strategy: fixedmaker: add option to use ATR to adjust spread ratio + - [#1118](https://github.com/c9s/bbgo/pull/1118): MINOR: use Debug config for debug log + - [#1113](https://github.com/c9s/bbgo/pull/1113): strategy: fixedmaker: add skew to adjust bid/ask price + - [#1116](https://github.com/c9s/bbgo/pull/1116): FIX: fix gracefully shutdown SaveState call for Isolation + - [#1115](https://github.com/c9s/bbgo/pull/1115): FIX: [grid2] fix correct price metrics + - [#1114](https://github.com/c9s/bbgo/pull/1114): FEATURE: verify the grids before emit filled orders + - [#1112](https://github.com/c9s/bbgo/pull/1112): FIX: fix wrong fee currency + - [#1111](https://github.com/c9s/bbgo/pull/1111): strategy: rebalance: make CreatePositions and CreateProfitStats public From 00352b2a0d77f8b13ed9b7c72b80bf0713c84246 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 11:36:32 +0800 Subject: [PATCH 0745/1392] FIX: recover even though inital order id is 0 --- pkg/strategy/grid2/strategy.go | 47 ++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e28ab1d3b4..42a0978b35 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -2,6 +2,7 @@ package grid2 import ( "context" + "encoding/json" "fmt" "math" "sort" @@ -825,6 +826,7 @@ func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.Exch s.handleOrderFilled(o) // sync the profits to redis + s.debugGridProfitStats("OrderUpdate") bbgo.Sync(ctx, s) s.updateGridNumOfOrdersMetricsWithLock() @@ -944,6 +946,7 @@ func (s *Strategy) CloseGrid(ctx context.Context) error { defer s.EmitGridClosed() + s.debugGridProfitStats("CloseGrid") bbgo.Sync(ctx, s) // now we can cancel the open orders @@ -1082,6 +1085,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) if len(orderIds) > 0 { s.GridProfitStats.InitialOrderID = orderIds[0] + s.debugGridProfitStats("openGrid") bbgo.Sync(ctx, s) } @@ -1212,6 +1216,23 @@ func (s *Strategy) debugOrders(desc string, orders []types.Order) { s.logger.Infof(sb.String()) } +func (s *Strategy) debugGridProfitStats(where string) { + if !s.Debug { + return + } + + stats := *s.GridProfitStats + // ProfitEntries may have too many profits, make it nil to readable + stats.ProfitEntries = nil + b, err := json.Marshal(stats) + if err != nil { + s.logger.WithError(err).Errorf("[%s] failed to debug grid profit stats", where) + return + } + + s.logger.Infof("[%s] grid profit stats: %s", where, string(b)) +} + func (s *Strategy) debugLog(format string, args ...interface{}) { if !s.Debug { return @@ -1441,10 +1462,12 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context s.debugOrders("emit filled orders", filledOrders) // 5. emit the filled orders - activeOrderBook := s.orderExecutor.ActiveMakerOrders() - for _, filledOrder := range filledOrders { - activeOrderBook.EmitFilled(filledOrder) - } + /* + activeOrderBook := s.orderExecutor.ActiveMakerOrders() + for _, filledOrder := range filledOrders { + activeOrderBook.EmitFilled(filledOrder) + } + */ // 6. emit grid ready s.EmitGridReady() @@ -2032,6 +2055,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.GridProfitStats.AddTrade(trade) }) orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + s.debugGridProfitStats("OnPositionUpdate") bbgo.Sync(ctx, s) }) orderExecutor.ActiveMakerOrders().OnFilled(s.newOrderUpdateHandler(ctx, session)) @@ -2132,6 +2156,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSession) { + s.debugGridProfitStats("startProcess") if s.RecoverOrdersWhenStart { // do recover only when triggerPrice is not set and not in the back-test mode s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") @@ -2187,19 +2212,12 @@ func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbg } func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { - // no initial order id means we don't need to recover - if s.GridProfitStats.InitialOrderID == 0 { - s.logger.Debug("new strategy, no need to recover") - return nil - } - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { return err } s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) - s.debugLog("recover grid with group id: %d", s.OrderGroupID) // filter out the order with the group id belongs to this grid var openOrdersOnGrid []types.Order @@ -2211,6 +2229,13 @@ func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbg s.logger.Infof("found %d open orders belong to this grid on the %s order book", len(openOrdersOnGrid), s.Symbol) + // no initial order id means we don't need to recover + // 3/31 updated : Find there may be 0 initial order id when the strategy is not strategy, so we need to add more checking on it. + if s.GridProfitStats.InitialOrderID == 0 && len(openOrdersOnGrid) == 0 { + s.debugLog("new strategy, no need to recover") + return nil + } + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) if !implemented { s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") From d953a6d7b885d8e4d7de6b9d75964ee8892723bd Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 14:59:03 +0800 Subject: [PATCH 0746/1392] check by trades + open orders --- pkg/strategy/grid2/recover.go | 305 +++++++++++++++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 273 ----------------------------- 2 files changed, 305 insertions(+), 273 deletions(-) create mode 100644 pkg/strategy/grid2/recover.go diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go new file mode 100644 index 0000000000..c50a9fc57f --- /dev/null +++ b/pkg/strategy/grid2/recover.go @@ -0,0 +1,305 @@ +package grid2 + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/pkg/errors" +) + +func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) + // if the exchange doesn't support ExchangeTradeHistoryService, do not run recover + if !implemented { + s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") + return nil + } + + s.logger.Infof("recover grid with group id: %d", s.OrderGroupID) + + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return errors.Wrapf(err, "check if strategy exists error when query open orders") + } + + s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) + + // filter out the order with the group id belongs to this grid + var openOrdersOnGrid []types.Order + for _, order := range openOrders { + if order.GroupID == s.OrderGroupID { + openOrdersOnGrid = append(openOrdersOnGrid, order) + } + } + + s.logger.Infof("found %d open orders belong to this grid on the %s order book", len(openOrdersOnGrid), s.Symbol) + + if s.GridProfitStats.InitialOrderID != 0 { + // existiting strategy, need recover + } else if len(openOrdersOnGrid) != 0 { + // open orders on grid is not 0. this strategy is existing, need recover + } else { + // initial order id may be new strategy or lost data in redis, so we need to check trades + open orders + // if there are open orders or trades, we need to recover + trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ + // from 1, because some API will ignore 0 last trade id + LastTradeID: 1, + // if there is any trades, we need to recover. + Limit: 1, + }) + + if err != nil { + return errors.Wrapf(err, "check if strategy exists error when query trades") + } + + if len(trades) == 0 { + s.logger.Info("new strategy, no need to recover") + return nil + } + } + + s.logger.Infof("start to recover") + if err := s.recoverGridWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { + return errors.Wrap(err, "grid recover error") + } + + return nil +} + +func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrdersOnGrid []types.Order) error { + if s.orderQueryService == nil { + return fmt.Errorf("orderQueryService is nil, it can't get orders by trade") + } + + // set grid + grid := s.newGrid() + s.setGrid(grid) + + // add open orders to active order book + s.addOrdersToActiveOrderBook(openOrdersOnGrid) + + expectedOrderNums := s.GridNum - 1 + openOrdersOnGridNums := int64(len(openOrdersOnGrid)) + s.debugLog("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) + if expectedOrderNums == openOrdersOnGridNums { + // no need to recover + return nil + } else if expectedOrderNums < openOrdersOnGridNums { + return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") + } + + // 1. build pin-order map + pinOrdersOpen, err := s.buildPinOrderMap(grid.Pins, openOrdersOnGrid) + if err != nil { + return errors.Wrapf(err, "failed to build pin order map with open orders") + } + + // 2. build the filled pin-order map by querying trades + pinOrdersFilled, err := s.buildFilledPinOrderMapFromTrades(ctx, historyService, pinOrdersOpen) + if err != nil { + return errors.Wrapf(err, "failed to build filled pin order map") + } + + // 3. get the filled orders from pin-order map + filledOrders := pinOrdersFilled.AscendingOrders() + numFilledOrders := len(filledOrders) + if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { + // nums of filled order is the same as Size - 1 - num(open orders) + } else if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) { + filledOrders = filledOrders[1:] + } else { + return fmt.Errorf("not reasonable num of filled orders") + } + + // 4. verify the grid + if err := s.verifyFilledGrid(s.grid.Pins, pinOrdersOpen, filledOrders); err != nil { + return errors.Wrapf(err, "verify grid with error") + } + + s.debugOrders("emit filled orders", filledOrders) + + // 5. emit the filled orders + activeOrderBook := s.orderExecutor.ActiveMakerOrders() + for _, filledOrder := range filledOrders { + activeOrderBook.EmitFilled(filledOrder) + } + + // 6. emit grid ready + s.EmitGridReady() + + // 7. debug and send metrics + // wait for the reverse order to be placed + time.Sleep(2 * time.Second) + debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) + s.updateGridNumOfOrdersMetricsWithLock() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) + + return nil +} + +func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { + s.debugLog("pins: %+v", pins) + s.debugLog("open pin orders:\n%s", pinOrders.String()) + s.debugOrders("filled orders", filledOrders) + + for _, filledOrder := range filledOrders { + price := filledOrder.Price + if o, exist := pinOrders[price]; !exist { + return fmt.Errorf("the price (%+v) is not in pins", price) + } else if o.OrderID != 0 { + return fmt.Errorf("there is already an order at this price (%+v)", price) + } else { + pinOrders[price] = filledOrder + } + } + + s.debugLog("filled pin orders:\n%+v", pinOrders.String()) + + side := types.SideTypeBuy + for _, pin := range pins { + order, exist := pinOrders[fixedpoint.Value(pin)] + if !exist { + return fmt.Errorf("there is no order at price (%+v)", pin) + } + + // if there is order with OrderID = 0, means we hit the empty pin + // there must be only one empty pin in the grid + // all orders below this pin need to be bid orders, above this pin need to be ask orders + if order.OrderID == 0 { + if side == types.SideTypeBuy { + side = types.SideTypeSell + continue + } + + return fmt.Errorf("not only one empty order in this grid") + } + + if order.Side != side { + return fmt.Errorf("the side is wrong") + } + } + + if side != types.SideTypeSell { + return fmt.Errorf("there is no empty pin in the grid") + } + + return nil +} + +// buildPinOrderMap build the pin-order map with grid and open orders. +// The keys of this map contains all required pins of this grid. +// If the Order of the pin is empty types.Order (OrderID == 0), it means there is no open orders at this pin. +func (s *Strategy) buildPinOrderMap(pins []Pin, openOrders []types.Order) (PinOrderMap, error) { + pinOrderMap := make(PinOrderMap) + + for _, pin := range pins { + pinOrderMap[fixedpoint.Value(pin)] = types.Order{} + } + + for _, openOrder := range openOrders { + pin := openOrder.Price + v, exist := pinOrderMap[pin] + if !exist { + return nil, fmt.Errorf("the price of the order (id: %d) is not in pins", openOrder.OrderID) + } + + if v.OrderID != 0 { + return nil, fmt.Errorf("there are duplicated open orders at the same pin") + } + + pinOrderMap[pin] = openOrder + } + + return pinOrderMap, nil +} + +// buildFilledPinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map +// It will skip the orders on pins at which open orders are already +func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, pinOrdersOpen PinOrderMap) (PinOrderMap, error) { + pinOrdersFilled := make(PinOrderMap) + + // existedOrders is used to avoid re-query the same orders + existedOrders := pinOrdersOpen.SyncOrderMap() + + var limit int64 = 1000 + // get the filled orders when bbgo is down in order from trades + // [NOTE] only retrieve from last 24 hours !!! + var fromTradeID uint64 = 0 + for { + trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ + LastTradeID: fromTradeID, + Limit: limit, + }) + + if err != nil { + return nil, errors.Wrapf(err, "failed to query trades to recover the grid with open orders") + } + + s.debugLog("QueryTrades return %d trades", len(trades)) + + for _, trade := range trades { + s.debugLog(trade.String()) + if existedOrders.Exists(trade.OrderID) { + // already queries, skip + continue + } + + order, err := s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ + OrderID: strconv.FormatUint(trade.OrderID, 10), + }) + + if err != nil { + return nil, errors.Wrapf(err, "failed to query order by trade") + } + + s.debugLog("%s (group_id: %d)", order.String(), order.GroupID) + + // avoid query this order again + existedOrders.Add(*order) + + // add 1 to avoid duplicate + fromTradeID = trade.ID + 1 + + // this trade doesn't belong to this grid + if order.GroupID != s.OrderGroupID { + continue + } + + // checked the trade's order is filled order + pin := order.Price + v, exist := pinOrdersOpen[pin] + if !exist { + return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") + } + + // skip open orders on grid + if v.OrderID != 0 { + continue + } + + // check the order's creation time + if pinOrder, exist := pinOrdersFilled[pin]; exist && pinOrder.CreationTime.Time().After(order.CreationTime.Time()) { + // do not replace the pin order if the order's creation time is not after pin order's creation time + // this situation should not happen actually, because the trades is already sorted. + s.logger.Infof("pinOrder's creation time (%s) should not be after order's creation time (%s)", pinOrder.CreationTime, order.CreationTime) + continue + } + pinOrdersFilled[pin] = *order + + // wait 100 ms to avoid rate limit + time.Sleep(100 * time.Millisecond) + } + + // stop condition + if int64(len(trades)) < limit { + break + } + } + + return pinOrdersFilled, nil +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 42a0978b35..e6a8d4fc93 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1409,241 +1409,6 @@ func (s *Strategy) checkMinimalQuoteInvestment(grid *Grid) error { return nil } -func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrdersOnGrid []types.Order) error { - if s.orderQueryService == nil { - return fmt.Errorf("orderQueryService is nil, it can't get orders by trade") - } - - // set grid - grid := s.newGrid() - s.setGrid(grid) - - // add open orders to active order book - s.addOrdersToActiveOrderBook(openOrdersOnGrid) - - expectedOrderNums := s.GridNum - 1 - openOrdersOnGridNums := int64(len(openOrdersOnGrid)) - s.debugLog("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) - if expectedOrderNums == openOrdersOnGridNums { - // no need to recover - return nil - } else if expectedOrderNums < openOrdersOnGridNums { - return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") - } - - // 1. build pin-order map - pinOrdersOpen, err := s.buildPinOrderMap(grid.Pins, openOrdersOnGrid) - if err != nil { - return errors.Wrapf(err, "failed to build pin order map with open orders") - } - - // 2. build the filled pin-order map by querying trades - pinOrdersFilled, err := s.buildFilledPinOrderMapFromTrades(ctx, historyService, pinOrdersOpen) - if err != nil { - return errors.Wrapf(err, "failed to build filled pin order map") - } - - // 3. get the filled orders from pin-order map - filledOrders := pinOrdersFilled.AscendingOrders() - numFilledOrders := len(filledOrders) - if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { - // nums of filled order is the same as Size - 1 - num(open orders) - } else if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) { - filledOrders = filledOrders[1:] - } else { - return fmt.Errorf("not reasonable num of filled orders") - } - - // 4. verify the grid - if err := s.verifyFilledGrid(s.grid.Pins, pinOrdersOpen, filledOrders); err != nil { - return errors.Wrapf(err, "verify grid with error") - } - - s.debugOrders("emit filled orders", filledOrders) - - // 5. emit the filled orders - /* - activeOrderBook := s.orderExecutor.ActiveMakerOrders() - for _, filledOrder := range filledOrders { - activeOrderBook.EmitFilled(filledOrder) - } - */ - - // 6. emit grid ready - s.EmitGridReady() - - // 7. debug and send metrics - // wait for the reverse order to be placed - time.Sleep(2 * time.Second) - debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) - s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) - - return nil -} - -func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { - s.debugLog("pins: %+v", pins) - s.debugLog("open pin orders:\n%s", pinOrders.String()) - s.debugOrders("filled orders", filledOrders) - - for _, filledOrder := range filledOrders { - price := filledOrder.Price - if o, exist := pinOrders[price]; !exist { - return fmt.Errorf("the price (%+v) is not in pins", price) - } else if o.OrderID != 0 { - return fmt.Errorf("there is already an order at this price (%+v)", price) - } else { - pinOrders[price] = filledOrder - } - } - - s.debugLog("filled pin orders:\n%+v", pinOrders.String()) - - side := types.SideTypeBuy - for _, pin := range pins { - order, exist := pinOrders[fixedpoint.Value(pin)] - if !exist { - return fmt.Errorf("there is no order at price (%+v)", pin) - } - - // if there is order with OrderID = 0, means we hit the empty pin - // there must be only one empty pin in the grid - // all orders below this pin need to be bid orders, above this pin need to be ask orders - if order.OrderID == 0 { - if side == types.SideTypeBuy { - side = types.SideTypeSell - continue - } - - return fmt.Errorf("not only one empty order in this grid") - } - - if order.Side != side { - return fmt.Errorf("the side is wrong") - } - } - - if side != types.SideTypeSell { - return fmt.Errorf("there is no empty pin in the grid") - } - - return nil -} - -// buildPinOrderMap build the pin-order map with grid and open orders. -// The keys of this map contains all required pins of this grid. -// If the Order of the pin is empty types.Order (OrderID == 0), it means there is no open orders at this pin. -func (s *Strategy) buildPinOrderMap(pins []Pin, openOrders []types.Order) (PinOrderMap, error) { - pinOrderMap := make(PinOrderMap) - - for _, pin := range pins { - pinOrderMap[fixedpoint.Value(pin)] = types.Order{} - } - - for _, openOrder := range openOrders { - pin := openOrder.Price - v, exist := pinOrderMap[pin] - if !exist { - return nil, fmt.Errorf("the price of the order (id: %d) is not in pins", openOrder.OrderID) - } - - if v.OrderID != 0 { - return nil, fmt.Errorf("there are duplicated open orders at the same pin") - } - - pinOrderMap[pin] = openOrder - } - - return pinOrderMap, nil -} - -// buildFilledPinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map -// It will skip the orders on pins at which open orders are already -func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, pinOrdersOpen PinOrderMap) (PinOrderMap, error) { - pinOrdersFilled := make(PinOrderMap) - - // existedOrders is used to avoid re-query the same orders - existedOrders := pinOrdersOpen.SyncOrderMap() - - var limit int64 = 1000 - // get the filled orders when bbgo is down in order from trades - // [NOTE] only retrieve from last 24 hours !!! - var fromTradeID uint64 = 0 - for { - trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ - LastTradeID: fromTradeID, - Limit: limit, - }) - - if err != nil { - return nil, errors.Wrapf(err, "failed to query trades to recover the grid with open orders") - } - - s.debugLog("QueryTrades return %d trades", len(trades)) - - for _, trade := range trades { - s.debugLog(trade.String()) - if existedOrders.Exists(trade.OrderID) { - // already queries, skip - continue - } - - order, err := s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ - OrderID: strconv.FormatUint(trade.OrderID, 10), - }) - - if err != nil { - return nil, errors.Wrapf(err, "failed to query order by trade") - } - - s.debugLog("%s (group_id: %d)", order.String(), order.GroupID) - - // avoid query this order again - existedOrders.Add(*order) - - // add 1 to avoid duplicate - fromTradeID = trade.ID + 1 - - // this trade doesn't belong to this grid - if order.GroupID != s.OrderGroupID { - continue - } - - // checked the trade's order is filled order - pin := order.Price - v, exist := pinOrdersOpen[pin] - if !exist { - return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") - } - - // skip open orders on grid - if v.OrderID != 0 { - continue - } - - // check the order's creation time - if pinOrder, exist := pinOrdersFilled[pin]; exist && pinOrder.CreationTime.Time().After(order.CreationTime.Time()) { - // do not replace the pin order if the order's creation time is not after pin order's creation time - // this situation should not happen actually, because the trades is already sorted. - s.logger.Infof("pinOrder's creation time (%s) should not be after order's creation time (%s)", pinOrder.CreationTime, order.CreationTime) - continue - } - pinOrdersFilled[pin] = *order - - // wait 100 ms to avoid rate limit - time.Sleep(100 * time.Millisecond) - } - - // stop condition - if int64(len(trades)) < limit { - break - } - } - - return pinOrdersFilled, nil -} - func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { grid := s.newGrid() @@ -2211,44 +1976,6 @@ func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbg return nil } -func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) - if err != nil { - return err - } - - s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) - s.debugLog("recover grid with group id: %d", s.OrderGroupID) - // filter out the order with the group id belongs to this grid - var openOrdersOnGrid []types.Order - for _, order := range openOrders { - if order.GroupID == s.OrderGroupID { - openOrdersOnGrid = append(openOrdersOnGrid, order) - } - } - - s.logger.Infof("found %d open orders belong to this grid on the %s order book", len(openOrdersOnGrid), s.Symbol) - - // no initial order id means we don't need to recover - // 3/31 updated : Find there may be 0 initial order id when the strategy is not strategy, so we need to add more checking on it. - if s.GridProfitStats.InitialOrderID == 0 && len(openOrdersOnGrid) == 0 { - s.debugLog("new strategy, no need to recover") - return nil - } - - historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) - if !implemented { - s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") - return nil - } - - if err := s.recoverGridWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { - return errors.Wrap(err, "grid recover error") - } - - return nil -} - // openOrdersMismatches verifies if the open orders are on the grid pins // return true if mismatches func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.ExchangeSession) (bool, error) { From 9fa647ed65d9964ee608be11c6b2ae2d04464af8 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 16:12:19 +0800 Subject: [PATCH 0747/1392] rename method --- pkg/strategy/grid2/recover.go | 6 +++--- pkg/strategy/grid2/strategy.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index c50a9fc57f..85ed5a91fd 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -12,7 +12,7 @@ import ( "github.com/pkg/errors" ) -func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { +func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) // if the exchange doesn't support ExchangeTradeHistoryService, do not run recover if !implemented { @@ -64,14 +64,14 @@ func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbg } s.logger.Infof("start to recover") - if err := s.recoverGridWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { + if err := s.recoverWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { return errors.Wrap(err, "grid recover error") } return nil } -func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrdersOnGrid []types.Order) error { +func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrdersOnGrid []types.Order) error { if s.orderQueryService == nil { return fmt.Errorf("orderQueryService is nil, it can't get orders by trade") } diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e6a8d4fc93..89ab8aaa03 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1942,14 +1942,14 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { if s.RecoverGridByScanningTrades { s.debugLog("recover grid by scanning trades") - return s.recoverGridByScanningTrades(ctx, session) + return s.recoverByScanningTrades(ctx, session) } s.debugLog("recover grid by scanning orders") - return s.recoverGridByScanningOrders(ctx, session) + return s.recoverByScanningOrders(ctx, session) } -func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbgo.ExchangeSession) error { +func (s *Strategy) recoverByScanningOrders(ctx context.Context, session *bbgo.ExchangeSession) error { openOrders, err := queryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) if err != nil { return err From c54507e07f3f9c250037c42679ce337faeff4a89 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 17:53:01 +0800 Subject: [PATCH 0748/1392] modif log message --- pkg/strategy/grid2/recover.go | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 85ed5a91fd..659144e816 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -24,26 +24,22 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { - return errors.Wrapf(err, "check if strategy exists error when query open orders") + return errors.Wrapf(err, "unable to query open orders when recovering") } s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) // filter out the order with the group id belongs to this grid - var openOrdersOnGrid []types.Order - for _, order := range openOrders { - if order.GroupID == s.OrderGroupID { - openOrdersOnGrid = append(openOrdersOnGrid, order) - } - } + openOrdersOnGrid := filterOrdersOnGrid(s.OrderGroupID, openOrders) s.logger.Infof("found %d open orders belong to this grid on the %s order book", len(openOrdersOnGrid), s.Symbol) if s.GridProfitStats.InitialOrderID != 0 { - // existiting strategy, need recover + s.logger.Info("InitialOrderID is already there, need to recover") } else if len(openOrdersOnGrid) != 0 { - // open orders on grid is not 0. this strategy is existing, need recover + s.logger.Info("even though InitialOrderID is 0, there are open orders on grid so need to recover") } else { + s.logger.Info("InitialOrderID is 0 and there is no open orders on grid, query trades to check it") // initial order id may be new strategy or lost data in redis, so we need to check trades + open orders // if there are open orders or trades, we need to recover trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ @@ -54,16 +50,16 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex }) if err != nil { - return errors.Wrapf(err, "check if strategy exists error when query trades") + return errors.Wrapf(err, "unable to query trades when recovering") } if len(trades) == 0 { - s.logger.Info("new strategy, no need to recover") + s.logger.Info("0 trades found, it's a new strategy so no need to recover") return nil } } - s.logger.Infof("start to recover") + s.logger.Infof("start recovering") if err := s.recoverWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { return errors.Wrap(err, "grid recover error") } @@ -303,3 +299,16 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return pinOrdersFilled, nil } + +func filterOrdersOnGrid(groupID uint32, orders []types.Order) []types.Order { + var filteredOrders []types.Order + for _, order := range orders { + if order.GroupID != groupID { + continue + } + + filteredOrders = append(filteredOrders, order) + } + + return filteredOrders +} From 542467245e91e8115ee6a132270fdbed162e1887 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 18:00:21 +0800 Subject: [PATCH 0749/1392] remove OrderGroupID checking --- pkg/strategy/grid2/recover.go | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 659144e816..b6c6efc6e8 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -20,8 +20,6 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex return nil } - s.logger.Infof("recover grid with group id: %d", s.OrderGroupID) - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { return errors.Wrapf(err, "unable to query open orders when recovering") @@ -29,17 +27,12 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) - // filter out the order with the group id belongs to this grid - openOrdersOnGrid := filterOrdersOnGrid(s.OrderGroupID, openOrders) - - s.logger.Infof("found %d open orders belong to this grid on the %s order book", len(openOrdersOnGrid), s.Symbol) - if s.GridProfitStats.InitialOrderID != 0 { s.logger.Info("InitialOrderID is already there, need to recover") - } else if len(openOrdersOnGrid) != 0 { - s.logger.Info("even though InitialOrderID is 0, there are open orders on grid so need to recover") + } else if len(openOrders) != 0 { + s.logger.Info("even though InitialOrderID is 0, there are open orders so need to recover") } else { - s.logger.Info("InitialOrderID is 0 and there is no open orders on grid, query trades to check it") + s.logger.Info("InitialOrderID is 0 and there is no open orders, query trades to check it") // initial order id may be new strategy or lost data in redis, so we need to check trades + open orders // if there are open orders or trades, we need to recover trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ @@ -60,7 +53,7 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex } s.logger.Infof("start recovering") - if err := s.recoverWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { + if err := s.recoverWithOpenOrdersByScanningTrades(ctx, historyService, openOrders); err != nil { return errors.Wrap(err, "grid recover error") } @@ -261,11 +254,6 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history // add 1 to avoid duplicate fromTradeID = trade.ID + 1 - // this trade doesn't belong to this grid - if order.GroupID != s.OrderGroupID { - continue - } - // checked the trade's order is filled order pin := order.Price v, exist := pinOrdersOpen[pin] @@ -299,16 +287,3 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return pinOrdersFilled, nil } - -func filterOrdersOnGrid(groupID uint32, orders []types.Order) []types.Order { - var filteredOrders []types.Order - for _, order := range orders { - if order.GroupID != groupID { - continue - } - - filteredOrders = append(filteredOrders, order) - } - - return filteredOrders -} From fba73f11ea33bc722abbc6dc96d719b8d69a2103 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 6 Apr 2023 23:24:08 +0800 Subject: [PATCH 0750/1392] grid2: update metrics and trigger ready callback --- pkg/strategy/grid2/recover.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index b6c6efc6e8..1f8f8a7916 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -6,10 +6,11 @@ import ( "strconv" "time" + "github.com/pkg/errors" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/pkg/errors" ) func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { @@ -77,6 +78,9 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi s.debugLog("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) if expectedOrderNums == openOrdersOnGridNums { // no need to recover + s.EmitGridReady() + s.updateGridNumOfOrdersMetricsWithLock() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } else if expectedOrderNums < openOrdersOnGridNums { return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") From cc5ebd5b2cd8d654b24fece4023fcced201b5034 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 23:57:54 +0800 Subject: [PATCH 0751/1392] move emit ready and update metrics --- pkg/strategy/grid2/recover.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 1f8f8a7916..aaecea8802 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -58,6 +58,16 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex return errors.Wrap(err, "grid recover error") } + // emit ready after recover + s.EmitGridReady() + + // debug and send metrics + // wait for the reverse order to be placed + time.Sleep(2 * time.Second) + debugGrid(s.logger, s.grid, s.orderExecutor.ActiveMakerOrders()) + s.updateGridNumOfOrdersMetricsWithLock() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) + return nil } @@ -122,16 +132,6 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi activeOrderBook.EmitFilled(filledOrder) } - // 6. emit grid ready - s.EmitGridReady() - - // 7. debug and send metrics - // wait for the reverse order to be placed - time.Sleep(2 * time.Second) - debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) - s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) - return nil } From 6029bd268d4b1630dce09d959ff7de8cd8d732e4 Mon Sep 17 00:00:00 2001 From: chiahung Date: Fri, 7 Apr 2023 00:40:32 +0800 Subject: [PATCH 0752/1392] update log message --- pkg/strategy/grid2/recover.go | 66 ++++++++++++++++++---------------- pkg/strategy/grid2/strategy.go | 10 +++--- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index aaecea8802..33f9d93894 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -83,16 +83,16 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi // add open orders to active order book s.addOrdersToActiveOrderBook(openOrdersOnGrid) - expectedOrderNums := s.GridNum - 1 - openOrdersOnGridNums := int64(len(openOrdersOnGrid)) - s.debugLog("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) - if expectedOrderNums == openOrdersOnGridNums { + expectedNumOfOrders := s.GridNum - 1 + numGridOpenOrders := int64(len(openOrdersOnGrid)) + s.debugLog("open orders nums: %d, expected nums: %d", numGridOpenOrders, expectedNumOfOrders) + if expectedNumOfOrders == numGridOpenOrders { // no need to recover s.EmitGridReady() s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil - } else if expectedOrderNums < openOrdersOnGridNums { + } else if expectedNumOfOrders < numGridOpenOrders { return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") } @@ -111,9 +111,10 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi // 3. get the filled orders from pin-order map filledOrders := pinOrdersFilled.AscendingOrders() numFilledOrders := len(filledOrders) - if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { + if numFilledOrders == int(expectedNumOfOrders-numGridOpenOrders) { // nums of filled order is the same as Size - 1 - num(open orders) - } else if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) { + s.logger.Infof("nums of filled order is the same as Size - 1 - len(open orders) : %d = %d - 1 - %d", numFilledOrders, s.grid.Size, numGridOpenOrders) + } else if numFilledOrders == int(expectedNumOfOrders-numGridOpenOrders+1) { filledOrders = filledOrders[1:] } else { return fmt.Errorf("not reasonable num of filled orders") @@ -136,24 +137,17 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi } func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { - s.debugLog("pins: %+v", pins) - s.debugLog("open pin orders:\n%s", pinOrders.String()) - s.debugOrders("filled orders", filledOrders) + s.debugLog("verifying filled grid - pins: %+v", pins) + s.debugLog("verifying filled grid - open pin orders:\n%s", pinOrders.String()) + s.debugOrders("verifying filled grid - filled orders", filledOrders) - for _, filledOrder := range filledOrders { - price := filledOrder.Price - if o, exist := pinOrders[price]; !exist { - return fmt.Errorf("the price (%+v) is not in pins", price) - } else if o.OrderID != 0 { - return fmt.Errorf("there is already an order at this price (%+v)", price) - } else { - pinOrders[price] = filledOrder - } + if err := addOrdersIntoPinOrderMap(pinOrders, filledOrders); err != nil { + return errors.Wrapf(err, "verifying filled grid error when add orders into pin order map") } - s.debugLog("filled pin orders:\n%+v", pinOrders.String()) + s.debugLog("verifying filled grid - filled pin orders:\n%+v", pinOrders.String()) - side := types.SideTypeBuy + expectedSide := types.SideTypeBuy for _, pin := range pins { order, exist := pinOrders[fixedpoint.Value(pin)] if !exist { @@ -164,20 +158,20 @@ func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrd // there must be only one empty pin in the grid // all orders below this pin need to be bid orders, above this pin need to be ask orders if order.OrderID == 0 { - if side == types.SideTypeBuy { - side = types.SideTypeSell + if expectedSide == types.SideTypeBuy { + expectedSide = types.SideTypeSell continue } - return fmt.Errorf("not only one empty order in this grid") + return fmt.Errorf("found more than one empty pins") } - if order.Side != side { - return fmt.Errorf("the side is wrong") + if order.Side != expectedSide { + return fmt.Errorf("the side of order (%s) is wrong, expected: %s", order.Side, expectedSide) } } - if side != types.SideTypeSell { + if expectedSide != types.SideTypeSell { return fmt.Errorf("there is no empty pin in the grid") } @@ -278,9 +272,6 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history continue } pinOrdersFilled[pin] = *order - - // wait 100 ms to avoid rate limit - time.Sleep(100 * time.Millisecond) } // stop condition @@ -291,3 +282,18 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return pinOrdersFilled, nil } + +func addOrdersIntoPinOrderMap(pinOrders PinOrderMap, orders []types.Order) error { + for _, order := range orders { + price := order.Price + if o, exist := pinOrders[price]; !exist { + return fmt.Errorf("the price (%+v) is not in pins", price) + } else if o.OrderID != 0 { + return fmt.Errorf("there is already an order at this price (%+v)", price) + } else { + pinOrders[price] = order + } + } + + return nil +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 89ab8aaa03..45fca871b8 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1216,7 +1216,7 @@ func (s *Strategy) debugOrders(desc string, orders []types.Order) { s.logger.Infof(sb.String()) } -func (s *Strategy) debugGridProfitStats(where string) { +func (s *Strategy) debugGridProfitStats(trigger string) { if !s.Debug { return } @@ -1226,11 +1226,11 @@ func (s *Strategy) debugGridProfitStats(where string) { stats.ProfitEntries = nil b, err := json.Marshal(stats) if err != nil { - s.logger.WithError(err).Errorf("[%s] failed to debug grid profit stats", where) + s.logger.WithError(err).Errorf("[%s] failed to debug grid profit stats", trigger) return } - s.logger.Infof("[%s] grid profit stats: %s", where, string(b)) + s.logger.Infof("trigger %s => grid profit stats : %s", trigger, string(b)) } func (s *Strategy) debugLog(format string, args ...interface{}) { @@ -1941,11 +1941,11 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { if s.RecoverGridByScanningTrades { - s.debugLog("recover grid by scanning trades") + s.debugLog("recovering grid by scanning trades") return s.recoverByScanningTrades(ctx, session) } - s.debugLog("recover grid by scanning orders") + s.debugLog("recovering grid by scanning orders") return s.recoverByScanningOrders(ctx, session) } From afc262da8b249d733ef015ae911ab1767edac5c9 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 11 Apr 2023 14:55:32 +0800 Subject: [PATCH 0753/1392] exits/trailingstop: more logs --- pkg/bbgo/exit_hh_ll_stop.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index 224bacc530..d4feb6336c 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -93,11 +93,13 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close func (s *HigherHighLowerLowStop) updateHighLowNumber(kline types.KLine) { s.klines.Truncate(s.Window - 1) - if s.klines.Len() > 0 { + if s.klines.Len() >= s.Window-1 { if s.klines.GetHigh().Compare(kline.GetHigh()) < 0 { s.highLows = append(s.highLows, types.DirectionUp) + log.Debugf("[hhllStop] new higher high for %s", s.Symbol) } else if s.klines.GetLow().Compare(kline.GetLow()) > 0 { s.highLows = append(s.highLows, types.DirectionDown) + log.Debugf("[hhllStop] new lower low for %s", s.Symbol) } else { s.highLows = append(s.highLows, types.DirectionNone) } @@ -164,7 +166,7 @@ func (s *HigherHighLowerLowStop) Bind(session *ExchangeSession, orderExecutor *G s.updateHighLowNumber(kline) // Close position & reset - if s.activated && s.shouldStop(position) { + if s.shouldStop(position) { err := s.orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "hhllStop") if err != nil { Notify("[hhllStop] Stop of %s triggered but failed to close %s position:", s.Symbol, err) From 7f33b54312faf895f7401fe61f0f1348c3432654 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 11 Apr 2023 15:11:11 +0800 Subject: [PATCH 0754/1392] exits/trailingstop: check parameters --- pkg/bbgo/exit_hh_ll_stop.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index d4feb6336c..afffc0ca0c 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -2,16 +2,13 @@ package bbgo import ( "context" + "fmt" log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) -// TODO: if parameter not set -// TODO: log and notify -// TODO: check all procedures - type HigherHighLowerLowStop struct { Symbol string `json:"symbol"` @@ -156,6 +153,17 @@ func (s *HigherHighLowerLowStop) shouldStop(position *types.Position) bool { } func (s *HigherHighLowerLowStop) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + // Check parameters + if s.Window <= 0 { + panic(fmt.Errorf("[hhllStop] window must be larger than zero")) + } + if s.HighLowWindow <= 0 { + panic(fmt.Errorf("[hhllStop] highLowWindow must be larger than zero")) + } + if s.MaxHighLow <= 0 && s.MinHighLow <= 0 { + panic(fmt.Errorf("[hhllStop] either maxHighLow or minHighLow must be larger than zero")) + } + s.session = session s.orderExecutor = orderExecutor From d4e42426abb3640d17db2fcc0317a680f9e20d5d Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 11 Apr 2023 16:02:54 +0800 Subject: [PATCH 0755/1392] exits/trailingstop: add descriptions for parameters --- config/supertrend.yaml | 30 ++++++++++++++++++------------ pkg/bbgo/exit_hh_ll_stop.go | 11 ++++++++++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 8a0fef446f..5eb57087cc 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -89,20 +89,26 @@ exchangeStrategies: side: both closePosition: 100% - higherHighLowerLowStopLoss: - interval: 1h - window: 20 - highLowWindow: 5 + # interval is the kline interval used by this exit + interval: 15 + # window is used as the range to determining higher highs and lower lows + window: 5 + # highLowWindow is the range to calculate the number of higher highs and lower lows + highLowWindow: 12 + # If the number of higher highs or lower lows with in HighLowWindow is less than MinHighLow, the exit is + # triggered. 0 disables this parameter. Either one of MaxHighLow and MinHighLow must be larger than 0 minHighLow: 2 + # If the number of higher highs or lower lows with in HighLowWindow is more than MaxHighLow, the exit is + # triggered. 0 disables this parameter. Either one of MaxHighLow and MinHighLow must be larger than 0 maxHighLow: 0 + # ActivationRatio is the trigger condition + # When the price goes higher (lower for short position) than this ratio, the stop will be activated. + # You can use this to combine several exits activationRatio: 0.5% + # DeactivationRatio is the kill condition + # When the price goes higher (lower for short position) than this ratio, the stop will be deactivated. + # You can use this to combine several exits deactivationRatio: 10% + # If true, looking for lower lows in long position and higher highs in short position. If false, looking for + # higher highs in long position and lower lows in short position oppositeDirectionAsPosition: false - - higherHighLowerLowStopLoss: - interval: 1h - window: 20 - highLowWindow: 5 - minHighLow: 0 - maxHighLow: 3 - activationRatio: 0.5% - deactivationRatio: 10% - oppositeDirectionAsPosition: true diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index afffc0ca0c..1b094fbc75 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -12,12 +12,19 @@ import ( type HigherHighLowerLowStop struct { Symbol string `json:"symbol"` + // Interval is the kline interval used by this exit. Window is used as the range to determining higher highs and + // lower lows types.IntervalWindow + // HighLowWindow is the range to calculate the number of higher highs and lower lows HighLowWindow int `json:"highLowWindow"` + // If the number of higher highs or lower lows with in HighLowWindow is more than MaxHighLow, the exit is triggered. + // 0 disables this parameter. Either one of MaxHighLow and MinHighLow must be larger than 0 MaxHighLow int `json:"maxHighLow"` + // If the number of higher highs or lower lows with in HighLowWindow is less than MinHighLow, the exit is triggered. + // 0 disables this parameter. Either one of MaxHighLow and MinHighLow must be larger than 0 MinHighLow int `json:"minHighLow"` // ActivationRatio is the trigger condition @@ -30,11 +37,13 @@ type HigherHighLowerLowStop struct { // You can use this to combine several exits DeactivationRatio fixedpoint.Value `json:"deactivationRatio"` + // If true, looking for lower lows in long position and higher highs in short position. If false, looking for higher + // highs in long position and lower lows in short position OppositeDirectionAsPosition bool `json:"oppositeDirectionAsPosition"` klines types.KLineWindow - // activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop + // activated: when the price reaches the min profit price, we set the activated to true to enable hhll stop activated bool highLows []types.Direction From 8d240e9b4c7177d5b19fc68c406b8fec127a7ef5 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Apr 2023 18:21:40 +0800 Subject: [PATCH 0756/1392] maxapi: improve nonce update with retry --- pkg/exchange/max/maxapi/restapi.go | 45 ++++++++++++++++++++++++------ pkg/util/backoff/general.go | 1 + 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index 5e0deeeade..1c2b1773a4 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -23,6 +23,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/util/backoff" "github.com/c9s/bbgo/pkg/version" ) @@ -147,16 +148,41 @@ func (c *RestClient) Auth(key string, secret string) *RestClient { return c } -func (c *RestClient) initNonce() { - var clientTime = time.Now() - var err error - serverTimestamp, err = c.PublicService.Timestamp() - if err != nil { - logger.WithError(err).Panic("failed to sync timestamp with max") +func (c *RestClient) queryAndUpdateServerTimestamp(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + + default: + op := func() error { + serverTs, err := c.PublicService.Timestamp() + if err != nil { + return err + } + if serverTs == 0 { + return errors.New("unexpected zero server timestamp") + } + + clientTime := time.Now() + offset := serverTs - clientTime.Unix() + + atomic.StoreInt64(&serverTimestamp, serverTs) + atomic.StoreInt64(&timeOffset, offset) + + logger.Infof("loaded max server timestamp: %d offset=%d", serverTimestamp, offset) + return nil + } + + if err := backoff.RetryGeneral(ctx, op); err != nil { + logger.WithError(err).Error("unable to sync timestamp with max") + } + } } +} - timeOffset = serverTimestamp - clientTime.Unix() - logger.Infof("loaded max server timestamp: %d offset=%d", serverTimestamp, timeOffset) +func (c *RestClient) initNonce() { + go c.queryAndUpdateServerTimestamp(context.Background()) } func (c *RestClient) getNonce() int64 { @@ -164,7 +190,8 @@ func (c *RestClient) getNonce() int64 { // nonce 與伺服器的時間差不得超過正負30秒,每個 nonce 只能使用一次。 var seconds = time.Now().Unix() var rc = atomic.AddInt64(&reqCount, 1) - return (seconds+timeOffset)*1000 - 1 + int64(math.Mod(float64(rc), 1000.0)) + var offset = atomic.LoadInt64(&timeOffset) + return (seconds+offset)*1000 - 1 + int64(math.Mod(float64(rc), 1000.0)) } func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, m string, refURL string, params url.Values, payload interface{}) (*http.Request, error) { diff --git a/pkg/util/backoff/general.go b/pkg/util/backoff/general.go index 968acd8ef5..e91da06b34 100644 --- a/pkg/util/backoff/general.go +++ b/pkg/util/backoff/general.go @@ -8,6 +8,7 @@ import ( var MaxRetries uint64 = 101 +// RetryGeneral retries operation with max retry times 101 and with the exponential backoff func RetryGeneral(ctx context.Context, op backoff.Operation) (err error) { err = backoff.Retry(op, backoff.WithContext( backoff.WithMaxRetries( From 2ae830911574ed0fc8de099fcdb7b444e1ef6197 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Apr 2023 18:27:19 +0800 Subject: [PATCH 0757/1392] maxapi: add global prefix to the var name --- pkg/exchange/max/maxapi/restapi.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index 1c2b1773a4..a4e1547933 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -69,11 +69,11 @@ var htmlTagPattern = regexp.MustCompile("<[/]?[a-zA-Z-]+.*?>") // The following variables are used for nonce. -// timeOffset is used for nonce -var timeOffset int64 = 0 +// globalTimeOffset is used for nonce +var globalTimeOffset int64 = 0 -// serverTimestamp is used for storing the server timestamp, default to Now -var serverTimestamp = time.Now().Unix() +// globalServerTimestamp is used for storing the server timestamp, default to Now +var globalServerTimestamp = time.Now().Unix() // reqCount is used for nonce, this variable counts the API request count. var reqCount int64 = 1 @@ -167,10 +167,10 @@ func (c *RestClient) queryAndUpdateServerTimestamp(ctx context.Context) { clientTime := time.Now() offset := serverTs - clientTime.Unix() - atomic.StoreInt64(&serverTimestamp, serverTs) - atomic.StoreInt64(&timeOffset, offset) + atomic.StoreInt64(&globalServerTimestamp, serverTs) + atomic.StoreInt64(&globalTimeOffset, offset) - logger.Infof("loaded max server timestamp: %d offset=%d", serverTimestamp, offset) + logger.Infof("loaded max server timestamp: %d offset=%d", globalServerTimestamp, offset) return nil } @@ -190,7 +190,7 @@ func (c *RestClient) getNonce() int64 { // nonce 與伺服器的時間差不得超過正負30秒,每個 nonce 只能使用一次。 var seconds = time.Now().Unix() var rc = atomic.AddInt64(&reqCount, 1) - var offset = atomic.LoadInt64(&timeOffset) + var offset = atomic.LoadInt64(&globalTimeOffset) return (seconds+offset)*1000 - 1 + int64(math.Mod(float64(rc), 1000.0)) } From 845ee3ce3337456a9a2797e39aa49a12f6d34e6b Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Apr 2023 18:28:34 +0800 Subject: [PATCH 0758/1392] maxapi: change info log to debug log level --- pkg/exchange/max/maxapi/restapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index a4e1547933..e75208e586 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -170,7 +170,7 @@ func (c *RestClient) queryAndUpdateServerTimestamp(ctx context.Context) { atomic.StoreInt64(&globalServerTimestamp, serverTs) atomic.StoreInt64(&globalTimeOffset, offset) - logger.Infof("loaded max server timestamp: %d offset=%d", globalServerTimestamp, offset) + logger.Debugf("loaded max server timestamp: %d offset=%d", globalServerTimestamp, offset) return nil } From 6eaacd63a823e1baeb478aad6a0c952b7b906dac Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 13:37:04 +0800 Subject: [PATCH 0759/1392] maxapi: use sync.Once to prevent duplicated update and avoid update negative offset --- pkg/exchange/max/maxapi/restapi.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index e75208e586..24766f8d35 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -15,6 +15,7 @@ import ( "net/url" "regexp" "strings" + "sync" "sync/atomic" "time" @@ -37,6 +38,8 @@ const ( // 2018-09-01 08:00:00 +0800 CST TimestampSince = 1535760000 + + maxAllowedDelayedTimeOffset = -20 ) var httpTransportMaxIdleConnsPerHost = http.DefaultMaxIdleConnsPerHost @@ -76,7 +79,9 @@ var globalTimeOffset int64 = 0 var globalServerTimestamp = time.Now().Unix() // reqCount is used for nonce, this variable counts the API request count. -var reqCount int64 = 1 +var reqCount uint64 = 1 + +var nonceOnce sync.Once // create an isolated http httpTransport rather than the default one var httpTransport = &http.Transport{ @@ -167,6 +172,16 @@ func (c *RestClient) queryAndUpdateServerTimestamp(ctx context.Context) { clientTime := time.Now() offset := serverTs - clientTime.Unix() + if offset < 0 { + // avoid updating a negative offset: server time is before the local time + if offset > maxAllowedDelayedTimeOffset { + return nil + } + + // if the offset is greater than 15 seconds, we should restart + logger.Panicf("max exchange server timestamp offset %d > %d seconds", offset, maxAllowedDelayedTimeOffset) + } + atomic.StoreInt64(&globalServerTimestamp, serverTs) atomic.StoreInt64(&globalTimeOffset, offset) @@ -182,14 +197,16 @@ func (c *RestClient) queryAndUpdateServerTimestamp(ctx context.Context) { } func (c *RestClient) initNonce() { - go c.queryAndUpdateServerTimestamp(context.Background()) + nonceOnce.Do(func() { + go c.queryAndUpdateServerTimestamp(context.Background()) + }) } func (c *RestClient) getNonce() int64 { // nonce 是以正整數表示的時間戳記,代表了從 Unix epoch 到當前時間所經過的毫秒數(ms)。 // nonce 與伺服器的時間差不得超過正負30秒,每個 nonce 只能使用一次。 var seconds = time.Now().Unix() - var rc = atomic.AddInt64(&reqCount, 1) + var rc = atomic.AddUint64(&reqCount, 1) var offset = atomic.LoadInt64(&globalTimeOffset) return (seconds+offset)*1000 - 1 + int64(math.Mod(float64(rc), 1000.0)) } From fb95072e5ba61bdf2cb1cdc939067139cbc4207b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 13:49:29 +0800 Subject: [PATCH 0760/1392] backoff: add default timeout to backoff.RetryGeneral --- pkg/util/backoff/general.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/util/backoff/general.go b/pkg/util/backoff/general.go index e91da06b34..19ae49aceb 100644 --- a/pkg/util/backoff/general.go +++ b/pkg/util/backoff/general.go @@ -2,6 +2,7 @@ package backoff import ( "context" + "time" "github.com/cenkalti/backoff/v4" ) @@ -9,7 +10,10 @@ import ( var MaxRetries uint64 = 101 // RetryGeneral retries operation with max retry times 101 and with the exponential backoff -func RetryGeneral(ctx context.Context, op backoff.Operation) (err error) { +func RetryGeneral(parent context.Context, op backoff.Operation) (err error) { + ctx, cancel := context.WithTimeout(parent, 15*time.Minute) + defer cancel() + err = backoff.Retry(op, backoff.WithContext( backoff.WithMaxRetries( backoff.NewExponentialBackOff(), From c366e98c43f4667c83fa63095ffd68af3c3d02fc Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 14:58:37 +0800 Subject: [PATCH 0761/1392] maxapi: update log message --- pkg/exchange/max/maxapi/restapi.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index 24766f8d35..7dc554a9b6 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -39,7 +39,7 @@ const ( // 2018-09-01 08:00:00 +0800 CST TimestampSince = 1535760000 - maxAllowedDelayedTimeOffset = -20 + maxAllowedNegativeTimeOffset = -20 ) var httpTransportMaxIdleConnsPerHost = http.DefaultMaxIdleConnsPerHost @@ -174,12 +174,12 @@ func (c *RestClient) queryAndUpdateServerTimestamp(ctx context.Context) { if offset < 0 { // avoid updating a negative offset: server time is before the local time - if offset > maxAllowedDelayedTimeOffset { + if offset > maxAllowedNegativeTimeOffset { return nil } // if the offset is greater than 15 seconds, we should restart - logger.Panicf("max exchange server timestamp offset %d > %d seconds", offset, maxAllowedDelayedTimeOffset) + logger.Panicf("max exchange server timestamp offset %d is less than the negative offset %d", offset, maxAllowedNegativeTimeOffset) } atomic.StoreInt64(&globalServerTimestamp, serverTs) From 51c1d47fbcbd04903f4c098eef95a41f0f3d4fb4 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Apr 2023 18:33:51 +0800 Subject: [PATCH 0762/1392] maxapi: move some methods to the rest client level --- pkg/exchange/max/exchange.go | 2 +- pkg/exchange/max/maxapi/account.go | 8 ++++---- pkg/exchange/max/maxapi/account_test.go | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 69e3cb5f90..b7106bc830 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -551,7 +551,7 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { return nil, err } - vipLevel, err := e.client.AccountService.NewGetVipLevelRequest().Do(ctx) + vipLevel, err := e.client.NewGetVipLevelRequest().Do(ctx) if err != nil { return nil, err } diff --git a/pkg/exchange/max/maxapi/account.go b/pkg/exchange/max/maxapi/account.go index 26ce9326ab..ea92f00e8f 100644 --- a/pkg/exchange/max/maxapi/account.go +++ b/pkg/exchange/max/maxapi/account.go @@ -79,8 +79,8 @@ type GetVipLevelRequest struct { client requestgen.AuthenticatedAPIClient } -func (s *AccountService) NewGetVipLevelRequest() *GetVipLevelRequest { - return &GetVipLevelRequest{client: s.client} +func (c *RestClient) NewGetVipLevelRequest() *GetVipLevelRequest { + return &GetVipLevelRequest{client: c} } //go:generate GetRequest -url "v2/members/accounts/:currency" -type GetAccountRequest -responseType .Account @@ -90,8 +90,8 @@ type GetAccountRequest struct { currency string `param:"currency,slug"` } -func (s *AccountService) NewGetAccountRequest() *GetAccountRequest { - return &GetAccountRequest{client: s.client} +func (c *RestClient) NewGetAccountRequest() *GetAccountRequest { + return &GetAccountRequest{client: c} } //go:generate GetRequest -url "v2/members/accounts" -type GetAccountsRequest -responseType []Account diff --git a/pkg/exchange/max/maxapi/account_test.go b/pkg/exchange/max/maxapi/account_test.go index e082586be4..804e173b76 100644 --- a/pkg/exchange/max/maxapi/account_test.go +++ b/pkg/exchange/max/maxapi/account_test.go @@ -38,14 +38,14 @@ func TestAccountService_GetAccountRequest(t *testing.T) { client := NewRestClient(ProductionAPIURL) client.Auth(key, secret) - req := client.AccountService.NewGetAccountRequest() + req := client.NewGetAccountRequest() req.Currency("twd") account, err := req.Do(ctx) assert.NoError(t, err) assert.NotNil(t, account) t.Logf("account: %+v", account) - req2 := client.AccountService.NewGetAccountRequest() + req2 := client.NewGetAccountRequest() req2.Currency("usdt") account, err = req.Do(ctx) assert.NoError(t, err) @@ -64,7 +64,7 @@ func TestAccountService_GetVipLevelRequest(t *testing.T) { client := NewRestClient(ProductionAPIURL) client.Auth(key, secret) - req := client.AccountService.NewGetVipLevelRequest() + req := client.NewGetVipLevelRequest() vipLevel, err := req.Do(ctx) assert.NoError(t, err) assert.NotNil(t, vipLevel) From 3ad553a876a630b3cbb7a39853008cb8a6b2516f Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Apr 2023 18:36:10 +0800 Subject: [PATCH 0763/1392] max: move methods --- pkg/exchange/max/exchange.go | 4 ++-- pkg/exchange/max/maxapi/account.go | 12 ++++++------ pkg/exchange/max/maxapi/account_test.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index b7106bc830..25997fa57a 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -642,7 +642,7 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since } log.Infof("querying withdraw %s: %s <=> %s", asset, startTime, endTime) - req := e.client.AccountService.NewGetWithdrawalHistoryRequest() + req := e.client.NewGetWithdrawalHistoryRequest() if len(asset) > 0 { req.Currency(toLocalCurrency(asset)) } @@ -739,7 +739,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, log.Infof("querying deposit history %s: %s <=> %s", asset, startTime, endTime) - req := e.client.AccountService.NewGetDepositHistoryRequest() + req := e.client.NewGetDepositHistoryRequest() if len(asset) > 0 { req.Currency(toLocalCurrency(asset)) } diff --git a/pkg/exchange/max/maxapi/account.go b/pkg/exchange/max/maxapi/account.go index ea92f00e8f..d4f8800144 100644 --- a/pkg/exchange/max/maxapi/account.go +++ b/pkg/exchange/max/maxapi/account.go @@ -99,8 +99,8 @@ type GetAccountsRequest struct { client requestgen.AuthenticatedAPIClient } -func (s *AccountService) NewGetAccountsRequest() *GetAccountsRequest { - return &GetAccountsRequest{client: s.client} +func (c *RestClient) NewGetAccountsRequest() *GetAccountsRequest { + return &GetAccountsRequest{client: c} } type Deposit struct { @@ -126,9 +126,9 @@ type GetDepositHistoryRequest struct { limit *int `param:"limit"` } -func (s *AccountService) NewGetDepositHistoryRequest() *GetDepositHistoryRequest { +func (c *RestClient) NewGetDepositHistoryRequest() *GetDepositHistoryRequest { return &GetDepositHistoryRequest{ - client: s.client, + client: c, } } @@ -165,8 +165,8 @@ type GetWithdrawHistoryRequest struct { limit *int `param:"limit"` } -func (s *AccountService) NewGetWithdrawalHistoryRequest() *GetWithdrawHistoryRequest { +func (c *RestClient) NewGetWithdrawalHistoryRequest() *GetWithdrawHistoryRequest { return &GetWithdrawHistoryRequest{ - client: s.client, + client: c, } } diff --git a/pkg/exchange/max/maxapi/account_test.go b/pkg/exchange/max/maxapi/account_test.go index 804e173b76..b074f3d9a4 100644 --- a/pkg/exchange/max/maxapi/account_test.go +++ b/pkg/exchange/max/maxapi/account_test.go @@ -18,7 +18,7 @@ func TestAccountService_GetAccountsRequest(t *testing.T) { client := NewRestClient(ProductionAPIURL) client.Auth(key, secret) - req := client.AccountService.NewGetAccountsRequest() + req := client.NewGetAccountsRequest() accounts, err := req.Do(ctx) assert.NoError(t, err) assert.NotNil(t, accounts) @@ -82,7 +82,7 @@ func TestAccountService_GetWithdrawHistoryRequest(t *testing.T) { client := NewRestClient(ProductionAPIURL) client.Auth(key, secret) - req := client.AccountService.NewGetWithdrawalHistoryRequest() + req := client.NewGetWithdrawalHistoryRequest() req.Currency("usdt") withdraws, err := req.Do(ctx) assert.NoError(t, err) @@ -102,7 +102,7 @@ func TestAccountService_NewGetDepositHistoryRequest(t *testing.T) { client := NewRestClient(ProductionAPIURL) client.Auth(key, secret) - req := client.AccountService.NewGetDepositHistoryRequest() + req := client.NewGetDepositHistoryRequest() req.Currency("usdt") deposits, err := req.Do(ctx) assert.NoError(t, err) From d95daba3f07d90676aec5200b4665c0d51419f92 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Apr 2023 18:36:16 +0800 Subject: [PATCH 0764/1392] maxapi: update requestgen files --- .../maxapi/get_account_request_requestgen.go | 36 ++++++------ .../maxapi/get_accounts_request_requestgen.go | 36 ++++++------ .../get_deposit_history_request_requestgen.go | 58 +++++++++++++------ .../get_vip_level_request_requestgen.go | 36 ++++++------ ...get_withdraw_history_request_requestgen.go | 58 +++++++++++++------ 5 files changed, 134 insertions(+), 90 deletions(-) diff --git a/pkg/exchange/max/maxapi/get_account_request_requestgen.go b/pkg/exchange/max/maxapi/get_account_request_requestgen.go index 4d71c47092..c69b72011d 100644 --- a/pkg/exchange/max/maxapi/get_account_request_requestgen.go +++ b/pkg/exchange/max/maxapi/get_account_request_requestgen.go @@ -21,8 +21,8 @@ func (g *GetAccountRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -44,13 +44,13 @@ func (g *GetAccountRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - if g.isVarSlice(v) { - g.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) }) } else { - query.Add(k, fmt.Sprintf("%v", v)) + query.Add(_k, fmt.Sprintf("%v", _v)) } } @@ -80,24 +80,24 @@ func (g *GetAccountRequest) GetSlugParameters() (map[string]interface{}, error) } func (g *GetAccountRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } -func (g *GetAccountRequest) iterateSlice(slice interface{}, f func(it interface{})) { +func (g *GetAccountRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) } } -func (g *GetAccountRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) +func (g *GetAccountRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: return true @@ -112,8 +112,8 @@ func (g *GetAccountRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil diff --git a/pkg/exchange/max/maxapi/get_accounts_request_requestgen.go b/pkg/exchange/max/maxapi/get_accounts_request_requestgen.go index b475ca8607..7e497c98ce 100644 --- a/pkg/exchange/max/maxapi/get_accounts_request_requestgen.go +++ b/pkg/exchange/max/maxapi/get_accounts_request_requestgen.go @@ -16,8 +16,8 @@ func (g *GetAccountsRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -39,13 +39,13 @@ func (g *GetAccountsRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - if g.isVarSlice(v) { - g.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) }) } else { - query.Add(k, fmt.Sprintf("%v", v)) + query.Add(_k, fmt.Sprintf("%v", _v)) } } @@ -70,24 +70,24 @@ func (g *GetAccountsRequest) GetSlugParameters() (map[string]interface{}, error) } func (g *GetAccountsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } -func (g *GetAccountsRequest) iterateSlice(slice interface{}, f func(it interface{})) { +func (g *GetAccountsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) } } -func (g *GetAccountsRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) +func (g *GetAccountsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: return true @@ -102,8 +102,8 @@ func (g *GetAccountsRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil diff --git a/pkg/exchange/max/maxapi/get_deposit_history_request_requestgen.go b/pkg/exchange/max/maxapi/get_deposit_history_request_requestgen.go index 444272a554..f7d3254cd2 100644 --- a/pkg/exchange/max/maxapi/get_deposit_history_request_requestgen.go +++ b/pkg/exchange/max/maxapi/get_deposit_history_request_requestgen.go @@ -41,8 +41,8 @@ func (g *GetDepositHistoryRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -63,6 +63,17 @@ func (g *GetDepositHistoryRequest) GetParameters() (map[string]interface{}, erro if g.from != nil { from := *g.from + // TEMPLATE check-valid-values + switch from { + case globalTimeOffset, reqCount: + params["from"] = from + + default: + return nil, fmt.Errorf("from value %v is invalid", from) + + } + // END TEMPLATE check-valid-values + // assign parameter of from params["from"] = from } else { @@ -71,6 +82,17 @@ func (g *GetDepositHistoryRequest) GetParameters() (map[string]interface{}, erro if g.to != nil { to := *g.to + // TEMPLATE check-valid-values + switch to { + case globalTimeOffset, reqCount: + params["to"] = to + + default: + return nil, fmt.Errorf("to value %v is invalid", to) + + } + // END TEMPLATE check-valid-values + // assign parameter of to params["to"] = to } else { @@ -104,13 +126,13 @@ func (g *GetDepositHistoryRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - if g.isVarSlice(v) { - g.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) }) } else { - query.Add(k, fmt.Sprintf("%v", v)) + query.Add(_k, fmt.Sprintf("%v", _v)) } } @@ -135,24 +157,24 @@ func (g *GetDepositHistoryRequest) GetSlugParameters() (map[string]interface{}, } func (g *GetDepositHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } -func (g *GetDepositHistoryRequest) iterateSlice(slice interface{}, f func(it interface{})) { +func (g *GetDepositHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) } } -func (g *GetDepositHistoryRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) +func (g *GetDepositHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: return true @@ -167,8 +189,8 @@ func (g *GetDepositHistoryRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil diff --git a/pkg/exchange/max/maxapi/get_vip_level_request_requestgen.go b/pkg/exchange/max/maxapi/get_vip_level_request_requestgen.go index e66465f805..790a9cd52d 100644 --- a/pkg/exchange/max/maxapi/get_vip_level_request_requestgen.go +++ b/pkg/exchange/max/maxapi/get_vip_level_request_requestgen.go @@ -16,8 +16,8 @@ func (g *GetVipLevelRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -39,13 +39,13 @@ func (g *GetVipLevelRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - if g.isVarSlice(v) { - g.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) }) } else { - query.Add(k, fmt.Sprintf("%v", v)) + query.Add(_k, fmt.Sprintf("%v", _v)) } } @@ -70,24 +70,24 @@ func (g *GetVipLevelRequest) GetSlugParameters() (map[string]interface{}, error) } func (g *GetVipLevelRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } -func (g *GetVipLevelRequest) iterateSlice(slice interface{}, f func(it interface{})) { +func (g *GetVipLevelRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) } } -func (g *GetVipLevelRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) +func (g *GetVipLevelRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: return true @@ -102,8 +102,8 @@ func (g *GetVipLevelRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil diff --git a/pkg/exchange/max/maxapi/get_withdraw_history_request_requestgen.go b/pkg/exchange/max/maxapi/get_withdraw_history_request_requestgen.go index 3f66dbd43e..1e0dc8281b 100644 --- a/pkg/exchange/max/maxapi/get_withdraw_history_request_requestgen.go +++ b/pkg/exchange/max/maxapi/get_withdraw_history_request_requestgen.go @@ -41,8 +41,8 @@ func (g *GetWithdrawHistoryRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -60,6 +60,17 @@ func (g *GetWithdrawHistoryRequest) GetParameters() (map[string]interface{}, err if g.from != nil { from := *g.from + // TEMPLATE check-valid-values + switch from { + case globalTimeOffset, reqCount: + params["from"] = from + + default: + return nil, fmt.Errorf("from value %v is invalid", from) + + } + // END TEMPLATE check-valid-values + // assign parameter of from params["from"] = from } else { @@ -68,6 +79,17 @@ func (g *GetWithdrawHistoryRequest) GetParameters() (map[string]interface{}, err if g.to != nil { to := *g.to + // TEMPLATE check-valid-values + switch to { + case globalTimeOffset, reqCount: + params["to"] = to + + default: + return nil, fmt.Errorf("to value %v is invalid", to) + + } + // END TEMPLATE check-valid-values + // assign parameter of to params["to"] = to } else { @@ -101,13 +123,13 @@ func (g *GetWithdrawHistoryRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - if g.isVarSlice(v) { - g.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) }) } else { - query.Add(k, fmt.Sprintf("%v", v)) + query.Add(_k, fmt.Sprintf("%v", _v)) } } @@ -132,24 +154,24 @@ func (g *GetWithdrawHistoryRequest) GetSlugParameters() (map[string]interface{}, } func (g *GetWithdrawHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } -func (g *GetWithdrawHistoryRequest) iterateSlice(slice interface{}, f func(it interface{})) { +func (g *GetWithdrawHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) } } -func (g *GetWithdrawHistoryRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) +func (g *GetWithdrawHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: return true @@ -164,8 +186,8 @@ func (g *GetWithdrawHistoryRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil From 4944fdda2dc8fe7c083d6ed94fcbc0eb260831f1 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Apr 2023 18:46:29 +0800 Subject: [PATCH 0765/1392] max: replace time type fields --- pkg/exchange/max/exchange.go | 4 ++-- pkg/exchange/max/maxapi/account.go | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 25997fa57a..5752e2a3f8 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -690,7 +690,7 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since txIDs[d.TxID] = struct{}{} withdraw := types.Withdraw{ Exchange: types.ExchangeMax, - ApplyTime: types.Time(time.Unix(d.CreatedAt, 0)), + ApplyTime: types.Time(d.CreatedAt), Asset: toGlobalCurrency(d.Currency), Amount: d.Amount, Address: "", @@ -710,7 +710,7 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since startTime = endTime } else { // its in descending order, so we get the first record - startTime = time.Unix(withdraws[0].CreatedAt, 0) + startTime = withdraws[0].CreatedAt.Time() } } diff --git a/pkg/exchange/max/maxapi/account.go b/pkg/exchange/max/maxapi/account.go index d4f8800144..fc48c85bf4 100644 --- a/pkg/exchange/max/maxapi/account.go +++ b/pkg/exchange/max/maxapi/account.go @@ -8,6 +8,7 @@ import ( "github.com/c9s/requestgen" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) type AccountService struct { @@ -147,11 +148,11 @@ type Withdraw struct { // "failed", "pending", "confirmed", // "kgi_manually_processing", "kgi_manually_confirmed", "kgi_possible_failed", // "sygna_verifying" - State string `json:"state"` - Confirmations int `json:"confirmations"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - Notes string `json:"notes"` + State string `json:"state"` + Confirmations int `json:"confirmations"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + UpdatedAt types.MillisecondTimestamp `json:"updated_at"` + Notes string `json:"notes"` } //go:generate GetRequest -url "v2/withdrawals" -type GetWithdrawHistoryRequest -responseType []Withdraw From fd6dfc5c9e250a79a9ff46c0166d7aab52a5da3d Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Apr 2023 18:52:23 +0800 Subject: [PATCH 0766/1392] maxapi: change time field to time.Time and update the generated code --- pkg/exchange/max/exchange.go | 4 +-- pkg/exchange/max/maxapi/account.go | 12 ++++--- ...get_withdraw_history_request_requestgen.go | 34 +++++-------------- 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 5752e2a3f8..efce18157a 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -648,8 +648,8 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since } withdraws, err := req. - From(startTime.Unix()). - To(endTime.Unix()). + From(startTime). + To(endTime). Limit(limit). Do(ctx) diff --git a/pkg/exchange/max/maxapi/account.go b/pkg/exchange/max/maxapi/account.go index fc48c85bf4..c5b01d9bee 100644 --- a/pkg/exchange/max/maxapi/account.go +++ b/pkg/exchange/max/maxapi/account.go @@ -5,6 +5,8 @@ package max //go:generate -command DeleteRequest requestgen -method DELETE import ( + "time" + "github.com/c9s/requestgen" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -159,11 +161,11 @@ type Withdraw struct { type GetWithdrawHistoryRequest struct { client requestgen.AuthenticatedAPIClient - currency string `param:"currency"` - from *int64 `param:"from"` // seconds - to *int64 `param:"to"` // seconds - state *string `param:"state"` // submitting, submitted, rejected, accepted, checking, refunded, canceled, suspect - limit *int `param:"limit"` + currency string `param:"currency"` + from *time.Time `param:"from,seconds"` // seconds + to *time.Time `param:"to,seconds"` // seconds + state *string `param:"state"` // submitting, submitted, rejected, accepted, checking, refunded, canceled, suspect + limit *int `param:"limit"` } func (c *RestClient) NewGetWithdrawalHistoryRequest() *GetWithdrawHistoryRequest { diff --git a/pkg/exchange/max/maxapi/get_withdraw_history_request_requestgen.go b/pkg/exchange/max/maxapi/get_withdraw_history_request_requestgen.go index 1e0dc8281b..200e7ee817 100644 --- a/pkg/exchange/max/maxapi/get_withdraw_history_request_requestgen.go +++ b/pkg/exchange/max/maxapi/get_withdraw_history_request_requestgen.go @@ -9,6 +9,8 @@ import ( "net/url" "reflect" "regexp" + "strconv" + "time" ) func (g *GetWithdrawHistoryRequest) Currency(currency string) *GetWithdrawHistoryRequest { @@ -16,12 +18,12 @@ func (g *GetWithdrawHistoryRequest) Currency(currency string) *GetWithdrawHistor return g } -func (g *GetWithdrawHistoryRequest) From(from int64) *GetWithdrawHistoryRequest { +func (g *GetWithdrawHistoryRequest) From(from time.Time) *GetWithdrawHistoryRequest { g.from = &from return g } -func (g *GetWithdrawHistoryRequest) To(to int64) *GetWithdrawHistoryRequest { +func (g *GetWithdrawHistoryRequest) To(to time.Time) *GetWithdrawHistoryRequest { g.to = &to return g } @@ -60,38 +62,18 @@ func (g *GetWithdrawHistoryRequest) GetParameters() (map[string]interface{}, err if g.from != nil { from := *g.from - // TEMPLATE check-valid-values - switch from { - case globalTimeOffset, reqCount: - params["from"] = from - - default: - return nil, fmt.Errorf("from value %v is invalid", from) - - } - // END TEMPLATE check-valid-values - // assign parameter of from - params["from"] = from + // convert time.Time to seconds time stamp + params["from"] = strconv.FormatInt(from.Unix(), 10) } else { } // check to field -> json key to if g.to != nil { to := *g.to - // TEMPLATE check-valid-values - switch to { - case globalTimeOffset, reqCount: - params["to"] = to - - default: - return nil, fmt.Errorf("to value %v is invalid", to) - - } - // END TEMPLATE check-valid-values - // assign parameter of to - params["to"] = to + // convert time.Time to seconds time stamp + params["to"] = strconv.FormatInt(to.Unix(), 10) } else { } // check state field -> json key state From fc3ffe399e1635b86d6b78172afa2078e843ea46 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 13:09:31 +0800 Subject: [PATCH 0767/1392] maxapi: update time type fields --- pkg/exchange/max/exchange.go | 8 ++--- pkg/exchange/max/maxapi/account.go | 28 +++++++-------- .../get_deposit_history_request_requestgen.go | 34 +++++-------------- 3 files changed, 26 insertions(+), 44 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index efce18157a..28573c5898 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -745,8 +745,8 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, } deposits, err := req. - From(startTime.Unix()). - To(endTime.Unix()). + From(startTime). + To(endTime). Limit(limit). Do(ctx) @@ -762,7 +762,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, allDeposits = append(allDeposits, types.Deposit{ Exchange: types.ExchangeMax, - Time: types.Time(time.Unix(d.CreatedAt, 0)), + Time: types.Time(d.CreatedAt), Amount: d.Amount, Asset: toGlobalCurrency(d.Currency), Address: "", // not supported @@ -775,7 +775,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, if len(deposits) < limit { startTime = endTime } else { - startTime = time.Unix(deposits[0].CreatedAt, 0) + startTime = time.Time(deposits[0].CreatedAt) } } diff --git a/pkg/exchange/max/maxapi/account.go b/pkg/exchange/max/maxapi/account.go index c5b01d9bee..91d98ebacf 100644 --- a/pkg/exchange/max/maxapi/account.go +++ b/pkg/exchange/max/maxapi/account.go @@ -107,26 +107,26 @@ func (c *RestClient) NewGetAccountsRequest() *GetAccountsRequest { } type Deposit struct { - Currency string `json:"currency"` - CurrencyVersion string `json:"currency_version"` // "eth" - Amount fixedpoint.Value `json:"amount"` - Fee fixedpoint.Value `json:"fee"` - TxID string `json:"txid"` - State string `json:"state"` - Confirmations int64 `json:"confirmations"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Currency string `json:"currency"` + CurrencyVersion string `json:"currency_version"` // "eth" + Amount fixedpoint.Value `json:"amount"` + Fee fixedpoint.Value `json:"fee"` + TxID string `json:"txid"` + State string `json:"state"` + Confirmations int64 `json:"confirmations"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + UpdatedAt types.MillisecondTimestamp `json:"updated_at"` } //go:generate GetRequest -url "v2/deposits" -type GetDepositHistoryRequest -responseType []Deposit type GetDepositHistoryRequest struct { client requestgen.AuthenticatedAPIClient - currency *string `param:"currency"` - from *int64 `param:"from"` // seconds - to *int64 `param:"to"` // seconds - state *string `param:"state"` // submitting, submitted, rejected, accepted, checking, refunded, canceled, suspect - limit *int `param:"limit"` + currency *string `param:"currency"` + from *time.Time `param:"from,seconds"` // seconds + to *time.Time `param:"to,seconds"` // seconds + state *string `param:"state"` // submitting, submitted, rejected, accepted, checking, refunded, canceled, suspect + limit *int `param:"limit"` } func (c *RestClient) NewGetDepositHistoryRequest() *GetDepositHistoryRequest { diff --git a/pkg/exchange/max/maxapi/get_deposit_history_request_requestgen.go b/pkg/exchange/max/maxapi/get_deposit_history_request_requestgen.go index f7d3254cd2..a60f24f9aa 100644 --- a/pkg/exchange/max/maxapi/get_deposit_history_request_requestgen.go +++ b/pkg/exchange/max/maxapi/get_deposit_history_request_requestgen.go @@ -9,6 +9,8 @@ import ( "net/url" "reflect" "regexp" + "strconv" + "time" ) func (g *GetDepositHistoryRequest) Currency(currency string) *GetDepositHistoryRequest { @@ -16,12 +18,12 @@ func (g *GetDepositHistoryRequest) Currency(currency string) *GetDepositHistoryR return g } -func (g *GetDepositHistoryRequest) From(from int64) *GetDepositHistoryRequest { +func (g *GetDepositHistoryRequest) From(from time.Time) *GetDepositHistoryRequest { g.from = &from return g } -func (g *GetDepositHistoryRequest) To(to int64) *GetDepositHistoryRequest { +func (g *GetDepositHistoryRequest) To(to time.Time) *GetDepositHistoryRequest { g.to = &to return g } @@ -63,38 +65,18 @@ func (g *GetDepositHistoryRequest) GetParameters() (map[string]interface{}, erro if g.from != nil { from := *g.from - // TEMPLATE check-valid-values - switch from { - case globalTimeOffset, reqCount: - params["from"] = from - - default: - return nil, fmt.Errorf("from value %v is invalid", from) - - } - // END TEMPLATE check-valid-values - // assign parameter of from - params["from"] = from + // convert time.Time to seconds time stamp + params["from"] = strconv.FormatInt(from.Unix(), 10) } else { } // check to field -> json key to if g.to != nil { to := *g.to - // TEMPLATE check-valid-values - switch to { - case globalTimeOffset, reqCount: - params["to"] = to - - default: - return nil, fmt.Errorf("to value %v is invalid", to) - - } - // END TEMPLATE check-valid-values - // assign parameter of to - params["to"] = to + // convert time.Time to seconds time stamp + params["to"] = strconv.FormatInt(to.Unix(), 10) } else { } // check state field -> json key state From c1b7f7fd95a91c58e81dc113bd9906f94123e194 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 14:12:36 +0800 Subject: [PATCH 0768/1392] maxapi: replace the legacy get markets api --- .../max/maxapi/get_markets_request.go | 18 +++ .../maxapi/get_markets_request_requestgen.go | 135 ++++++++++++++++++ .../max/maxapi/get_timestamp_request.go | 20 +++ .../get_timestamp_request_requestgen.go | 135 ++++++++++++++++++ pkg/exchange/max/maxapi/public.go | 40 ++---- 5 files changed, 318 insertions(+), 30 deletions(-) create mode 100644 pkg/exchange/max/maxapi/get_markets_request.go create mode 100644 pkg/exchange/max/maxapi/get_markets_request_requestgen.go create mode 100644 pkg/exchange/max/maxapi/get_timestamp_request.go create mode 100644 pkg/exchange/max/maxapi/get_timestamp_request_requestgen.go diff --git a/pkg/exchange/max/maxapi/get_markets_request.go b/pkg/exchange/max/maxapi/get_markets_request.go new file mode 100644 index 0000000000..8cbbf70ed9 --- /dev/null +++ b/pkg/exchange/max/maxapi/get_markets_request.go @@ -0,0 +1,18 @@ +package max + +import ( + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +//go:generate GetRequest -url "/api/v2/markets" -type GetMarketsRequest -responseType []Market +type GetMarketsRequest struct { + client requestgen.APIClient +} + +func (c *RestClient) NewGetMarketsRequest() *GetMarketsRequest { + return &GetMarketsRequest{client: c} +} diff --git a/pkg/exchange/max/maxapi/get_markets_request_requestgen.go b/pkg/exchange/max/maxapi/get_markets_request_requestgen.go new file mode 100644 index 0000000000..0084bfaebc --- /dev/null +++ b/pkg/exchange/max/maxapi/get_markets_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method GET -url /api/v2/markets -type GetMarketsRequest -responseType []Market"; DO NOT EDIT. + +package max + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarketsRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarketsRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarketsRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarketsRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarketsRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarketsRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarketsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarketsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarketsRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarketsRequest) Do(ctx context.Context) ([]Market, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/v2/markets" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []Market + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/get_timestamp_request.go b/pkg/exchange/max/maxapi/get_timestamp_request.go new file mode 100644 index 0000000000..9feef84468 --- /dev/null +++ b/pkg/exchange/max/maxapi/get_timestamp_request.go @@ -0,0 +1,20 @@ +package max + +import ( + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +type Timestamp int64 + +//go:generate GetRequest -url "/api/v2/timestamp" -type GetTimestampRequest -responseType .Timestamp +type GetTimestampRequest struct { + client requestgen.APIClient +} + +func (c *RestClient) NewGetTimestampRequest() *GetTimestampRequest { + return &GetTimestampRequest{client: c} +} diff --git a/pkg/exchange/max/maxapi/get_timestamp_request_requestgen.go b/pkg/exchange/max/maxapi/get_timestamp_request_requestgen.go new file mode 100644 index 0000000000..20777a92a1 --- /dev/null +++ b/pkg/exchange/max/maxapi/get_timestamp_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method GET -url /api/v2/timestamp -type GetTimestampRequest -responseType .Timestamp"; DO NOT EDIT. + +package max + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetTimestampRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetTimestampRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetTimestampRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetTimestampRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetTimestampRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetTimestampRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetTimestampRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetTimestampRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetTimestampRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetTimestampRequest) Do(ctx context.Context) (*Timestamp, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/v2/timestamp" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse Timestamp + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/public.go b/pkg/exchange/max/maxapi/public.go index 4591a3274c..bc81f7f52e 100644 --- a/pkg/exchange/max/maxapi/public.go +++ b/pkg/exchange/max/maxapi/public.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "github.com/c9s/requestgen" "github.com/pkg/errors" "github.com/valyala/fastjson" @@ -16,7 +15,7 @@ import ( ) type PublicService struct { - client requestgen.AuthenticatedAPIClient + client *RestClient } type Market struct { @@ -46,43 +45,24 @@ type Ticker struct { VolumeInBTC string `json:"vol_in_btc"` } -func (s *PublicService) Timestamp() (serverTimestamp int64, err error) { - // sync timestamp with server - req, err := s.client.NewRequest(context.Background(), "GET", "v2/timestamp", nil, nil) - if err != nil { - return 0, err - } - - response, err := s.client.SendRequest(req) - if err != nil { - return 0, err - } - - err = response.DecodeJSON(&serverTimestamp) - if err != nil { - return 0, err +func (s *PublicService) Timestamp() (int64, error) { + req := s.client.NewGetTimestampRequest() + ts, err := req.Do(context.Background()) + if err != nil || ts == nil { + return 0, nil } - return serverTimestamp, nil + return int64(*ts), nil } func (s *PublicService) Markets() ([]Market, error) { - req, err := s.client.NewRequest(context.Background(), "GET", "v2/markets", url.Values{}, nil) + req := s.client.NewGetMarketsRequest() + markets, err := req.Do(context.Background()) if err != nil { return nil, err } - response, err := s.client.SendRequest(req) - if err != nil { - return nil, err - } - - var m []Market - if err := response.DecodeJSON(&m); err != nil { - return nil, err - } - - return m, nil + return markets, nil } func (s *PublicService) Tickers() (map[string]Ticker, error) { From d1c6fde97882e218ea338e652533b8401bbc5a8e Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 14:42:30 +0800 Subject: [PATCH 0769/1392] update github.com/c9s/requestgen@v1.3.4 --- go.mod | 15 ++++++++------- go.sum | 12 ++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 9e11b06f71..1db141fd26 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/Masterminds/squirrel v1.5.3 github.com/adshao/go-binance/v2 v2.4.1 github.com/c-bata/goptuna v0.8.1 - github.com/c9s/requestgen v1.3.0 + github.com/c9s/requestgen v1.3.4 github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b github.com/cenkalti/backoff/v4 v4.2.0 github.com/cheggaaa/pb/v3 v3.0.8 @@ -55,7 +55,7 @@ require ( github.com/x-cray/logrus-prefixed-formatter v0.5.2 github.com/zserge/lorca v0.1.9 go.uber.org/multierr v1.7.0 - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/sync v0.1.0 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba gonum.org/v1/gonum v0.8.2 google.golang.org/grpc v1.45.0 @@ -137,11 +137,12 @@ require ( go.uber.org/atomic v1.9.0 // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect - golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.11 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/tools v0.7.0 // indirect google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect diff --git a/go.sum b/go.sum index a925fed026..1655f216e6 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,8 @@ github.com/c-bata/goptuna v0.8.1 h1:25+n1MLv0yvCsD56xv4nqIus3oLHL9GuPAZDLIqmX1U= github.com/c-bata/goptuna v0.8.1/go.mod h1:knmS8+Iyq5PPy1YUeIEq0pMFR4Y6x7z/CySc9HlZTCY= github.com/c9s/requestgen v1.3.0 h1:3cTHvWIlrc37nGEdJLIO07XaVidDeOwcew06csBz++U= github.com/c9s/requestgen v1.3.0/go.mod h1:5n9FU3hr5307IiXAmbMiZbHYaPiys1u9jCWYexZr9qA= +github.com/c9s/requestgen v1.3.4 h1:kK2rIO3OAt9JoY5gT0OSkSpq0dy/+JeuI22FwSKpUrY= +github.com/c9s/requestgen v1.3.4/go.mod h1:wp4saiPdh0zLF5AkopGCqPQfy9Q5xvRh+TQBOA1l1r4= github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b h1:wT8c03PHLv7+nZUIGqxAzRvIfYHNxMCNVWwvdGkOXTs= github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b/go.mod h1:EKObf66Cp7erWxym2de+07qNN5T1N9PXxHdh97N44EQ= github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= @@ -788,6 +790,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -833,6 +837,7 @@ golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -851,6 +856,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -914,9 +920,12 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -926,6 +935,7 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -985,6 +995,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 012ef4a6f9239da139335e89714ed8932a6d94ef Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 14:44:11 +0800 Subject: [PATCH 0770/1392] maxapi: refactor and clean up public service api --- .../max/maxapi/get_tickers_request.go | 20 +++ .../maxapi/get_tickers_request_requestgen.go | 135 ++++++++++++++++++ pkg/exchange/max/maxapi/public.go | 32 +---- pkg/exchange/max/maxapi/reward_test.go | 27 ++++ 4 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 pkg/exchange/max/maxapi/get_tickers_request.go create mode 100644 pkg/exchange/max/maxapi/get_tickers_request_requestgen.go diff --git a/pkg/exchange/max/maxapi/get_tickers_request.go b/pkg/exchange/max/maxapi/get_tickers_request.go new file mode 100644 index 0000000000..a8c82c4266 --- /dev/null +++ b/pkg/exchange/max/maxapi/get_tickers_request.go @@ -0,0 +1,20 @@ +package max + +import ( + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +type TickerMap map[string]Ticker + +//go:generate GetRequest -url "/api/v2/tickers" -type GetTickersRequest -responseType .TickerMap +type GetTickersRequest struct { + client requestgen.APIClient +} + +func (c *RestClient) NewGetTickersRequest() *GetTickersRequest { + return &GetTickersRequest{client: c} +} diff --git a/pkg/exchange/max/maxapi/get_tickers_request_requestgen.go b/pkg/exchange/max/maxapi/get_tickers_request_requestgen.go new file mode 100644 index 0000000000..8c072fb775 --- /dev/null +++ b/pkg/exchange/max/maxapi/get_tickers_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method GET -url /api/v2/tickers -type GetTickersRequest -responseType .TickerMap"; DO NOT EDIT. + +package max + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetTickersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetTickersRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetTickersRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetTickersRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetTickersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetTickersRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetTickersRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetTickersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetTickersRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetTickersRequest) Do(ctx context.Context) (TickerMap, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/v2/tickers" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse TickerMap + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/public.go b/pkg/exchange/max/maxapi/public.go index bc81f7f52e..b71fe97794 100644 --- a/pkg/exchange/max/maxapi/public.go +++ b/pkg/exchange/max/maxapi/public.go @@ -65,35 +65,9 @@ func (s *PublicService) Markets() ([]Market, error) { return markets, nil } -func (s *PublicService) Tickers() (map[string]Ticker, error) { - var endPoint = "v2/tickers" - req, err := s.client.NewRequest(context.Background(), "GET", endPoint, url.Values{}, nil) - if err != nil { - return nil, err - } - - response, err := s.client.SendRequest(req) - if err != nil { - return nil, err - } - - v, err := fastjson.ParseBytes(response.Body) - if err != nil { - return nil, err - } - - o, err := v.Object() - if err != nil { - return nil, err - } - - var tickers = make(map[string]Ticker) - o.Visit(func(key []byte, v *fastjson.Value) { - var ticker = mustParseTicker(v) - tickers[string(key)] = ticker - }) - - return tickers, nil +func (s *PublicService) Tickers() (TickerMap, error) { + req := s.client.NewGetTickersRequest() + return req.Do(context.Background()) } func (s *PublicService) Ticker(market string) (*Ticker, error) { diff --git a/pkg/exchange/max/maxapi/reward_test.go b/pkg/exchange/max/maxapi/reward_test.go index cb89066a31..e4d0c38e06 100644 --- a/pkg/exchange/max/maxapi/reward_test.go +++ b/pkg/exchange/max/maxapi/reward_test.go @@ -7,6 +7,33 @@ import ( "github.com/stretchr/testify/assert" ) +func TestPublicService(t *testing.T) { + key, secret, ok := integrationTestConfigured(t, "MAX") + if !ok { + t.SkipNow() + } + + ctx := context.Background() + client := NewRestClient(ProductionAPIURL) + client.Auth(key, secret) + + t.Run("v2/timestamp", func(t *testing.T) { + req := client.NewGetTimestampRequest() + serverTimestamp, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotZero(t, serverTimestamp) + }) + + t.Run("v2/tickers", func(t *testing.T) { + req := client.NewGetTickersRequest() + tickers, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, tickers) + assert.NotEmpty(t, tickers) + assert.NotEmpty(t, tickers["btcusdt"]) + }) +} + func TestRewardService_GetRewardsRequest(t *testing.T) { key, secret, ok := integrationTestConfigured(t, "MAX") if !ok { From f7d3fca1ec753b9d294ed5bf5d12fa0afba67312 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 14:56:33 +0800 Subject: [PATCH 0771/1392] maxapi: simplify ticker response parsing --- go.sum | 1 + pkg/exchange/max/exchange.go | 32 ++-- pkg/exchange/max/maxapi/get_ticker_request.go | 20 +++ .../maxapi/get_ticker_request_requestgen.go | 154 ++++++++++++++++++ pkg/exchange/max/maxapi/public.go | 56 ++----- pkg/exchange/max/maxapi/reward_test.go | 9 + 6 files changed, 212 insertions(+), 60 deletions(-) create mode 100644 pkg/exchange/max/maxapi/get_ticker_request.go create mode 100644 pkg/exchange/max/maxapi/get_ticker_request_requestgen.go diff --git a/go.sum b/go.sum index 1655f216e6..64dda1d23b 100644 --- a/go.sum +++ b/go.sum @@ -935,6 +935,7 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 28573c5898..5e53089c9d 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -72,13 +72,13 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke return &types.Ticker{ Time: ticker.Time, - Volume: fixedpoint.MustNewFromString(ticker.Volume), - Last: fixedpoint.MustNewFromString(ticker.Last), - Open: fixedpoint.MustNewFromString(ticker.Open), - High: fixedpoint.MustNewFromString(ticker.High), - Low: fixedpoint.MustNewFromString(ticker.Low), - Buy: fixedpoint.MustNewFromString(ticker.Buy), - Sell: fixedpoint.MustNewFromString(ticker.Sell), + Volume: ticker.Volume, + Last: ticker.Last, + Open: ticker.Open, + High: ticker.High, + Low: ticker.Low, + Buy: ticker.Buy, + Sell: ticker.Sell, }, nil } @@ -112,15 +112,16 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri if _, ok := m[toGlobalSymbol(k)]; len(symbol) != 0 && !ok { continue } + tickers[toGlobalSymbol(k)] = types.Ticker{ Time: v.Time, - Volume: fixedpoint.MustNewFromString(v.Volume), - Last: fixedpoint.MustNewFromString(v.Last), - Open: fixedpoint.MustNewFromString(v.Open), - High: fixedpoint.MustNewFromString(v.High), - Low: fixedpoint.MustNewFromString(v.Low), - Buy: fixedpoint.MustNewFromString(v.Buy), - Sell: fixedpoint.MustNewFromString(v.Sell), + Volume: v.Volume, + Last: v.Last, + Open: v.Open, + High: v.High, + Low: v.Low, + Buy: v.Buy, + Sell: v.Sell, } } } @@ -959,8 +960,7 @@ func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (fixedp return fixedpoint.Zero, err } - return fixedpoint.MustNewFromString(ticker.Sell). - Add(fixedpoint.MustNewFromString(ticker.Buy)).Div(Two), nil + return ticker.Sell.Add(ticker.Buy).Div(Two), nil } func (e *Exchange) RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { diff --git a/pkg/exchange/max/maxapi/get_ticker_request.go b/pkg/exchange/max/maxapi/get_ticker_request.go new file mode 100644 index 0000000000..7650e20022 --- /dev/null +++ b/pkg/exchange/max/maxapi/get_ticker_request.go @@ -0,0 +1,20 @@ +package max + +import ( + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +//go:generate GetRequest -url "/api/v2/tickers/:market" -type GetTickerRequest -responseType .Ticker +type GetTickerRequest struct { + client requestgen.APIClient + + market *string `param:"market,slug"` +} + +func (c *RestClient) NewGetTickerRequest() *GetTickerRequest { + return &GetTickerRequest{client: c} +} diff --git a/pkg/exchange/max/maxapi/get_ticker_request_requestgen.go b/pkg/exchange/max/maxapi/get_ticker_request_requestgen.go new file mode 100644 index 0000000000..0f8329d728 --- /dev/null +++ b/pkg/exchange/max/maxapi/get_ticker_request_requestgen.go @@ -0,0 +1,154 @@ +// Code generated by "requestgen -method GET -url /api/v2/tickers/:market -type GetTickerRequest -responseType .Ticker"; DO NOT EDIT. + +package max + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetTickerRequest) Market(market string) *GetTickerRequest { + g.market = &market + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetTickerRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetTickerRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetTickerRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetTickerRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetTickerRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check market field -> json key market + if g.market != nil { + market := *g.market + + // assign parameter of market + params["market"] = market + + } + + return params, nil +} + +func (g *GetTickerRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetTickerRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetTickerRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetTickerRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetTickerRequest) Do(ctx context.Context) (*Ticker, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/v2/tickers/:market" + slugs, err := g.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = g.applySlugsToUrl(apiURL, slugs) + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse Ticker + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/public.go b/pkg/exchange/max/maxapi/public.go index b71fe97794..ffa14ce8d2 100644 --- a/pkg/exchange/max/maxapi/public.go +++ b/pkg/exchange/max/maxapi/public.go @@ -3,7 +3,6 @@ package max import ( "context" "fmt" - "net/url" "strings" "time" @@ -34,15 +33,15 @@ type Market struct { type Ticker struct { Time time.Time - At int64 `json:"at"` - Buy string `json:"buy"` - Sell string `json:"sell"` - Open string `json:"open"` - High string `json:"high"` - Low string `json:"low"` - Last string `json:"last"` - Volume string `json:"vol"` - VolumeInBTC string `json:"vol_in_btc"` + At int64 `json:"at"` + Buy fixedpoint.Value `json:"buy"` + Sell fixedpoint.Value `json:"sell"` + Open fixedpoint.Value `json:"open"` + High fixedpoint.Value `json:"high"` + Low fixedpoint.Value `json:"low"` + Last fixedpoint.Value `json:"last"` + Volume fixedpoint.Value `json:"vol"` + VolumeInBTC fixedpoint.Value `json:"vol_in_btc"` } func (s *PublicService) Timestamp() (int64, error) { @@ -71,40 +70,9 @@ func (s *PublicService) Tickers() (TickerMap, error) { } func (s *PublicService) Ticker(market string) (*Ticker, error) { - var endPoint = "v2/tickers/" + market - req, err := s.client.NewRequest(context.Background(), "GET", endPoint, url.Values{}, nil) - if err != nil { - return nil, err - } - - response, err := s.client.SendRequest(req) - if err != nil { - return nil, err - } - - v, err := fastjson.ParseBytes(response.Body) - if err != nil { - return nil, err - } - - var ticker = mustParseTicker(v) - return &ticker, nil -} - -func mustParseTicker(v *fastjson.Value) Ticker { - var at = v.GetInt64("at") - return Ticker{ - Time: time.Unix(at, 0), - At: at, - Buy: string(v.GetStringBytes("buy")), - Sell: string(v.GetStringBytes("sell")), - Volume: string(v.GetStringBytes("vol")), - VolumeInBTC: string(v.GetStringBytes("vol_in_btc")), - Last: string(v.GetStringBytes("last")), - Open: string(v.GetStringBytes("open")), - High: string(v.GetStringBytes("high")), - Low: string(v.GetStringBytes("low")), - } + req := s.client.NewGetTickerRequest() + req.Market(market) + return req.Do(context.Background()) } type Interval int64 diff --git a/pkg/exchange/max/maxapi/reward_test.go b/pkg/exchange/max/maxapi/reward_test.go index e4d0c38e06..7bae066050 100644 --- a/pkg/exchange/max/maxapi/reward_test.go +++ b/pkg/exchange/max/maxapi/reward_test.go @@ -32,6 +32,15 @@ func TestPublicService(t *testing.T) { assert.NotEmpty(t, tickers) assert.NotEmpty(t, tickers["btcusdt"]) }) + + t.Run("v2/ticker/:market", func(t *testing.T) { + req := client.NewGetTickerRequest() + req.Market("btcusdt") + ticker, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, ticker) + assert.NotEmpty(t, ticker.Sell) + }) } func TestRewardService_GetRewardsRequest(t *testing.T) { From 13d28edebb72a103359737e3ae77fd64ccec6b94 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 15:01:18 +0800 Subject: [PATCH 0772/1392] maxapi: remove unused parseKLines function --- pkg/exchange/max/maxapi/public.go | 51 ------------------------------- 1 file changed, 51 deletions(-) diff --git a/pkg/exchange/max/maxapi/public.go b/pkg/exchange/max/maxapi/public.go index ffa14ce8d2..1326410274 100644 --- a/pkg/exchange/max/maxapi/public.go +++ b/pkg/exchange/max/maxapi/public.go @@ -6,9 +6,6 @@ import ( "strings" "time" - "github.com/pkg/errors" - "github.com/valyala/fastjson" - "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -190,51 +187,3 @@ func (s *PublicService) KLines(symbol string, resolution string, start time.Time return kLines, nil // return parseKLines(resp.Body, symbol, resolution, interval) } - -func parseKLines(payload []byte, symbol, resolution string, interval Interval) (klines []KLine, err error) { - var parser fastjson.Parser - - v, err := parser.ParseBytes(payload) - if err != nil { - return nil, errors.Wrapf(err, "failed to parse payload: %s", payload) - } - - arr, err := v.Array() - if err != nil { - return nil, errors.Wrapf(err, "failed to get array: %s", payload) - } - - for _, x := range arr { - slice, err := x.Array() - if err != nil { - return nil, errors.Wrapf(err, "failed to get array: %s", payload) - } - - if len(slice) < 6 { - return nil, fmt.Errorf("unexpected length of ohlc elements: %s", payload) - } - - ts, err := slice[0].Int64() - if err != nil { - return nil, fmt.Errorf("failed to parse timestamp: %s", payload) - } - - startTime := time.Unix(ts, 0) - endTime := time.Unix(ts, 0).Add(time.Duration(interval)*time.Minute - time.Millisecond) - isClosed := time.Now().Before(endTime) - klines = append(klines, KLine{ - Symbol: symbol, - Interval: resolution, - StartTime: startTime, - EndTime: endTime, - Open: fixedpoint.NewFromFloat(slice[1].GetFloat64()), - High: fixedpoint.NewFromFloat(slice[2].GetFloat64()), - Low: fixedpoint.NewFromFloat(slice[3].GetFloat64()), - Close: fixedpoint.NewFromFloat(slice[4].GetFloat64()), - Volume: fixedpoint.NewFromFloat(slice[5].GetFloat64()), - Closed: isClosed, - }) - } - - return klines, nil -} From 03d24e6947064b6f639831ead1d87e1b1a9cfe86 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 15:02:14 +0800 Subject: [PATCH 0773/1392] maxapi: move test files --- pkg/exchange/max/maxapi/public_test.go | 44 ++++++++++++++++++++++++++ pkg/exchange/max/maxapi/reward_test.go | 36 --------------------- 2 files changed, 44 insertions(+), 36 deletions(-) create mode 100644 pkg/exchange/max/maxapi/public_test.go diff --git a/pkg/exchange/max/maxapi/public_test.go b/pkg/exchange/max/maxapi/public_test.go new file mode 100644 index 0000000000..bc230c266f --- /dev/null +++ b/pkg/exchange/max/maxapi/public_test.go @@ -0,0 +1,44 @@ +package max + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPublicService(t *testing.T) { + key, secret, ok := integrationTestConfigured(t, "MAX") + if !ok { + t.SkipNow() + } + + ctx := context.Background() + client := NewRestClient(ProductionAPIURL) + client.Auth(key, secret) + + t.Run("v2/timestamp", func(t *testing.T) { + req := client.NewGetTimestampRequest() + serverTimestamp, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotZero(t, serverTimestamp) + }) + + t.Run("v2/tickers", func(t *testing.T) { + req := client.NewGetTickersRequest() + tickers, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, tickers) + assert.NotEmpty(t, tickers) + assert.NotEmpty(t, tickers["btcusdt"]) + }) + + t.Run("v2/ticker/:market", func(t *testing.T) { + req := client.NewGetTickerRequest() + req.Market("btcusdt") + ticker, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, ticker) + assert.NotEmpty(t, ticker.Sell) + }) +} diff --git a/pkg/exchange/max/maxapi/reward_test.go b/pkg/exchange/max/maxapi/reward_test.go index 7bae066050..cb89066a31 100644 --- a/pkg/exchange/max/maxapi/reward_test.go +++ b/pkg/exchange/max/maxapi/reward_test.go @@ -7,42 +7,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestPublicService(t *testing.T) { - key, secret, ok := integrationTestConfigured(t, "MAX") - if !ok { - t.SkipNow() - } - - ctx := context.Background() - client := NewRestClient(ProductionAPIURL) - client.Auth(key, secret) - - t.Run("v2/timestamp", func(t *testing.T) { - req := client.NewGetTimestampRequest() - serverTimestamp, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotZero(t, serverTimestamp) - }) - - t.Run("v2/tickers", func(t *testing.T) { - req := client.NewGetTickersRequest() - tickers, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, tickers) - assert.NotEmpty(t, tickers) - assert.NotEmpty(t, tickers["btcusdt"]) - }) - - t.Run("v2/ticker/:market", func(t *testing.T) { - req := client.NewGetTickerRequest() - req.Market("btcusdt") - ticker, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, ticker) - assert.NotEmpty(t, ticker.Sell) - }) -} - func TestRewardService_GetRewardsRequest(t *testing.T) { key, secret, ok := integrationTestConfigured(t, "MAX") if !ok { From 24f37e273d4eb969d67006b5879fe2915810c7b5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 16:24:07 +0800 Subject: [PATCH 0774/1392] go: mod tidy --- go.mod | 1 - go.sum | 19 +++---------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 1db141fd26..d24c248dc0 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b github.com/cenkalti/backoff/v4 v4.2.0 github.com/cheggaaa/pb/v3 v3.0.8 - github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 github.com/ethereum/go-ethereum v1.10.23 github.com/evanphx/json-patch/v5 v5.6.0 diff --git a/go.sum b/go.sum index 64dda1d23b..a6f2dc205d 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,6 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= -github.com/adshao/go-binance/v2 v2.3.10 h1:iWtHD/sQ8GK6r+cSMMdOynpGI/4Q6P5LZtiEHdWOjag= -github.com/adshao/go-binance/v2 v2.3.10/go.mod h1:Z3MCnWI0gHC4Rea8TWiF3aN1t4nV9z3CaU/TeHcKsLM= github.com/adshao/go-binance/v2 v2.4.1 h1:fOZ2tCbN7sgDZvvsawUMjhsOoe40X87JVE4DklIyyyc= github.com/adshao/go-binance/v2 v2.4.1/go.mod h1:6Qoh+CYcj8U43h4HgT6mqJnsGj4mWZKA/nsj8LN8ZTU= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -76,7 +74,6 @@ github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkN github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= @@ -84,8 +81,6 @@ github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFA github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/c-bata/goptuna v0.8.1 h1:25+n1MLv0yvCsD56xv4nqIus3oLHL9GuPAZDLIqmX1U= github.com/c-bata/goptuna v0.8.1/go.mod h1:knmS8+Iyq5PPy1YUeIEq0pMFR4Y6x7z/CySc9HlZTCY= -github.com/c9s/requestgen v1.3.0 h1:3cTHvWIlrc37nGEdJLIO07XaVidDeOwcew06csBz++U= -github.com/c9s/requestgen v1.3.0/go.mod h1:5n9FU3hr5307IiXAmbMiZbHYaPiys1u9jCWYexZr9qA= github.com/c9s/requestgen v1.3.4 h1:kK2rIO3OAt9JoY5gT0OSkSpq0dy/+JeuI22FwSKpUrY= github.com/c9s/requestgen v1.3.4/go.mod h1:wp4saiPdh0zLF5AkopGCqPQfy9Q5xvRh+TQBOA1l1r4= github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b h1:wT8c03PHLv7+nZUIGqxAzRvIfYHNxMCNVWwvdGkOXTs= @@ -107,7 +102,6 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a h1:ym8P2+ZvUvVtpLzy8wFLLvdggUIU31mvldvxixQQI2o= github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -429,7 +423,6 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= @@ -835,8 +828,7 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -854,8 +846,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -918,13 +910,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -933,7 +923,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -994,8 +983,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= -golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 9dab2470ef3283e667c19ff977ba2d3359c01135 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 16:27:45 +0800 Subject: [PATCH 0775/1392] maxapi: add TestWithdrawal --- pkg/exchange/max/maxapi/public_test.go | 3 ++- pkg/exchange/max/maxapi/withdrawal_test.go | 27 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 pkg/exchange/max/maxapi/withdrawal_test.go diff --git a/pkg/exchange/max/maxapi/public_test.go b/pkg/exchange/max/maxapi/public_test.go index bc230c266f..c0e353117e 100644 --- a/pkg/exchange/max/maxapi/public_test.go +++ b/pkg/exchange/max/maxapi/public_test.go @@ -15,7 +15,8 @@ func TestPublicService(t *testing.T) { ctx := context.Background() client := NewRestClient(ProductionAPIURL) - client.Auth(key, secret) + _ = key + _ = secret t.Run("v2/timestamp", func(t *testing.T) { req := client.NewGetTimestampRequest() diff --git a/pkg/exchange/max/maxapi/withdrawal_test.go b/pkg/exchange/max/maxapi/withdrawal_test.go new file mode 100644 index 0000000000..7eb3601e35 --- /dev/null +++ b/pkg/exchange/max/maxapi/withdrawal_test.go @@ -0,0 +1,27 @@ +package max + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWithdrawal(t *testing.T) { + key, secret, ok := integrationTestConfigured(t, "MAX") + if !ok { + t.SkipNow() + } + + ctx := context.Background() + client := NewRestClient(ProductionAPIURL) + client.Auth(key, secret) + + t.Run("v2/withdrawals", func(t *testing.T) { + req := client.NewGetWithdrawalHistoryRequest() + req.Currency("usdt") + withdrawals, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, withdrawals) + }) +} From a84a22bc2db5fced74ccc92ad8d72886ee59e95e Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 16:32:56 +0800 Subject: [PATCH 0776/1392] maxapi: refactor reward tests --- pkg/exchange/max/maxapi/reward_test.go | 53 +++++++++++--------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/pkg/exchange/max/maxapi/reward_test.go b/pkg/exchange/max/maxapi/reward_test.go index cb89066a31..a60eecf4ca 100644 --- a/pkg/exchange/max/maxapi/reward_test.go +++ b/pkg/exchange/max/maxapi/reward_test.go @@ -18,35 +18,26 @@ func TestRewardService_GetRewardsRequest(t *testing.T) { client := NewRestClient(ProductionAPIURL) client.Auth(key, secret) - req := client.RewardService.NewGetRewardsRequest() - rewards, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, rewards) - assert.NotEmpty(t, rewards) - - t.Logf("rewards: %+v", rewards) -} - -func TestRewardService_GetRewardsOfTypeRequest(t *testing.T) { - key, secret, ok := integrationTestConfigured(t, "MAX") - if !ok { - t.SkipNow() - } - - ctx := context.Background() - - client := NewRestClient(ProductionAPIURL) - client.Auth(key, secret) - - req := client.RewardService.NewGetRewardsOfTypeRequest(RewardCommission) - rewards, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, rewards) - assert.NotEmpty(t, rewards) - - t.Logf("rewards: %+v", rewards) - - for _, reward := range rewards { - assert.Equal(t, RewardCommission, reward.Type) - } + t.Run("v2/rewards", func(t *testing.T) { + req := client.RewardService.NewGetRewardsRequest() + rewards, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, rewards) + assert.NotEmpty(t, rewards) + t.Logf("rewards: %+v", rewards) + }) + + t.Run("v2/rewards with type", func(t *testing.T) { + req := client.RewardService.NewGetRewardsOfTypeRequest(RewardCommission) + rewards, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, rewards) + assert.NotEmpty(t, rewards) + + t.Logf("rewards: %+v", rewards) + + for _, reward := range rewards { + assert.Equal(t, RewardCommission, reward.Type) + } + }) } From 3e41c1fb1559a8a0ace7ddc2071406e3d8a5e281 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 22:29:14 +0800 Subject: [PATCH 0777/1392] maxapi: add max v2 markets api test --- pkg/exchange/max/maxapi/public.go | 7 +------ pkg/exchange/max/maxapi/public_test.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/max/maxapi/public.go b/pkg/exchange/max/maxapi/public.go index 1326410274..11e5b3a678 100644 --- a/pkg/exchange/max/maxapi/public.go +++ b/pkg/exchange/max/maxapi/public.go @@ -53,12 +53,7 @@ func (s *PublicService) Timestamp() (int64, error) { func (s *PublicService) Markets() ([]Market, error) { req := s.client.NewGetMarketsRequest() - markets, err := req.Do(context.Background()) - if err != nil { - return nil, err - } - - return markets, nil + return req.Do(context.Background()) } func (s *PublicService) Tickers() (TickerMap, error) { diff --git a/pkg/exchange/max/maxapi/public_test.go b/pkg/exchange/max/maxapi/public_test.go index c0e353117e..0eff786607 100644 --- a/pkg/exchange/max/maxapi/public_test.go +++ b/pkg/exchange/max/maxapi/public_test.go @@ -25,6 +25,16 @@ func TestPublicService(t *testing.T) { assert.NotZero(t, serverTimestamp) }) + t.Run("v2/markets", func(t *testing.T) { + req := client.NewGetMarketsRequest() + markets, err := req.Do(context.Background()) + assert.NoError(t, err) + if assert.NotEmpty(t, markets) { + assert.NotZero(t, markets[0].MinBaseAmount) + assert.NotZero(t, markets[0].MinQuoteAmount) + } + }) + t.Run("v2/tickers", func(t *testing.T) { req := client.NewGetTickersRequest() tickers, err := req.Do(ctx) From cbbe6e286dd191711228bfc725e1d416d6cae45c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 22:43:32 +0800 Subject: [PATCH 0778/1392] maxapi: add kline api test --- pkg/exchange/max/maxapi/get_k_lines_request.go | 4 ++-- pkg/exchange/max/maxapi/public.go | 2 +- pkg/exchange/max/maxapi/public_test.go | 10 ++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/max/maxapi/get_k_lines_request.go b/pkg/exchange/max/maxapi/get_k_lines_request.go index 710b6d8730..fe28d10d3d 100644 --- a/pkg/exchange/max/maxapi/get_k_lines_request.go +++ b/pkg/exchange/max/maxapi/get_k_lines_request.go @@ -22,6 +22,6 @@ type GetKLinesRequest struct { timestamp time.Time `param:"timestamp,seconds"` } -func (s *PublicService) NewGetKLinesRequest() *GetKLinesRequest { - return &GetKLinesRequest{client: s.client} +func (c *RestClient) NewGetKLinesRequest() *GetKLinesRequest { + return &GetKLinesRequest{client: c} } diff --git a/pkg/exchange/max/maxapi/public.go b/pkg/exchange/max/maxapi/public.go index 11e5b3a678..22d5e695dc 100644 --- a/pkg/exchange/max/maxapi/public.go +++ b/pkg/exchange/max/maxapi/public.go @@ -153,7 +153,7 @@ func (s *PublicService) KLines(symbol string, resolution string, start time.Time return nil, err } - req := s.NewGetKLinesRequest() + req := s.client.NewGetKLinesRequest() req.Market(symbol).Period(int(interval)).Timestamp(start).Limit(limit) data, err := req.Do(context.Background()) if err != nil { diff --git a/pkg/exchange/max/maxapi/public_test.go b/pkg/exchange/max/maxapi/public_test.go index 0eff786607..592ba25a63 100644 --- a/pkg/exchange/max/maxapi/public_test.go +++ b/pkg/exchange/max/maxapi/public_test.go @@ -35,6 +35,16 @@ func TestPublicService(t *testing.T) { } }) + t.Run("v2/k", func(t *testing.T) { + req := client.NewGetKLinesRequest() + data, err := req.Market("btcusdt").Period(int(60)).Limit(100).Do(ctx) + assert.NoError(t, err) + if assert.NotEmpty(t, data) { + assert.NotEmpty(t, data[0]) + assert.Len(t, data[0], 6) + } + }) + t.Run("v2/tickers", func(t *testing.T) { req := client.NewGetTickersRequest() tickers, err := req.Do(ctx) From 7c9109aeea50020eb481e7b3b79d6fbca4a777e2 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 22:56:23 +0800 Subject: [PATCH 0779/1392] max: move more rate limiter to the exchange instance --- pkg/exchange/max/exchange.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 69e3cb5f90..abea90ecfe 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -20,11 +20,6 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// closedOrderQueryLimiter is used for the closed orders query rate limit, 1 request per second -var closedOrderQueryLimiter = rate.NewLimiter(rate.Every(1*time.Second), 1) -var accountQueryLimiter = rate.NewLimiter(rate.Every(3*time.Second), 1) -var marketDataLimiter = rate.NewLimiter(rate.Every(2*time.Second), 10) - var log = logrus.WithField("exchange", "max") type Exchange struct { @@ -36,7 +31,7 @@ type Exchange struct { v3order *v3.OrderService v3margin *v3.MarginService - submitOrderLimiter, queryTradeLimiter *rate.Limiter + submitOrderLimiter, queryTradeLimiter, accountQueryLimiter, closedOrderQueryLimiter, marketDataLimiter *rate.Limiter } func New(key, secret string) *Exchange { @@ -57,6 +52,11 @@ func New(key, secret string) *Exchange { queryTradeLimiter: rate.NewLimiter(rate.Every(1*time.Second), 2), submitOrderLimiter: rate.NewLimiter(rate.Every(100*time.Millisecond), 10), + + // closedOrderQueryLimiter is used for the closed orders query rate limit, 1 request per second + closedOrderQueryLimiter: rate.NewLimiter(rate.Every(1*time.Second), 1), + accountQueryLimiter: rate.NewLimiter(rate.Every(3*time.Second), 1), + marketDataLimiter: rate.NewLimiter(rate.Every(2*time.Second), 10), } } @@ -83,7 +83,7 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke } func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { - if err := marketDataLimiter.Wait(ctx); err != nil { + if err := e.marketDataLimiter.Wait(ctx); err != nil { return nil, err } @@ -258,7 +258,7 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, } func (e *Exchange) queryClosedOrdersByLastOrderID(ctx context.Context, symbol string, lastOrderID uint64) (orders []types.Order, err error) { - if err := closedOrderQueryLimiter.Wait(ctx); err != nil { + if err := e.closedOrderQueryLimiter.Wait(ctx); err != nil { return orders, err } @@ -547,7 +547,7 @@ func (e *Exchange) getLaunchDate() (time.Time, error) { } func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { - if err := accountQueryLimiter.Wait(ctx); err != nil { + if err := e.accountQueryLimiter.Wait(ctx); err != nil { return nil, err } @@ -590,7 +590,7 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { } func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { - if err := accountQueryLimiter.Wait(ctx); err != nil { + if err := e.accountQueryLimiter.Wait(ctx); err != nil { return nil, err } @@ -912,7 +912,7 @@ func (e *Exchange) QueryRewards(ctx context.Context, startTime time.Time) ([]typ // The above query will return a kline that starts with 1620202440 (unix timestamp) without endTime. // We need to calculate the endTime by ourself. func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - if err := marketDataLimiter.Wait(ctx); err != nil { + if err := e.marketDataLimiter.Wait(ctx); err != nil { return nil, err } From 19621e48feab367fe825bab04d05625addf74628 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Apr 2023 22:58:10 +0800 Subject: [PATCH 0780/1392] max: adjust account query rate limiter --- pkg/exchange/max/exchange.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index abea90ecfe..2afcfd3e38 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -55,7 +55,7 @@ func New(key, secret string) *Exchange { // closedOrderQueryLimiter is used for the closed orders query rate limit, 1 request per second closedOrderQueryLimiter: rate.NewLimiter(rate.Every(1*time.Second), 1), - accountQueryLimiter: rate.NewLimiter(rate.Every(3*time.Second), 1), + accountQueryLimiter: rate.NewLimiter(rate.Every(1*time.Second), 1), marketDataLimiter: rate.NewLimiter(rate.Every(2*time.Second), 10), } } From fed5d5f0b8454497dd894d9fa5621e3896b996f9 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 13 Apr 2023 16:18:11 +0800 Subject: [PATCH 0781/1392] maxapi: add more market info assertion --- pkg/exchange/max/maxapi/public_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/exchange/max/maxapi/public_test.go b/pkg/exchange/max/maxapi/public_test.go index 592ba25a63..cf2c20803e 100644 --- a/pkg/exchange/max/maxapi/public_test.go +++ b/pkg/exchange/max/maxapi/public_test.go @@ -32,6 +32,11 @@ func TestPublicService(t *testing.T) { if assert.NotEmpty(t, markets) { assert.NotZero(t, markets[0].MinBaseAmount) assert.NotZero(t, markets[0].MinQuoteAmount) + assert.NotEmpty(t, markets[0].Name) + assert.NotEmpty(t, markets[0].ID) + assert.NotEmpty(t, markets[0].BaseUnit) + assert.NotEmpty(t, markets[0].QuoteUnit) + t.Logf("%+v", markets[0]) } }) From 8c02b5e64ecbccfccacc432ab414452cbf8a67c9 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 13 Apr 2023 16:40:07 +0800 Subject: [PATCH 0782/1392] maxapi: pass context object to the requests --- pkg/exchange/max/exchange.go | 9 ++++----- pkg/exchange/max/maxapi/public.go | 12 ++++++------ pkg/exchange/max/maxapi/restapi.go | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 5e53089c9d..98cf73936e 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -96,8 +96,8 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri tickers[toGlobalSymbol(symbol[0])] = *ticker } else { - - maxTickers, err := e.client.PublicService.Tickers() + req := e.client.NewGetTickersRequest() + maxTickers, err := req.Do(ctx) if err != nil { return nil, err } @@ -130,9 +130,8 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri } func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { - log.Info("querying market info...") - - remoteMarkets, err := e.client.PublicService.Markets() + req := e.client.NewGetMarketsRequest() + remoteMarkets, err := req.Do(ctx) if err != nil { return nil, err } diff --git a/pkg/exchange/max/maxapi/public.go b/pkg/exchange/max/maxapi/public.go index 22d5e695dc..e4d3aac823 100644 --- a/pkg/exchange/max/maxapi/public.go +++ b/pkg/exchange/max/maxapi/public.go @@ -41,9 +41,9 @@ type Ticker struct { VolumeInBTC fixedpoint.Value `json:"vol_in_btc"` } -func (s *PublicService) Timestamp() (int64, error) { +func (s *PublicService) Timestamp(ctx context.Context) (int64, error) { req := s.client.NewGetTimestampRequest() - ts, err := req.Do(context.Background()) + ts, err := req.Do(ctx) if err != nil || ts == nil { return 0, nil } @@ -51,14 +51,14 @@ func (s *PublicService) Timestamp() (int64, error) { return int64(*ts), nil } -func (s *PublicService) Markets() ([]Market, error) { +func (s *PublicService) Markets(ctx context.Context) ([]Market, error) { req := s.client.NewGetMarketsRequest() - return req.Do(context.Background()) + return req.Do(ctx) } -func (s *PublicService) Tickers() (TickerMap, error) { +func (s *PublicService) Tickers(ctx context.Context) (TickerMap, error) { req := s.client.NewGetTickersRequest() - return req.Do(context.Background()) + return req.Do(ctx) } func (s *PublicService) Ticker(market string) (*Ticker, error) { diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index 7dc554a9b6..3b45af1dfe 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -161,7 +161,7 @@ func (c *RestClient) queryAndUpdateServerTimestamp(ctx context.Context) { default: op := func() error { - serverTs, err := c.PublicService.Timestamp() + serverTs, err := c.PublicService.Timestamp(ctx) if err != nil { return err } From 25daefabab4f0e19787c2d30cae86a676a748239 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 13 Apr 2023 17:20:59 +0800 Subject: [PATCH 0783/1392] maxapi: fix nonce updater --- pkg/bbgo/session.go | 10 ++++++++-- pkg/exchange/max/maxapi/restapi.go | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 13e500dbda..7aa3ba8b15 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -2,6 +2,7 @@ package bbgo import ( "context" + "errors" "fmt" "strings" "sync" @@ -25,6 +26,8 @@ import ( var KLinePreloadLimit int64 = 1000 +var ErrEmptyMarketInfo = errors.New("market info should not be empty, 0 markets loaded") + // ExchangeSession presents the exchange connection Session // It also maintains and collects the data returned from the stream. type ExchangeSession struct { @@ -177,6 +180,7 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) var log = log.WithField("session", session.Name) // load markets first + log.Infof("querying market info from %s...", session.Name) var disableMarketsCache = false var markets types.MarketMap @@ -191,7 +195,7 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) } if len(markets) == 0 { - return fmt.Errorf("market config should not be empty") + return ErrEmptyMarketInfo } session.markets = markets @@ -224,6 +228,8 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) // query and initialize the balances if !session.PublicOnly { + log.Infof("querying account balances...") + account, err := session.Exchange.QueryAccount(ctx) if err != nil { return err @@ -233,7 +239,7 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) session.Account = account session.accountMutex.Unlock() - log.Infof("%s account", session.Name) + log.Infof("account %s balances:", session.Name) account.Balances().Print() // forward trade updates and order updates to the order executor diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index 3b45af1dfe..97345aea29 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -165,6 +165,7 @@ func (c *RestClient) queryAndUpdateServerTimestamp(ctx context.Context) { if err != nil { return err } + if serverTs == 0 { return errors.New("unexpected zero server timestamp") } @@ -191,6 +192,8 @@ func (c *RestClient) queryAndUpdateServerTimestamp(ctx context.Context) { if err := backoff.RetryGeneral(ctx, op); err != nil { logger.WithError(err).Error("unable to sync timestamp with max") + } else { + return } } } From 92b8652f78cd0e6b70503513e5156ab423cbb014 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 13 Apr 2023 17:29:23 +0800 Subject: [PATCH 0784/1392] maxapi: remove duplicated for loop --- pkg/exchange/max/maxapi/restapi.go | 66 +++++++++++++----------------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index 97345aea29..9f3eb7b28d 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -154,48 +154,38 @@ func (c *RestClient) Auth(key string, secret string) *RestClient { } func (c *RestClient) queryAndUpdateServerTimestamp(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - - default: - op := func() error { - serverTs, err := c.PublicService.Timestamp(ctx) - if err != nil { - return err - } - - if serverTs == 0 { - return errors.New("unexpected zero server timestamp") - } - - clientTime := time.Now() - offset := serverTs - clientTime.Unix() - - if offset < 0 { - // avoid updating a negative offset: server time is before the local time - if offset > maxAllowedNegativeTimeOffset { - return nil - } - - // if the offset is greater than 15 seconds, we should restart - logger.Panicf("max exchange server timestamp offset %d is less than the negative offset %d", offset, maxAllowedNegativeTimeOffset) - } - - atomic.StoreInt64(&globalServerTimestamp, serverTs) - atomic.StoreInt64(&globalTimeOffset, offset) - - logger.Debugf("loaded max server timestamp: %d offset=%d", globalServerTimestamp, offset) + op := func() error { + serverTs, err := c.PublicService.Timestamp(ctx) + if err != nil { + return err + } + + if serverTs == 0 { + return errors.New("unexpected zero server timestamp") + } + + clientTime := time.Now() + offset := serverTs - clientTime.Unix() + + if offset < 0 { + // avoid updating a negative offset: server time is before the local time + if offset > maxAllowedNegativeTimeOffset { return nil } - if err := backoff.RetryGeneral(ctx, op); err != nil { - logger.WithError(err).Error("unable to sync timestamp with max") - } else { - return - } + // if the offset is greater than 15 seconds, we should restart + logger.Panicf("max exchange server timestamp offset %d is less than the negative offset %d", offset, maxAllowedNegativeTimeOffset) } + + atomic.StoreInt64(&globalServerTimestamp, serverTs) + atomic.StoreInt64(&globalTimeOffset, offset) + + logger.Debugf("loaded max server timestamp: %d offset=%d", globalServerTimestamp, offset) + return nil + } + + if err := backoff.RetryGeneral(ctx, op); err != nil { + logger.WithError(err).Error("unable to sync timestamp with max") } } From 4ab54f586b6412301d2c8d016a3d46c7085c58e4 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 14 Apr 2023 15:15:28 +0800 Subject: [PATCH 0785/1392] grid2: emit grid error when open grid failed --- pkg/strategy/grid2/strategy.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 45fca871b8..11cfec1eb3 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -984,7 +984,9 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { - return errors.Wrap(err, "failed to get the last trade price") + err2 := errors.Wrap(err, "unable to get the last trade price") + s.EmitGridError(err2) + return err2 } // check if base and quote are enough @@ -1006,11 +1008,13 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) if s.QuantityOrAmount.IsSet() { if quantity := s.QuantityOrAmount.Quantity; !quantity.IsZero() { if _, _, err2 := s.checkRequiredInvestmentByQuantity(totalBase, totalQuote, lastPrice, s.QuantityOrAmount.Quantity, s.grid.Pins); err != nil { + s.EmitGridError(err2) return err2 } } if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { if _, _, err2 := s.checkRequiredInvestmentByAmount(totalBase, totalQuote, lastPrice, amount, s.grid.Pins); err != nil { + s.EmitGridError(err2) return err2 } } @@ -1019,15 +1023,19 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) if !s.QuoteInvestment.IsZero() && !s.BaseInvestment.IsZero() { quantity, err2 := s.calculateBaseQuoteInvestmentQuantity(s.QuoteInvestment, s.BaseInvestment, lastPrice, s.grid.Pins) if err2 != nil { + s.EmitGridError(err2) return err2 } + s.QuantityOrAmount.Quantity = quantity } else if !s.QuoteInvestment.IsZero() { quantity, err2 := s.calculateQuoteInvestmentQuantity(s.QuoteInvestment, lastPrice, s.grid.Pins) if err2 != nil { + s.EmitGridError(err2) return err2 } + s.QuantityOrAmount.Quantity = quantity } } @@ -1036,10 +1044,14 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) // investment configuration is valid with the current balances if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { if s.BaseInvestment.Compare(totalBase) > 0 { - return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", s.BaseInvestment.Float64(), totalBase.Float64()) + err2 := fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", s.BaseInvestment.Float64(), totalBase.Float64()) + s.EmitGridError(err2) + return err2 } if s.QuoteInvestment.Compare(totalQuote) > 0 { - return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", s.QuoteInvestment.Float64(), totalQuote.Float64()) + err2 := fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", s.QuoteInvestment.Float64(), totalQuote.Float64()) + s.EmitGridError(err2) + return err2 } } @@ -1052,6 +1064,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) } if err != nil { + s.EmitGridError(err) return err } @@ -1061,6 +1074,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) createdOrders, err2 := s.orderExecutor.SubmitOrders(writeCtx, submitOrders...) if err2 != nil { + s.EmitGridError(err2) return err2 } From 3f7e6170048ddf3ac7f68240d61b21afffdeae25 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Fri, 14 Apr 2023 15:23:34 +0800 Subject: [PATCH 0786/1392] FEATURE: add market info in-mem cache --- pkg/cache/cache.go | 101 +++++++++++++++++++++++++++++++++++++-- pkg/cache/cache_test.go | 103 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 pkg/cache/cache_test.go diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 5a5f0ccc26..f659e0f3fb 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -8,17 +8,74 @@ import ( "os" "path" "reflect" + "sync" "time" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/util/backoff" ) -type DataFetcher func() (interface{}, error) +const memCacheExpiry = 5 * time.Minute +const fileCacheExpiry = 24 * time.Hour + +var globalMarketMemCache *marketMemCache = newMarketMemCache() + +type marketMemCache struct { + sync.Mutex + markets map[string]marketMapWithTime +} + +type marketMapWithTime struct { + updatedAt time.Time + markets types.MarketMap +} + +func newMarketMemCache() *marketMemCache { + cache := &marketMemCache{ + markets: make(map[string]marketMapWithTime), + } + return cache +} + +func (c *marketMemCache) IsOutdated(exName string) bool { + c.Lock() + defer c.Unlock() + + data, ok := c.markets[exName] + return !ok || time.Since(data.updatedAt) > memCacheExpiry +} + +func (c *marketMemCache) Set(exName string, markets types.MarketMap) { + c.Lock() + defer c.Unlock() + + c.markets[exName] = marketMapWithTime{ + updatedAt: time.Now(), + markets: markets, + } +} + +func (c *marketMemCache) Get(exName string) (types.MarketMap, bool) { + c.Lock() + defer c.Unlock() + + markets, ok := c.markets[exName] + if !ok { + return nil, false + } -const cacheExpiry = 24 * time.Hour + copied := types.MarketMap{} + for key, val := range markets.markets { + copied[key] = val + } + return copied, true +} + +type DataFetcher func() (interface{}, error) // WithCache let you use the cache with the given cache key, variable reference and your data fetcher, // The key must be an unique ID. @@ -29,7 +86,7 @@ func WithCache(key string, obj interface{}, fetcher DataFetcher) error { cacheFile := path.Join(cacheDir, key+".json") stat, err := os.Stat(cacheFile) - if os.IsNotExist(err) || (stat != nil && time.Since(stat.ModTime()) > cacheExpiry) { + if os.IsNotExist(err) || (stat != nil && time.Since(stat.ModTime()) > fileCacheExpiry) { log.Debugf("cache %s not found or cache expired, executing fetcher callback to get the data", cacheFile) data, err := fetcher() @@ -70,6 +127,42 @@ func WithCache(key string, obj interface{}, fetcher DataFetcher) error { } func LoadExchangeMarketsWithCache(ctx context.Context, ex types.Exchange) (markets types.MarketMap, err error) { + inMem, ok := util.GetEnvVarBool("USE_MARKETS_CACHE_IN_MEMORY") + if ok && inMem { + return loadMarketsFromMem(ctx, ex) + } + + // fallback to use files as cache + return loadMarketsFromFile(ctx, ex) +} + +// loadMarketsFromMem is useful for one process to run multiple bbgos in different go routines. +func loadMarketsFromMem(ctx context.Context, ex types.Exchange) (markets types.MarketMap, _ error) { + exName := ex.Name().String() + if globalMarketMemCache.IsOutdated(exName) { + op := func() error { + rst, err2 := ex.QueryMarkets(ctx) + if err2 != nil { + return err2 + } + + markets = rst + globalMarketMemCache.Set(exName, rst) + return nil + } + + if err := backoff.RetryGeneral(ctx, op); err != nil { + return nil, err + } + + return markets, nil + } + + rst, _ := globalMarketMemCache.Get(exName) + return rst, nil +} + +func loadMarketsFromFile(ctx context.Context, ex types.Exchange) (markets types.MarketMap, err error) { key := fmt.Sprintf("%s-markets", ex.Name()) if futureExchange, implemented := ex.(types.FuturesExchange); implemented { settings := futureExchange.GetFuturesSettings() @@ -82,4 +175,4 @@ func LoadExchangeMarketsWithCache(ctx context.Context, ex types.Exchange) (marke return ex.QueryMarkets(ctx) }) return markets, err -} +} \ No newline at end of file diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go new file mode 100644 index 0000000000..8e9c6db361 --- /dev/null +++ b/pkg/cache/cache_test.go @@ -0,0 +1,103 @@ +package cache + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/mocks" +) + +func Test_newMarketMemCache(t *testing.T) { + cache := newMarketMemCache() + assert.NotNil(t, cache) + assert.NotNil(t, cache.markets) +} + +func Test_marketMemCache_GetSet(t *testing.T) { + cache := newMarketMemCache() + cache.Set("max", types.MarketMap{ + "btctwd": types.Market{ + Symbol: "btctwd", + LocalSymbol: "btctwd", + }, + "ethtwd": types.Market{ + Symbol: "ethtwd", + LocalSymbol: "ethtwd", + }, + }) + markets, ok := cache.Get("max") + assert.True(t, ok) + + btctwd, ok := markets["btctwd"] + assert.True(t, ok) + ethtwd, ok := markets["ethtwd"] + assert.True(t, ok) + assert.Equal(t, types.Market{ + Symbol: "btctwd", + LocalSymbol: "btctwd", + }, btctwd) + assert.Equal(t, types.Market{ + Symbol: "ethtwd", + LocalSymbol: "ethtwd", + }, ethtwd) + + _, ok = cache.Get("binance") + assert.False(t, ok) + + expired := cache.IsOutdated("max") + assert.False(t, expired) + + detailed := cache.markets["max"] + detailed.updatedAt = time.Now().Add(-2 * memCacheExpiry) + cache.markets["max"] = detailed + expired = cache.IsOutdated("max") + assert.True(t, expired) + + expired = cache.IsOutdated("binance") + assert.True(t, expired) +} + +func Test_loadMarketsFromMem(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockEx := mocks.NewMockExchange(mockCtrl) + mockEx.EXPECT().Name().Return(types.ExchangeName("max")).AnyTimes() + mockEx.EXPECT().QueryMarkets(gomock.Any()).Return(nil, errors.New("faked")).Times(1) + mockEx.EXPECT().QueryMarkets(gomock.Any()).Return(types.MarketMap{ + "btctwd": types.Market{ + Symbol: "btctwd", + LocalSymbol: "btctwd", + }, + "ethtwd": types.Market{ + Symbol: "ethtwd", + LocalSymbol: "ethtwd", + }, + }, nil).Times(1) + + for i := 0; i < 10; i++ { + markets, err := loadMarketsFromMem(context.Background(), mockEx) + assert.NoError(t, err) + + btctwd, ok := markets["btctwd"] + assert.True(t, ok) + ethtwd, ok := markets["ethtwd"] + assert.True(t, ok) + assert.Equal(t, types.Market{ + Symbol: "btctwd", + LocalSymbol: "btctwd", + }, btctwd) + assert.Equal(t, types.Market{ + Symbol: "ethtwd", + LocalSymbol: "ethtwd", + }, ethtwd) + } + + globalMarketMemCache = newMarketMemCache() // reset the global cache +} \ No newline at end of file From 1158b9582a6aa04d278b102118fbbc3ff483eeb5 Mon Sep 17 00:00:00 2001 From: chiahung Date: Fri, 14 Apr 2023 16:44:56 +0800 Subject: [PATCH 0787/1392] FEATURE: max get-order v3 api support client order id parameter --- pkg/exchange/max/exchange.go | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index c5eb26b396..a2fac55be0 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -210,16 +210,30 @@ func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([] } func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { - if q.OrderID == "" { - return nil, errors.New("max.QueryOrder: OrderID is required parameter") + if len(q.OrderID) == 0 && len(q.ClientOrderID) == 0 { + return nil, errors.New("max.QueryOrder: one of OrderID/ClientOrderID is required parameter") } - orderID, err := strconv.ParseInt(q.OrderID, 10, 64) - if err != nil { - return nil, err + if len(q.OrderID) != 0 && len(q.ClientOrderID) != 0 { + return nil, errors.New("max.QueryOrder: only accept one parameter of OrderID/ClientOrderID") + } + + request := e.v3order.NewGetOrderRequest() + + if len(q.OrderID) != 0 { + orderID, err := strconv.ParseInt(q.OrderID, 10, 64) + if err != nil { + return nil, err + } + + request.Id(uint64(orderID)) + } + + if len(q.ClientOrderID) != 0 { + request.ClientOrderID(q.ClientOrderID) } - maxOrder, err := e.v3order.NewGetOrderRequest().Id(uint64(orderID)).Do(ctx) + maxOrder, err := request.Do(ctx) if err != nil { return nil, err } From a0aae23bf3882427ee51f8f033b11357dfa01fcc Mon Sep 17 00:00:00 2001 From: chiahung Date: Fri, 14 Apr 2023 18:30:38 +0800 Subject: [PATCH 0788/1392] FIX: fix emit ready twice and add error log --- pkg/strategy/grid2/recover.go | 8 ++------ pkg/strategy/grid2/strategy.go | 1 + 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 33f9d93894..877cd24c21 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -58,13 +58,12 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex return errors.Wrap(err, "grid recover error") } - // emit ready after recover - s.EmitGridReady() - // debug and send metrics // wait for the reverse order to be placed time.Sleep(2 * time.Second) debugGrid(s.logger, s.grid, s.orderExecutor.ActiveMakerOrders()) + // emit ready after recover + s.EmitGridReady() s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) @@ -88,9 +87,6 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi s.debugLog("open orders nums: %d, expected nums: %d", numGridOpenOrders, expectedNumOfOrders) if expectedNumOfOrders == numGridOpenOrders { // no need to recover - s.EmitGridReady() - s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } else if expectedNumOfOrders < numGridOpenOrders { return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 11cfec1eb3..9c9ef43561 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1941,6 +1941,7 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") if err := s.recoverGrid(ctx, session); err != nil { // if recover fail, return and do not open grid + s.logger.WithError(err).Error("failed to start process, recover error") s.EmitGridError(errors.Wrapf(err, "failed to start process, recover error")) return } From a178fd0a8469fd0f7aadd93660c5dcb2e4ef5a17 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 14 Apr 2023 18:57:13 +0800 Subject: [PATCH 0789/1392] max: add max auth authenticated log --- pkg/exchange/max/stream.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/exchange/max/stream.go b/pkg/exchange/max/stream.go index 2a850a047e..89f885d1df 100644 --- a/pkg/exchange/max/stream.go +++ b/pkg/exchange/max/stream.go @@ -51,6 +51,9 @@ func NewStream(key, secret string) *Stream { stream.SetParser(max.ParseMessage) stream.SetDispatcher(stream.dispatchEvent) stream.OnConnect(stream.handleConnect) + stream.OnAuthEvent(func(e max.AuthEvent) { + log.Infof("max websocket connection authenticated: %+v", e) + }) stream.OnKLineEvent(stream.handleKLineEvent) stream.OnOrderSnapshotEvent(stream.handleOrderSnapshotEvent) stream.OnOrderUpdateEvent(stream.handleOrderUpdateEvent) From aa33836fb35c3a8632034b27e3d52b06279a16ee Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 16 Apr 2023 21:26:52 +0800 Subject: [PATCH 0790/1392] interact: fix concurrent map write - add mutex on interact --- pkg/interact/interact.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/interact/interact.go b/pkg/interact/interact.go index c9358b9ac2..6c0c97db31 100644 --- a/pkg/interact/interact.go +++ b/pkg/interact/interact.go @@ -3,6 +3,7 @@ package interact import ( "context" "fmt" + "sync" "time" log "github.com/sirupsen/logrus" @@ -49,6 +50,8 @@ type Interact struct { customInteractions []CustomInteraction messengers []Messenger + + mu sync.Mutex } func New() *Interact { @@ -63,25 +66,36 @@ func New() *Interact { func (it *Interact) AddCustomInteraction(custom CustomInteraction) { custom.Commands(it) + + it.mu.Lock() it.customInteractions = append(it.customInteractions, custom) + it.mu.Unlock() } func (it *Interact) PrivateCommand(command, desc string, f interface{}) *Command { cmd := NewCommand(command, desc, f) + it.mu.Lock() it.privateCommands[command] = cmd + it.mu.Unlock() return cmd } func (it *Interact) Command(command string, desc string, f interface{}) *Command { cmd := NewCommand(command, desc, f) + it.mu.Lock() it.commands[command] = cmd + it.mu.Unlock() return cmd } func (it *Interact) getNextState(session Session, currentState State) (nextState State, final bool) { var ok bool final = false + + it.mu.Lock() nextState, ok = it.states[currentState] + it.mu.Unlock() + if ok { // check if it's the final state if _, hasTransition := it.statesFunc[nextState]; !hasTransition { @@ -128,6 +142,9 @@ func (it *Interact) handleResponse(session Session, text string, ctxObjects ...i } func (it *Interact) getCommand(session Session, command string) (*Command, error) { + it.mu.Lock() + defer it.mu.Unlock() + if session.IsAuthorized() { if cmd, ok := it.privateCommands[command]; ok { return cmd, nil From 47e398abc3a42b7c8f8db7d129f626adcf8150f0 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 17 Apr 2023 16:28:38 +0800 Subject: [PATCH 0791/1392] types: do not return for normal closure --- pkg/types/stream.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/types/stream.go b/pkg/types/stream.go index c1f9a8f766..c53472ac35 100644 --- a/pkg/types/stream.go +++ b/pkg/types/stream.go @@ -203,14 +203,12 @@ func (s *StandardStream) Read(ctx context.Context, conn *websocket.Conn, cancel // if it's a websocket related error case *websocket.CloseError: - if err.Code == websocket.CloseNormalClosure { - return + if err.Code != websocket.CloseNormalClosure { + log.WithError(err).Errorf("websocket error abnormal close: %+v", err) } - log.WithError(err).Errorf("websocket error abnormal close: %+v", err) - _ = conn.Close() - // for unexpected close error, we should re-connect + // for close error, we should re-connect // emit reconnect to start a new connection s.Reconnect() return From c3318cbb50c2cf0fcddebac9e7014a13c69f835f Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 18 Apr 2023 11:31:51 +0800 Subject: [PATCH 0792/1392] exits/trailingstop: update comment --- pkg/bbgo/exit_hh_ll_stop.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index 1b094fbc75..af8ec89bc0 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -54,7 +54,6 @@ type HigherHighLowerLowStop struct { // Subscribe required k-line stream func (s *HigherHighLowerLowStop) Subscribe(session *ExchangeSession) { - // use 1m kline to handle roi stop session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } From d00a91441c518c1e5b534cc499844ce01343d9ed Mon Sep 17 00:00:00 2001 From: chiahung Date: Tue, 18 Apr 2023 15:12:50 +0800 Subject: [PATCH 0793/1392] FEATURE: move metrics to defer funciton --- pkg/strategy/grid2/recover.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 877cd24c21..0e9a43cf27 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -14,6 +14,11 @@ import ( ) func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { + defer func() { + s.updateGridNumOfOrdersMetricsWithLock() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) + }() + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) // if the exchange doesn't support ExchangeTradeHistoryService, do not run recover if !implemented { @@ -64,8 +69,6 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex debugGrid(s.logger, s.grid, s.orderExecutor.ActiveMakerOrders()) // emit ready after recover s.EmitGridReady() - s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } @@ -79,9 +82,6 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi grid := s.newGrid() s.setGrid(grid) - // add open orders to active order book - s.addOrdersToActiveOrderBook(openOrdersOnGrid) - expectedNumOfOrders := s.GridNum - 1 numGridOpenOrders := int64(len(openOrdersOnGrid)) s.debugLog("open orders nums: %d, expected nums: %d", numGridOpenOrders, expectedNumOfOrders) @@ -121,9 +121,11 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi return errors.Wrapf(err, "verify grid with error") } - s.debugOrders("emit filled orders", filledOrders) + // 5. add open orders to active order book + s.addOrdersToActiveOrderBook(openOrdersOnGrid) - // 5. emit the filled orders + // 6. emit the filled orders + s.debugOrders("emit filled orders", filledOrders) activeOrderBook := s.orderExecutor.ActiveMakerOrders() for _, filledOrder := range filledOrders { activeOrderBook.EmitFilled(filledOrder) From ed4e0b03e7ed59b2c3702f0ec9335e09e27279b2 Mon Sep 17 00:00:00 2001 From: chiahung Date: Tue, 18 Apr 2023 15:47:00 +0800 Subject: [PATCH 0794/1392] add open orders back to active order book if no need to recover --- pkg/strategy/grid2/recover.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 0e9a43cf27..f8b26d4a6f 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -86,7 +86,8 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi numGridOpenOrders := int64(len(openOrdersOnGrid)) s.debugLog("open orders nums: %d, expected nums: %d", numGridOpenOrders, expectedNumOfOrders) if expectedNumOfOrders == numGridOpenOrders { - // no need to recover + // no need to recover, only need to add open orders back to active order book + s.addOrdersToActiveOrderBook(openOrdersOnGrid) return nil } else if expectedNumOfOrders < numGridOpenOrders { return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") @@ -121,7 +122,7 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi return errors.Wrapf(err, "verify grid with error") } - // 5. add open orders to active order book + // 5. add open orders to active order book. s.addOrdersToActiveOrderBook(openOrdersOnGrid) // 6. emit the filled orders From dec3176ea5f217a59ee8500a6790c24281fe61a6 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 19 Apr 2023 14:11:02 +0800 Subject: [PATCH 0795/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index 58d1e68ecc..91ebbfd821 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -60,4 +60,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 71137208d7..2374db5e3c 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -43,4 +43,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 45a01c79ed..ca859ad1b7 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -52,4 +52,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index 3d2dbe60ca..4ba2f64a01 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -42,4 +42,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index e7e42df6d5..fc6080b076 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -41,4 +41,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index 0ae96aa8c6..c7c57c33f8 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -51,4 +51,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index 089e1da0df..697f6b84d0 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -43,4 +43,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index e1902b59ca..1361793a4b 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -50,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 7c4b0f7111..a9f7fbcef4 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -44,4 +44,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index 7537ff6603..38d45b14df 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -47,4 +47,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 9ac68d94e1..a6f0911ecf 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -44,4 +44,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index 9dc1635cb9..2be614ea7a 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -43,4 +43,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index c247bfd45b..d217693c58 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -40,4 +40,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index 2df0cf06cc..52a7a9b50e 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -43,4 +43,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 5701486fbc..8b5656c629 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -43,4 +43,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index 3deadf1069..ab62182f45 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -43,4 +43,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index c81a96ae71..90d3321428 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -42,4 +42,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index efb4456ede..ef920d7e27 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -46,4 +46,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index 5b872156c1..da8a3cefde 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -44,4 +44,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index 0329b3f958..523e6f842c 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -42,4 +42,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index ebfe58cf58..ef4579edd4 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -51,4 +51,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index e8f0a3f8de..93a34a0a08 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -53,4 +53,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 5f4192ee6e..1a2f0df0d3 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -48,4 +48,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index 5e7773f28a..0cf5d7bcf5 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -44,4 +44,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index 0fde24263e..c6346d2963 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -44,4 +44,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index d2907b3fe3..5d727619c2 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -42,4 +42,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 0e3f42d3e5..4218f97209 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -44,4 +44,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index a9e4a05f94..b7e90fbed9 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -42,4 +42,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index 95979a7ee0..0125e8a218 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -41,4 +41,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 3-Apr-2023 +###### Auto generated by spf13/cobra on 19-Apr-2023 From 5372fd3f30c5789bdad786a39044dbaa8ae405fd Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 19 Apr 2023 14:11:02 +0800 Subject: [PATCH 0796/1392] bump version to v1.46.0 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 1c7d792778..83b3cfbf76 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.45.0-5b09ad67-dev" +const Version = "v1.46.0-a06ed9e5-dev" -const VersionGitRef = "5b09ad67" +const VersionGitRef = "a06ed9e5" diff --git a/pkg/version/version.go b/pkg/version/version.go index 84730a3c6a..4a2adebc2b 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.45.0-5b09ad67" +const Version = "v1.46.0-a06ed9e5" -const VersionGitRef = "5b09ad67" +const VersionGitRef = "a06ed9e5" From 45a27ffe06a7a3ef5112824569702181b8193c20 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 19 Apr 2023 14:11:02 +0800 Subject: [PATCH 0797/1392] add v1.46.0 release note --- doc/release/v1.46.0.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 doc/release/v1.46.0.md diff --git a/doc/release/v1.46.0.md b/doc/release/v1.46.0.md new file mode 100644 index 0000000000..b5835b0c27 --- /dev/null +++ b/doc/release/v1.46.0.md @@ -0,0 +1,20 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.45.0...main) + + - [#1153](https://github.com/c9s/bbgo/pull/1153): FEATURE: [grid2] move metrics to defer funciton + - [#1121](https://github.com/c9s/bbgo/pull/1121): FEATURE: add hhllstop + - [#1151](https://github.com/c9s/bbgo/pull/1151): FIX: types: do not return for normal closure + - [#1147](https://github.com/c9s/bbgo/pull/1147): FEATURE: max get-order v3 api support client order id parameter + - [#1150](https://github.com/c9s/bbgo/pull/1150): FIX: interact: fix concurrent map write - add mutex on interact + - [#1149](https://github.com/c9s/bbgo/pull/1149): CHORE: max: add max auth authenticated log + - [#1148](https://github.com/c9s/bbgo/pull/1148): FIX: fix emit ready twice and add error log + - [#1146](https://github.com/c9s/bbgo/pull/1146): FIX: grid2: emit grid error when open grid failed + - [#1145](https://github.com/c9s/bbgo/pull/1145): FEATURE: add market info in-mem cache + - [#1144](https://github.com/c9s/bbgo/pull/1144): FIX: maxapi: fix nonce updater + - [#1143](https://github.com/c9s/bbgo/pull/1143): FIX: maxapi: pass context object to the requests + - [#1141](https://github.com/c9s/bbgo/pull/1141): REFACTOR: maxapi: refactor and add max v2 markets api test + - [#1142](https://github.com/c9s/bbgo/pull/1142): FIX: max: move more rate limiter to the exchange instance + - [#1139](https://github.com/c9s/bbgo/pull/1139): REFACTOR: [max] refactor api requests + - [#1140](https://github.com/c9s/bbgo/pull/1140): FIX: maxapi: improve log message + - [#1138](https://github.com/c9s/bbgo/pull/1138): FIX: maxapi: use sync.Once to prevent duplicated update and avoid update negative offset + - [#1137](https://github.com/c9s/bbgo/pull/1137): FIX: maxapi: improve nonce update with retry + - [#1135](https://github.com/c9s/bbgo/pull/1135): FIX: recover even though inital order id is 0 From 4b8adf6ed5bcdafa02122ac270e7c9191348bf6f Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 20 Apr 2023 17:53:20 +0800 Subject: [PATCH 0798/1392] improve/supertrend: adding opposite position amount to the order amount instead of closing opposite position --- pkg/strategy/supertrend/strategy.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index c40a018544..2a2aa2320a 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -621,18 +621,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // The default value of side is an empty string. Unless side is set by the checks above, the result of the following condition is false if side == types.SideTypeSell || side == types.SideTypeBuy { bbgo.Notify("open %s position for signal %v", s.Symbol, side) - // Close opposite position if any - if !s.Position.IsDust(closePrice) { - if (side == types.SideTypeSell && s.Position.IsLong()) || (side == types.SideTypeBuy && s.Position.IsShort()) { - bbgo.Notify("close existing %s position before open a new position", s.Symbol) - _ = s.ClosePosition(ctx, fixedpoint.One) - } else { - bbgo.Notify("existing %s position has the same direction with the signal", s.Symbol) - return - } + + amount := s.calculateQuantity(ctx, closePrice, side) + + // Add opposite position amount if any + if (side == types.SideTypeSell && s.Position.IsLong()) || (side == types.SideTypeBuy && s.Position.IsShort()) { + bbgo.Notify("add existing opposite position amount of %s to the amount of open new position order", s.Symbol) + amount = amount + s.Position.GetQuantity() + } else if !s.Position.IsDust(closePrice) { + bbgo.Notify("existing %s position has the same direction as the signal", s.Symbol) + return } - orderForm := s.generateOrderForm(side, s.calculateQuantity(ctx, closePrice, side), types.SideEffectTypeMarginBuy) + orderForm := s.generateOrderForm(side, amount, types.SideEffectTypeMarginBuy) log.Infof("submit open position order %v", orderForm) _, err := s.orderExecutor.SubmitOrders(ctx, orderForm) if err != nil { From 1fb6e7909055cdedff654ff9312f12e83a95d994 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 20 Apr 2023 18:11:47 +0800 Subject: [PATCH 0799/1392] improve/supertrend: fix typo --- pkg/strategy/supertrend/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 2a2aa2320a..309ecc4cf4 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -627,7 +627,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Add opposite position amount if any if (side == types.SideTypeSell && s.Position.IsLong()) || (side == types.SideTypeBuy && s.Position.IsShort()) { bbgo.Notify("add existing opposite position amount of %s to the amount of open new position order", s.Symbol) - amount = amount + s.Position.GetQuantity() + amount = amount.Add(s.Position.GetQuantity()) } else if !s.Position.IsDust(closePrice) { bbgo.Notify("existing %s position has the same direction as the signal", s.Symbol) return From 9f8576bb38b9141d9269b8a2e38d66ae37b43c22 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 20 Apr 2023 18:37:48 +0800 Subject: [PATCH 0800/1392] improve/supertrend: different way to calculate order amount for backtesting --- pkg/strategy/supertrend/strategy.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 309ecc4cf4..1003c621ee 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -626,8 +626,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Add opposite position amount if any if (side == types.SideTypeSell && s.Position.IsLong()) || (side == types.SideTypeBuy && s.Position.IsShort()) { - bbgo.Notify("add existing opposite position amount of %s to the amount of open new position order", s.Symbol) - amount = amount.Add(s.Position.GetQuantity()) + if bbgo.IsBackTesting { + _ = s.ClosePosition(ctx, fixedpoint.One) + bbgo.Notify("close existing %s position before open a new position", s.Symbol) + amount = s.calculateQuantity(ctx, closePrice, side) + } else { + bbgo.Notify("add existing opposite position amount %f of %s to the amount %f of open new position order", s.Position.GetQuantity().Float64(), s.Symbol, amount.Float64()) + amount = amount.Add(s.Position.GetQuantity()) + } } else if !s.Position.IsDust(closePrice) { bbgo.Notify("existing %s position has the same direction as the signal", s.Symbol) return From a9b02703904c238f757dcaa2b90dd2d757b10b55 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Apr 2023 18:29:07 +0800 Subject: [PATCH 0801/1392] bbgo: add context to LoadState --- cmd/bbgo-lorca/main.go | 2 +- cmd/bbgo-webview/main.go | 2 +- pkg/bbgo/trader.go | 8 +++----- pkg/cmd/run.go | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cmd/bbgo-lorca/main.go b/cmd/bbgo-lorca/main.go index b44a225d8a..3718ee5727 100644 --- a/cmd/bbgo-lorca/main.go +++ b/cmd/bbgo-lorca/main.go @@ -101,7 +101,7 @@ func main() { return } - if err := trader.LoadState(); err != nil { + if err := trader.LoadState(ctx); err != nil { log.WithError(err).Error("failed to load strategy states") return } diff --git a/cmd/bbgo-webview/main.go b/cmd/bbgo-webview/main.go index 3402370903..7cc5060e2c 100644 --- a/cmd/bbgo-webview/main.go +++ b/cmd/bbgo-webview/main.go @@ -109,7 +109,7 @@ func main() { return } - if err := trader.LoadState(); err != nil { + if err := trader.LoadState(ctx); err != nil { log.WithError(err).Error("failed to load strategy states") return } diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 53fc2086f3..fee49d90ff 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -365,16 +365,14 @@ func (trader *Trader) Run(ctx context.Context) error { return trader.environment.Connect(ctx) } -func (trader *Trader) LoadState() error { +func (trader *Trader) LoadState(ctx context.Context) error { if trader.environment.BacktestService != nil { return nil } - if persistenceServiceFacade == nil { - return nil - } + isolation := GetIsolationFromContext(ctx) - ps := persistenceServiceFacade.Get() + ps := isolation.persistenceServiceFacade.Get() log.Infof("loading strategies states...") diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 975ede42a3..bc601e0dc2 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -163,7 +163,7 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con return err } - if err := trader.LoadState(); err != nil { + if err := trader.LoadState(tradingCtx); err != nil { return err } From a1e297e296df9908675096475f40c54dc9675b9b Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Apr 2023 19:38:31 +0800 Subject: [PATCH 0802/1392] types: improve order struct json size --- pkg/types/order.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/types/order.go b/pkg/types/order.go index b103fb3a55..369b8889b1 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -117,7 +117,7 @@ const ( ) type SubmitOrder struct { - ClientOrderID string `json:"clientOrderID" db:"client_order_id"` + ClientOrderID string `json:"clientOrderID,omitempty" db:"client_order_id"` Symbol string `json:"symbol" db:"symbol"` Side SideType `json:"side" db:"side"` @@ -127,7 +127,7 @@ type SubmitOrder struct { Price fixedpoint.Value `json:"price" db:"price"` // AveragePrice is only used in back-test currently - AveragePrice fixedpoint.Value `json:"averagePrice"` + AveragePrice fixedpoint.Value `json:"averagePrice,omitempty"` StopPrice fixedpoint.Value `json:"stopPrice,omitempty" db:"stop_price"` @@ -139,10 +139,10 @@ type SubmitOrder struct { MarginSideEffect MarginOrderSideEffectType `json:"marginSideEffect,omitempty"` // AUTO_REPAY = repay, MARGIN_BUY = borrow, defaults to NO_SIDE_EFFECT - ReduceOnly bool `json:"reduceOnly" db:"reduce_only"` - ClosePosition bool `json:"closePosition" db:"close_position"` + ReduceOnly bool `json:"reduceOnly,omitempty" db:"reduce_only"` + ClosePosition bool `json:"closePosition,omitempty" db:"close_position"` - Tag string `json:"tag" db:"-"` + Tag string `json:"tag,omitempty" db:"-"` } func (o *SubmitOrder) In() (fixedpoint.Value, string) { @@ -248,7 +248,7 @@ type Order struct { Exchange ExchangeName `json:"exchange" db:"exchange"` // GID is used for relational database storage, it's an incremental ID - GID uint64 `json:"gid" db:"gid"` + GID uint64 `json:"gid,omitempty" db:"gid"` OrderID uint64 `json:"orderID" db:"order_id"` // order id UUID string `json:"uuid,omitempty"` @@ -258,9 +258,9 @@ type Order struct { CreationTime Time `json:"creationTime" db:"created_at"` UpdateTime Time `json:"updateTime" db:"updated_at"` - IsFutures bool `json:"isFutures" db:"is_futures"` - IsMargin bool `json:"isMargin" db:"is_margin"` - IsIsolated bool `json:"isIsolated" db:"is_isolated"` + IsFutures bool `json:"isFutures,omitempty" db:"is_futures"` + IsMargin bool `json:"isMargin,omitempty" db:"is_margin"` + IsIsolated bool `json:"isIsolated,omitempty" db:"is_isolated"` } func (o Order) CsvHeader() []string { From a13ad2f6ab3e3b502950085168ae52f978f88089 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 00:37:13 +0800 Subject: [PATCH 0803/1392] fix: avoid global persistenceServiceFacade concurrent write --- pkg/bbgo/bootstrap.go | 4 ++-- pkg/bbgo/environment.go | 7 ++++--- pkg/bbgo/isolation.go | 2 +- pkg/bbgo/persistence.go | 3 +-- pkg/bbgo/trader.go | 14 +++++++++----- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pkg/bbgo/bootstrap.go b/pkg/bbgo/bootstrap.go index 78ec8eaebe..63b2dc4520 100644 --- a/pkg/bbgo/bootstrap.go +++ b/pkg/bbgo/bootstrap.go @@ -46,7 +46,7 @@ func BootstrapEnvironment(ctx context.Context, environ *Environment, userConfig } } - if err := environ.ConfigureNotificationSystem(userConfig); err != nil { + if err := environ.ConfigureNotificationSystem(ctx, userConfig); err != nil { return errors.Wrap(err, "notification configure error") } @@ -55,4 +55,4 @@ func BootstrapEnvironment(ctx context.Context, environ *Environment, userConfig func BootstrapBacktestEnvironment(ctx context.Context, environ *Environment) error { return environ.ConfigureDatabase(ctx) -} \ No newline at end of file +} diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index d35e125964..6bdad5e424 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -605,13 +605,14 @@ func (environ *Environment) syncSession(ctx context.Context, session *ExchangeSe return environ.SyncService.SyncSessionSymbols(ctx, session.Exchange, environ.syncStartTime, symbols...) } -func (environ *Environment) ConfigureNotificationSystem(userConfig *Config) error { +func (environ *Environment) ConfigureNotificationSystem(ctx context.Context, userConfig *Config) error { // setup default notification config if userConfig.Notifications == nil { userConfig.Notifications = &NotificationConfig{} } - var persistence = persistenceServiceFacade.Get() + var isolation = GetIsolationFromContext(ctx) + var persistence = isolation.persistenceServiceFacade.Get() err := environ.setupInteraction(persistence) if err != nil { @@ -985,4 +986,4 @@ func (session *ExchangeSession) getSessionSymbols(defaultSymbols ...string) ([]s } return session.FindPossibleSymbols() -} \ No newline at end of file +} diff --git a/pkg/bbgo/isolation.go b/pkg/bbgo/isolation.go index df3d43ae07..51da5a310d 100644 --- a/pkg/bbgo/isolation.go +++ b/pkg/bbgo/isolation.go @@ -18,7 +18,7 @@ type Isolation struct { func NewDefaultIsolation() *Isolation { return &Isolation{ gracefulShutdown: GracefulShutdown{}, - persistenceServiceFacade: persistenceServiceFacade, + persistenceServiceFacade: defaultPersistenceServiceFacade, } } diff --git a/pkg/bbgo/persistence.go b/pkg/bbgo/persistence.go index bbc7c75b42..4a0ae302f3 100644 --- a/pkg/bbgo/persistence.go +++ b/pkg/bbgo/persistence.go @@ -111,7 +111,6 @@ func ConfigurePersistence(ctx context.Context, environ *Environment, conf *Persi isolation := GetIsolationFromContext(ctx) isolation.persistenceServiceFacade = facade - persistenceServiceFacade = facade environ.PersistentService = facade return nil -} \ No newline at end of file +} diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index fee49d90ff..0d0c0ca8b0 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -233,7 +233,7 @@ func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error { return errors.New("strategy object is not a struct") } - if err := trader.injectCommonServices(strategy); err != nil { + if err := trader.injectCommonServices(ctx, strategy); err != nil { return err } @@ -303,7 +303,7 @@ func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error { continue } - if err := trader.injectCommonServices(strategy); err != nil { + if err := trader.injectCommonServices(ctx, strategy); err != nil { return err } @@ -425,7 +425,11 @@ func (trader *Trader) Shutdown(ctx context.Context) { trader.gracefulShutdown.Shutdown(ctx) } -func (trader *Trader) injectCommonServices(s interface{}) error { +func (trader *Trader) injectCommonServices(ctx context.Context, s interface{}) error { + isolation := GetIsolationFromContext(ctx) + + ps := isolation.persistenceServiceFacade + // a special injection for persistence selector: // if user defined the selector, the facade pointer will be nil, hence we need to update the persistence facade pointer sv := reflect.ValueOf(s).Elem() @@ -437,7 +441,7 @@ func (trader *Trader) injectCommonServices(s interface{}) error { return fmt.Errorf("field Persistence is not a struct element, %s given", field) } - if err := dynamic.InjectField(elem, "Facade", persistenceServiceFacade, true); err != nil { + if err := dynamic.InjectField(elem, "Facade", ps, true); err != nil { return err } @@ -457,6 +461,6 @@ func (trader *Trader) injectCommonServices(s interface{}) error { trader.environment.DatabaseService, trader.environment.AccountService, trader.environment, - persistenceServiceFacade, // if the strategy use persistence facade separately + ps, // if the strategy use persistence facade separately ) } From 3d7cdd9938c65a598293cf40ee5fa0004b5a18db Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 00:42:33 +0800 Subject: [PATCH 0804/1392] fix: drop the global persistenceServiceFacade --- pkg/bbgo/isolation_test.go | 2 +- pkg/bbgo/persistence.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/bbgo/isolation_test.go b/pkg/bbgo/isolation_test.go index bd0688ef09..12b164ee46 100644 --- a/pkg/bbgo/isolation_test.go +++ b/pkg/bbgo/isolation_test.go @@ -20,5 +20,5 @@ func TestNewDefaultIsolation(t *testing.T) { assert.NotNil(t, isolation) assert.NotNil(t, isolation.persistenceServiceFacade) assert.NotNil(t, isolation.gracefulShutdown) - assert.Equal(t, persistenceServiceFacade, isolation.persistenceServiceFacade) + assert.Equal(t, defaultPersistenceServiceFacade, isolation.persistenceServiceFacade) } diff --git a/pkg/bbgo/persistence.go b/pkg/bbgo/persistence.go index 4a0ae302f3..d237ae30d1 100644 --- a/pkg/bbgo/persistence.go +++ b/pkg/bbgo/persistence.go @@ -17,8 +17,6 @@ var defaultPersistenceServiceFacade = &service.PersistenceServiceFacade{ Memory: service.NewMemoryService(), } -var persistenceServiceFacade = defaultPersistenceServiceFacade - // Sync syncs the object properties into the persistence layer func Sync(ctx context.Context, obj interface{}) { id := dynamic.CallID(obj) From 4b2c5198fae471f499219e34a58e2e4ad23e9d37 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 26 Apr 2023 10:32:43 +0800 Subject: [PATCH 0805/1392] strategy/supertrend: add strategy parameter fields in profit report --- pkg/strategy/supertrend/strategy.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 1003c621ee..88a3895df1 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -32,6 +32,8 @@ func init() { // AccumulatedProfitReport For accumulated profit report output type AccumulatedProfitReport struct { + s *Strategy + // AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"` @@ -75,7 +77,9 @@ type AccumulatedProfitReport struct { previousAccumulatedTrades int } -func (r *AccumulatedProfitReport) Initialize() { +func (r *AccumulatedProfitReport) Initialize(strategy *Strategy) { + r.s = strategy + if r.AccumulatedProfitMAWindow <= 0 { r.AccumulatedProfitMAWindow = 60 } @@ -135,7 +139,7 @@ func (r *AccumulatedProfitReport) Output(symbol string) { } defer tsvwiter.Close() // Output symbol, total acc. profit, acc. profit 60MA, interval acc. profit, fee, win rate, profit factor - _ = tsvwiter.Write([]string{"#", "Symbol", "accumulatedProfit", "accumulatedProfitMA", fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), "accumulatedFee", "winRatio", "profitFactor", "60D trades"}) + _ = tsvwiter.Write([]string{"#", "Symbol", "accumulatedProfit", "accumulatedProfitMA", fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), "accumulatedFee", "winRatio", "profitFactor", "60D trades", "Window", "Multiplier", "FastDEMA", "SlowDEMA", "LinReg"}) for i := 0; i <= r.NumberOfInterval-1; i++ { accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i) accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit) @@ -148,8 +152,13 @@ func (r *AccumulatedProfitReport) Output(symbol string) { profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i)) trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum() tradesStr := fmt.Sprintf("%f", trades) + windowStr := fmt.Sprintf("%d", r.s.Window) + multiplierStr := fmt.Sprintf("%f", r.s.SupertrendMultiplier) + fastDEMAStr := fmt.Sprintf("%d", r.s.FastDEMAWindow) + slowDEMAStr := fmt.Sprintf("%d", r.s.SlowDEMAWindow) + linRegStr := fmt.Sprintf("%d", r.s.LinearRegression.Window) - _ = tsvwiter.Write([]string{fmt.Sprintf("%d", i+1), symbol, accumulatedProfitStr, accumulatedProfitMAStr, intervalAccumulatedProfitStr, accumulatedFee, winRatio, profitFactor, tradesStr}) + _ = tsvwiter.Write([]string{fmt.Sprintf("%d", i+1), symbol, accumulatedProfitStr, accumulatedProfitMAStr, intervalAccumulatedProfitStr, accumulatedFee, winRatio, profitFactor, tradesStr, windowStr, multiplierStr, fastDEMAStr, slowDEMAStr, linRegStr}) } } } @@ -486,7 +495,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if s.AccumulatedProfitReport == nil { s.AccumulatedProfitReport = &AccumulatedProfitReport{} } - s.AccumulatedProfitReport.Initialize() + s.AccumulatedProfitReport.Initialize(s) s.orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) { if profit == nil { return From e8a761e331a878dbddc8f0afcf2378711b5791ff Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 16:14:53 +0800 Subject: [PATCH 0806/1392] grid2: add profitFixer and tests --- pkg/strategy/grid2/profit_fixer.go | 78 ++++++++++++++ pkg/strategy/grid2/profit_fixer_test.go | 102 ++++++++++++++++++ pkg/strategy/grid2/recover.go | 14 +++ pkg/strategy/grid2/strategy.go | 3 + pkg/types/exchange.go | 1 + .../mocks/mock_exchange_trade_history.go | 67 ++++++++++++ 6 files changed, 265 insertions(+) create mode 100644 pkg/strategy/grid2/profit_fixer.go create mode 100644 pkg/strategy/grid2/profit_fixer_test.go create mode 100644 pkg/types/mocks/mock_exchange_trade_history.go diff --git a/pkg/strategy/grid2/profit_fixer.go b/pkg/strategy/grid2/profit_fixer.go new file mode 100644 index 0000000000..0bfc7f6546 --- /dev/null +++ b/pkg/strategy/grid2/profit_fixer.go @@ -0,0 +1,78 @@ +package grid2 + +import ( + "context" + "time" + + "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/exchange/batch" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type ProfitFixer struct { + symbol string + grid *Grid + historyService types.ExchangeTradeHistoryService +} + +func newProfitFixer(grid *Grid, symbol string, historyService types.ExchangeTradeHistoryService) *ProfitFixer { + return &ProfitFixer{ + symbol: symbol, + grid: grid, + historyService: historyService, + } +} + +// Fix fixes the total quote profit of the given grid +func (f *ProfitFixer) Fix(ctx context.Context, since, until time.Time, initialOrderID uint64, profitStats *GridProfitStats) error { + profitStats.TotalQuoteProfit = fixedpoint.Zero + profitStats.ArbitrageCount = 0 + + q := &batch.ClosedOrderBatchQuery{ExchangeTradeHistoryService: f.historyService} + orderC, errC := q.Query(ctx, f.symbol, since, until, initialOrderID) + + for { + select { + case <-ctx.Done(): + if errors.Is(ctx.Err(), context.Canceled) { + return nil + } + + return ctx.Err() + + case order, ok := <-orderC: + if !ok { + return <-errC + } + + if !f.grid.HasPrice(order.Price) { + continue + } + + if profitStats.InitialOrderID == 0 { + profitStats.InitialOrderID = order.OrderID + } + + if order.Status != types.OrderStatusFilled { + continue + } + + if order.Type != types.OrderTypeLimit { + continue + } + + if order.Side != types.SideTypeSell { + continue + } + + quoteProfit := order.Quantity.Mul(f.grid.Spread) + profitStats.TotalQuoteProfit = profitStats.TotalQuoteProfit.Add(quoteProfit) + profitStats.ArbitrageCount++ + + log.Debugf("profitFixer: filledSellOrder=%#v", order) + log.Debugf("profitFixer: profitStats=%#v", profitStats) + } + } +} diff --git a/pkg/strategy/grid2/profit_fixer_test.go b/pkg/strategy/grid2/profit_fixer_test.go new file mode 100644 index 0000000000..04cbc5021c --- /dev/null +++ b/pkg/strategy/grid2/profit_fixer_test.go @@ -0,0 +1,102 @@ +package grid2 + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/mocks" +) + +func mustNewTime(v string) time.Time { + t, err := time.Parse(time.RFC3339, v) + if err != nil { + panic(err) + } + + return t +} + +var testClosedOrderID = uint64(0) + +func newClosedLimitOrder(symbol string, side types.SideType, price, quantity fixedpoint.Value, ta ...time.Time) types.Order { + testClosedOrderID++ + creationTime := time.Now() + updateTime := creationTime + + if len(ta) > 0 { + creationTime = ta[0] + if len(ta) > 1 { + updateTime = ta[1] + } + } + + return types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: symbol, + Side: side, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + }, + Exchange: types.ExchangeBinance, + OrderID: testClosedOrderID, + Status: types.OrderStatusFilled, + ExecutedQuantity: quantity, + CreationTime: types.Time(creationTime), + UpdateTime: types.Time(updateTime), + } +} + +func TestProfitFixer(t *testing.T) { + testOrderID = 0 + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + ctx := context.Background() + mockHistoryService := mocks.NewMockExchangeTradeHistoryService(mockCtrl) + + mockHistoryService.EXPECT().QueryClosedOrders(ctx, "ETHUSDT", mustNewTime("2022-01-01T00:00:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(0)). + Return([]types.Order{ + newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1800.0), number(0.1), mustNewTime("2022-01-01T00:01:00Z")), + newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1700.0), number(0.1), mustNewTime("2022-01-01T00:01:00Z")), + newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1800.0), number(0.1), mustNewTime("2022-01-01T00:01:00Z")), + newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1900.0), number(0.1), mustNewTime("2022-01-01T00:03:00Z")), + newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1905.0), number(0.1), mustNewTime("2022-01-01T00:03:00Z")), + }, nil) + + mockHistoryService.EXPECT().QueryClosedOrders(ctx, "ETHUSDT", mustNewTime("2022-01-01T00:03:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(5)). + Return([]types.Order{ + newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1900.0), number(0.1), mustNewTime("2022-01-01T00:04:00Z")), + newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1800.0), number(0.1), mustNewTime("2022-01-01T00:04:00Z")), + newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1700.0), number(0.1), mustNewTime("2022-01-01T00:04:00Z")), + newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1800.0), number(0.1), mustNewTime("2022-01-01T00:04:00Z")), + newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1900.0), number(0.1), mustNewTime("2022-01-01T00:08:00Z")), + }, nil) + + mockHistoryService.EXPECT().QueryClosedOrders(ctx, "ETHUSDT", mustNewTime("2022-01-01T00:08:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(10)). + Return([]types.Order{}, nil) + + grid := NewGrid(number(1000.0), number(2000.0), number(11), number(0.01)) + grid.CalculateArithmeticPins() + + since, err := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z") + assert.NoError(t, err) + + until, err := time.Parse(time.RFC3339, "2022-01-07T00:00:00Z") + assert.NoError(t, err) + + stats := &GridProfitStats{} + fixer := newProfitFixer(grid, "ETHUSDT", mockHistoryService) + err = fixer.Fix(ctx, since, until, 0, stats) + assert.NoError(t, err) + + assert.Equal(t, "40", stats.TotalQuoteProfit.String()) + assert.Equal(t, 4, stats.ArbitrageCount) +} diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index f8b26d4a6f..8b28588548 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -70,6 +70,20 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex // emit ready after recover s.EmitGridReady() + defer bbgo.Sync(ctx, s) + + if s.EnableProfitFixer { + until := time.Now() + since := until.Add(7 * 24 * time.Hour) + if s.FixProfitSince != nil { + since = s.FixProfitSince.Time() + } + + fixer := newProfitFixer(s.grid, s.Symbol, historyService) + // set initial order ID = 0 instead of s.GridProfitStats.InitialOrderID because the order ID could be incorrect + return fixer.Fix(ctx, since, until, 0, s.GridProfitStats) + } + return nil } diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 9c9ef43561..df803eac2d 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -165,6 +165,9 @@ type Strategy struct { SkipSpreadCheck bool `json:"skipSpreadCheck"` RecoverGridByScanningTrades bool `json:"recoverGridByScanningTrades"` + EnableProfitFixer bool `json:"enableProfitFixer"` + FixProfitSince *types.Time `json:"fixProfitSince"` + // Debug enables the debug mode Debug bool `json:"debug"` diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index cab368a420..800ac5be62 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -108,6 +108,7 @@ type ExchangeAmountFeeProtect interface { SetModifyOrderAmountForFee(ExchangeFee) } +//go:generate mockgen -destination=mocks/mock_exchange_trade_history.go -package=mocks . ExchangeTradeHistoryService type ExchangeTradeHistoryService interface { QueryTrades(ctx context.Context, symbol string, options *TradeQueryOptions) ([]Trade, error) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []Order, err error) diff --git a/pkg/types/mocks/mock_exchange_trade_history.go b/pkg/types/mocks/mock_exchange_trade_history.go new file mode 100644 index 0000000000..8497bef176 --- /dev/null +++ b/pkg/types/mocks/mock_exchange_trade_history.go @@ -0,0 +1,67 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/c9s/bbgo/pkg/types (interfaces: ExchangeTradeHistoryService) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + time "time" + + types "github.com/c9s/bbgo/pkg/types" + gomock "github.com/golang/mock/gomock" +) + +// MockExchangeTradeHistoryService is a mock of ExchangeTradeHistoryService interface. +type MockExchangeTradeHistoryService struct { + ctrl *gomock.Controller + recorder *MockExchangeTradeHistoryServiceMockRecorder +} + +// MockExchangeTradeHistoryServiceMockRecorder is the mock recorder for MockExchangeTradeHistoryService. +type MockExchangeTradeHistoryServiceMockRecorder struct { + mock *MockExchangeTradeHistoryService +} + +// NewMockExchangeTradeHistoryService creates a new mock instance. +func NewMockExchangeTradeHistoryService(ctrl *gomock.Controller) *MockExchangeTradeHistoryService { + mock := &MockExchangeTradeHistoryService{ctrl: ctrl} + mock.recorder = &MockExchangeTradeHistoryServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchangeTradeHistoryService) EXPECT() *MockExchangeTradeHistoryServiceMockRecorder { + return m.recorder +} + +// QueryClosedOrders mocks base method. +func (m *MockExchangeTradeHistoryService) QueryClosedOrders(arg0 context.Context, arg1 string, arg2, arg3 time.Time, arg4 uint64) ([]types.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryClosedOrders", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].([]types.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryClosedOrders indicates an expected call of QueryClosedOrders. +func (mr *MockExchangeTradeHistoryServiceMockRecorder) QueryClosedOrders(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryClosedOrders", reflect.TypeOf((*MockExchangeTradeHistoryService)(nil).QueryClosedOrders), arg0, arg1, arg2, arg3, arg4) +} + +// QueryTrades mocks base method. +func (m *MockExchangeTradeHistoryService) QueryTrades(arg0 context.Context, arg1 string, arg2 *types.TradeQueryOptions) ([]types.Trade, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryTrades", arg0, arg1, arg2) + ret0, _ := ret[0].([]types.Trade) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryTrades indicates an expected call of QueryTrades. +func (mr *MockExchangeTradeHistoryServiceMockRecorder) QueryTrades(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTrades", reflect.TypeOf((*MockExchangeTradeHistoryService)(nil).QueryTrades), arg0, arg1, arg2) +} From 036bae692ee55c6a20cfb256bf8bbe59c246be1d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 17:30:31 +0800 Subject: [PATCH 0807/1392] grid2: move emitGridReady to earlier --- pkg/strategy/grid2/profit_fixer_test.go | 2 +- pkg/strategy/grid2/recover.go | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/profit_fixer_test.go b/pkg/strategy/grid2/profit_fixer_test.go index 04cbc5021c..e3e8a9722b 100644 --- a/pkg/strategy/grid2/profit_fixer_test.go +++ b/pkg/strategy/grid2/profit_fixer_test.go @@ -54,7 +54,7 @@ func newClosedLimitOrder(symbol string, side types.SideType, price, quantity fix } func TestProfitFixer(t *testing.T) { - testOrderID = 0 + testClosedOrderID = 0 mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 8b28588548..29b1dff27f 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -63,18 +63,19 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex return errors.Wrap(err, "grid recover error") } + // emit ready after recover + s.EmitGridReady() + // debug and send metrics // wait for the reverse order to be placed time.Sleep(2 * time.Second) debugGrid(s.logger, s.grid, s.orderExecutor.ActiveMakerOrders()) - // emit ready after recover - s.EmitGridReady() defer bbgo.Sync(ctx, s) if s.EnableProfitFixer { until := time.Now() - since := until.Add(7 * 24 * time.Hour) + since := until.Add(-7 * 24 * time.Hour) if s.FixProfitSince != nil { since = s.FixProfitSince.Time() } From 77f6c6bb4627c5d6aa4a8216bd53d4f5eb25ec91 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 17:40:01 +0800 Subject: [PATCH 0808/1392] bbgo: lock strategy before we sync data --- pkg/bbgo/persistence.go | 8 ++++++++ pkg/strategy/grid2/strategy.go | 3 +++ 2 files changed, 11 insertions(+) diff --git a/pkg/bbgo/persistence.go b/pkg/bbgo/persistence.go index d237ae30d1..34b8205b5d 100644 --- a/pkg/bbgo/persistence.go +++ b/pkg/bbgo/persistence.go @@ -4,6 +4,7 @@ import ( "context" "os" "reflect" + "sync" "github.com/codingconcepts/env" "github.com/pkg/errors" @@ -28,6 +29,13 @@ func Sync(ctx context.Context, obj interface{}) { isolation := GetIsolationFromContext(ctx) ps := isolation.persistenceServiceFacade.Get() + + locker, ok := obj.(sync.Locker) + if ok { + locker.Lock() + defer locker.Unlock() + } + err := storePersistenceFields(obj, id, ps) if err != nil { log.WithError(err).Errorf("persistence sync failed") diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index df803eac2d..2c6beacd54 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -199,6 +199,9 @@ type Strategy struct { tradingCtx, writeCtx context.Context cancelWrite context.CancelFunc + + // this ensures that bbgo.Sync to lock the object + sync.Mutex } func (s *Strategy) ID() string { From 55c84e005b6fe4821e73738b13e795ff03777f21 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 22:06:53 +0800 Subject: [PATCH 0809/1392] grid2: add one more check for profitStats.InitialOrderID --- pkg/strategy/grid2/profit_fixer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/profit_fixer.go b/pkg/strategy/grid2/profit_fixer.go index 0bfc7f6546..35f24db4e3 100644 --- a/pkg/strategy/grid2/profit_fixer.go +++ b/pkg/strategy/grid2/profit_fixer.go @@ -51,7 +51,7 @@ func (f *ProfitFixer) Fix(ctx context.Context, since, until time.Time, initialOr continue } - if profitStats.InitialOrderID == 0 { + if profitStats.InitialOrderID == 0 || order.OrderID < profitStats.InitialOrderID { profitStats.InitialOrderID = order.OrderID } From 68974bc0b4ac621c31250eb105e0b554d7b4d7ce Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 22:10:45 +0800 Subject: [PATCH 0810/1392] grid2: fix profitstats.Since when possible --- pkg/strategy/grid2/profit_fixer.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/strategy/grid2/profit_fixer.go b/pkg/strategy/grid2/profit_fixer.go index 35f24db4e3..07444eab4c 100644 --- a/pkg/strategy/grid2/profit_fixer.go +++ b/pkg/strategy/grid2/profit_fixer.go @@ -55,6 +55,11 @@ func (f *ProfitFixer) Fix(ctx context.Context, since, until time.Time, initialOr profitStats.InitialOrderID = order.OrderID } + if profitStats.Since == nil || profitStats.Since.IsZero() || order.CreationTime.Time().Before(*profitStats.Since) { + ct := order.CreationTime.Time() + profitStats.Since = &ct + } + if order.Status != types.OrderStatusFilled { continue } From bd5e98e543fd7c62360de0086d5fb009ea6ba634 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 23:07:01 +0800 Subject: [PATCH 0811/1392] grid2: add more log --- pkg/strategy/grid2/profit_fixer.go | 10 ++++++++++ pkg/strategy/grid2/recover.go | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/profit_fixer.go b/pkg/strategy/grid2/profit_fixer.go index 07444eab4c..1cc5d530de 100644 --- a/pkg/strategy/grid2/profit_fixer.go +++ b/pkg/strategy/grid2/profit_fixer.go @@ -27,9 +27,19 @@ func newProfitFixer(grid *Grid, symbol string, historyService types.ExchangeTrad // Fix fixes the total quote profit of the given grid func (f *ProfitFixer) Fix(ctx context.Context, since, until time.Time, initialOrderID uint64, profitStats *GridProfitStats) error { + // reset profit profitStats.TotalQuoteProfit = fixedpoint.Zero profitStats.ArbitrageCount = 0 + defer log.Infof("profit fix is done") + + /* + if profitStats.Since != nil && profitStats.Since.Before(since) { + log.Infof("profitStats.since %s is ealier than the given since %s, setting since to %s", profitStats.Since, since, profitStats.Since) + since = *profitStats.Since + } + */ + q := &batch.ClosedOrderBatchQuery{ExchangeTradeHistoryService: f.historyService} orderC, errC := q.Query(ctx, f.symbol, since, until, initialOrderID) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 29b1dff27f..e6a1b90006 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -82,7 +82,12 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex fixer := newProfitFixer(s.grid, s.Symbol, historyService) // set initial order ID = 0 instead of s.GridProfitStats.InitialOrderID because the order ID could be incorrect - return fixer.Fix(ctx, since, until, 0, s.GridProfitStats) + err := fixer.Fix(ctx, since, until, 0, s.GridProfitStats) + if err != nil { + return err + } + + s.logger.Infof("fixed profitStats: %#v", s.GridProfitStats) } return nil From 2efdee93473f9a14ae3319df61fb18bea426ef29 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 23:28:12 +0800 Subject: [PATCH 0812/1392] grid2: add timeout context to the fixer --- pkg/strategy/grid2/profit_fixer.go | 5 ++++- pkg/strategy/grid2/profit_fixer_test.go | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/profit_fixer.go b/pkg/strategy/grid2/profit_fixer.go index 1cc5d530de..40e41c416f 100644 --- a/pkg/strategy/grid2/profit_fixer.go +++ b/pkg/strategy/grid2/profit_fixer.go @@ -26,7 +26,7 @@ func newProfitFixer(grid *Grid, symbol string, historyService types.ExchangeTrad } // Fix fixes the total quote profit of the given grid -func (f *ProfitFixer) Fix(ctx context.Context, since, until time.Time, initialOrderID uint64, profitStats *GridProfitStats) error { +func (f *ProfitFixer) Fix(parent context.Context, since, until time.Time, initialOrderID uint64, profitStats *GridProfitStats) error { // reset profit profitStats.TotalQuoteProfit = fixedpoint.Zero profitStats.ArbitrageCount = 0 @@ -40,6 +40,9 @@ func (f *ProfitFixer) Fix(ctx context.Context, since, until time.Time, initialOr } */ + ctx, cancel := context.WithTimeout(parent, 15*time.Minute) + defer cancel() + q := &batch.ClosedOrderBatchQuery{ExchangeTradeHistoryService: f.historyService} orderC, errC := q.Query(ctx, f.symbol, since, until, initialOrderID) diff --git a/pkg/strategy/grid2/profit_fixer_test.go b/pkg/strategy/grid2/profit_fixer_test.go index e3e8a9722b..52d2964151 100644 --- a/pkg/strategy/grid2/profit_fixer_test.go +++ b/pkg/strategy/grid2/profit_fixer_test.go @@ -62,7 +62,7 @@ func TestProfitFixer(t *testing.T) { ctx := context.Background() mockHistoryService := mocks.NewMockExchangeTradeHistoryService(mockCtrl) - mockHistoryService.EXPECT().QueryClosedOrders(ctx, "ETHUSDT", mustNewTime("2022-01-01T00:00:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(0)). + mockHistoryService.EXPECT().QueryClosedOrders(gomock.Any(), "ETHUSDT", mustNewTime("2022-01-01T00:00:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(0)). Return([]types.Order{ newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1800.0), number(0.1), mustNewTime("2022-01-01T00:01:00Z")), newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1700.0), number(0.1), mustNewTime("2022-01-01T00:01:00Z")), @@ -71,7 +71,7 @@ func TestProfitFixer(t *testing.T) { newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1905.0), number(0.1), mustNewTime("2022-01-01T00:03:00Z")), }, nil) - mockHistoryService.EXPECT().QueryClosedOrders(ctx, "ETHUSDT", mustNewTime("2022-01-01T00:03:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(5)). + mockHistoryService.EXPECT().QueryClosedOrders(gomock.Any(), "ETHUSDT", mustNewTime("2022-01-01T00:03:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(5)). Return([]types.Order{ newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1900.0), number(0.1), mustNewTime("2022-01-01T00:04:00Z")), newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1800.0), number(0.1), mustNewTime("2022-01-01T00:04:00Z")), @@ -80,7 +80,7 @@ func TestProfitFixer(t *testing.T) { newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1900.0), number(0.1), mustNewTime("2022-01-01T00:08:00Z")), }, nil) - mockHistoryService.EXPECT().QueryClosedOrders(ctx, "ETHUSDT", mustNewTime("2022-01-01T00:08:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(10)). + mockHistoryService.EXPECT().QueryClosedOrders(gomock.Any(), "ETHUSDT", mustNewTime("2022-01-01T00:08:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(10)). Return([]types.Order{}, nil) grid := NewGrid(number(1000.0), number(2000.0), number(11), number(0.01)) From f1919a2b437346cecbb6f8f5fe96afef70078822 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 23:34:56 +0800 Subject: [PATCH 0813/1392] grid2: check profitStats.Since for the since time range --- pkg/strategy/grid2/profit_fixer.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/profit_fixer.go b/pkg/strategy/grid2/profit_fixer.go index 40e41c416f..c572a61bb4 100644 --- a/pkg/strategy/grid2/profit_fixer.go +++ b/pkg/strategy/grid2/profit_fixer.go @@ -31,14 +31,12 @@ func (f *ProfitFixer) Fix(parent context.Context, since, until time.Time, initia profitStats.TotalQuoteProfit = fixedpoint.Zero profitStats.ArbitrageCount = 0 - defer log.Infof("profit fix is done") + defer log.Infof("profitFixer: done") - /* - if profitStats.Since != nil && profitStats.Since.Before(since) { - log.Infof("profitStats.since %s is ealier than the given since %s, setting since to %s", profitStats.Since, since, profitStats.Since) - since = *profitStats.Since - } - */ + if profitStats.Since != nil && profitStats.Since.Before(since) { + log.Infof("profitFixer: profitStats.since %s is ealier than the given since %s, setting since to %s", profitStats.Since, since, profitStats.Since) + since = *profitStats.Since + } ctx, cancel := context.WithTimeout(parent, 15*time.Minute) defer cancel() @@ -46,6 +44,10 @@ func (f *ProfitFixer) Fix(parent context.Context, since, until time.Time, initia q := &batch.ClosedOrderBatchQuery{ExchangeTradeHistoryService: f.historyService} orderC, errC := q.Query(ctx, f.symbol, since, until, initialOrderID) + defer func() { + log.Infof("profitFixer: fixed profitStats=%#v", profitStats) + }() + for { select { case <-ctx.Done(): @@ -90,7 +92,6 @@ func (f *ProfitFixer) Fix(parent context.Context, since, until time.Time, initia profitStats.ArbitrageCount++ log.Debugf("profitFixer: filledSellOrder=%#v", order) - log.Debugf("profitFixer: profitStats=%#v", profitStats) } } } From b358cec2359a86aa5cd3813478510ad01bf35761 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 23:36:53 +0800 Subject: [PATCH 0814/1392] grid2: check if profitStats.Since.IsZero --- pkg/strategy/grid2/profit_fixer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/profit_fixer.go b/pkg/strategy/grid2/profit_fixer.go index c572a61bb4..328934a182 100644 --- a/pkg/strategy/grid2/profit_fixer.go +++ b/pkg/strategy/grid2/profit_fixer.go @@ -33,7 +33,7 @@ func (f *ProfitFixer) Fix(parent context.Context, since, until time.Time, initia defer log.Infof("profitFixer: done") - if profitStats.Since != nil && profitStats.Since.Before(since) { + if profitStats.Since != nil && !profitStats.Since.IsZero() && profitStats.Since.Before(since) { log.Infof("profitFixer: profitStats.since %s is ealier than the given since %s, setting since to %s", profitStats.Since, since, profitStats.Since) since = *profitStats.Since } From 0c72ac2386430cb54c24cdb62f15257aeacabab8 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 23:37:20 +0800 Subject: [PATCH 0815/1392] grid2: fix typo --- pkg/strategy/grid2/profit_fixer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/profit_fixer.go b/pkg/strategy/grid2/profit_fixer.go index 328934a182..156995d603 100644 --- a/pkg/strategy/grid2/profit_fixer.go +++ b/pkg/strategy/grid2/profit_fixer.go @@ -34,7 +34,7 @@ func (f *ProfitFixer) Fix(parent context.Context, since, until time.Time, initia defer log.Infof("profitFixer: done") if profitStats.Since != nil && !profitStats.Since.IsZero() && profitStats.Since.Before(since) { - log.Infof("profitFixer: profitStats.since %s is ealier than the given since %s, setting since to %s", profitStats.Since, since, profitStats.Since) + log.Infof("profitFixer: profitStats.since %s is earlier than the given since %s, setting since to %s", profitStats.Since, since, profitStats.Since) since = *profitStats.Since } From 46a6d896a269aa521c5804b1d5fb50b475085a33 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Apr 2023 23:48:02 +0800 Subject: [PATCH 0816/1392] grid2: improve the if err syntax --- pkg/strategy/grid2/profit_fixer.go | 16 ++++++++++++---- pkg/strategy/grid2/recover.go | 5 +++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/profit_fixer.go b/pkg/strategy/grid2/profit_fixer.go index 156995d603..1b315fe236 100644 --- a/pkg/strategy/grid2/profit_fixer.go +++ b/pkg/strategy/grid2/profit_fixer.go @@ -5,6 +5,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/exchange/batch" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -15,6 +16,8 @@ type ProfitFixer struct { symbol string grid *Grid historyService types.ExchangeTradeHistoryService + + logger logrus.FieldLogger } func newProfitFixer(grid *Grid, symbol string, historyService types.ExchangeTradeHistoryService) *ProfitFixer { @@ -22,19 +25,24 @@ func newProfitFixer(grid *Grid, symbol string, historyService types.ExchangeTrad symbol: symbol, grid: grid, historyService: historyService, + logger: logrus.StandardLogger(), } } +func (f *ProfitFixer) SetLogger(logger logrus.FieldLogger) { + f.logger = logger +} + // Fix fixes the total quote profit of the given grid func (f *ProfitFixer) Fix(parent context.Context, since, until time.Time, initialOrderID uint64, profitStats *GridProfitStats) error { // reset profit profitStats.TotalQuoteProfit = fixedpoint.Zero profitStats.ArbitrageCount = 0 - defer log.Infof("profitFixer: done") + defer f.logger.Infof("profitFixer: done") if profitStats.Since != nil && !profitStats.Since.IsZero() && profitStats.Since.Before(since) { - log.Infof("profitFixer: profitStats.since %s is earlier than the given since %s, setting since to %s", profitStats.Since, since, profitStats.Since) + f.logger.Infof("profitFixer: profitStats.since %s is earlier than the given since %s, setting since to %s", profitStats.Since, since, profitStats.Since) since = *profitStats.Since } @@ -45,7 +53,7 @@ func (f *ProfitFixer) Fix(parent context.Context, since, until time.Time, initia orderC, errC := q.Query(ctx, f.symbol, since, until, initialOrderID) defer func() { - log.Infof("profitFixer: fixed profitStats=%#v", profitStats) + f.logger.Infof("profitFixer: fixed profitStats=%#v", profitStats) }() for { @@ -91,7 +99,7 @@ func (f *ProfitFixer) Fix(parent context.Context, since, until time.Time, initia profitStats.TotalQuoteProfit = profitStats.TotalQuoteProfit.Add(quoteProfit) profitStats.ArbitrageCount++ - log.Debugf("profitFixer: filledSellOrder=%#v", order) + f.logger.Debugf("profitFixer: filledSellOrder=%#v", order) } } } diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index e6a1b90006..1fdcfa79b5 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -81,9 +81,10 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex } fixer := newProfitFixer(s.grid, s.Symbol, historyService) + fixer.SetLogger(s.logger) + // set initial order ID = 0 instead of s.GridProfitStats.InitialOrderID because the order ID could be incorrect - err := fixer.Fix(ctx, since, until, 0, s.GridProfitStats) - if err != nil { + if err := fixer.Fix(ctx, since, until, 0, s.GridProfitStats); err != nil { return err } From 32b2c431987778839ff3006aec7f86dc66f62c3f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 27 Apr 2023 00:33:42 +0800 Subject: [PATCH 0817/1392] grid2: emit grid profit after profit stats fix --- pkg/strategy/grid2/recover.go | 2 ++ pkg/strategy/grid2/strategy.go | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 1fdcfa79b5..fec69a2ef8 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -89,6 +89,8 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex } s.logger.Infof("fixed profitStats: %#v", s.GridProfitStats) + + s.EmitGridProfit(s.GridProfitStats, nil) } return nil diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2c6beacd54..bd6743009e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1852,7 +1852,9 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.orderExecutor = orderExecutor s.OnGridProfit(func(stats *GridProfitStats, profit *GridProfit) { - bbgo.Notify(profit) + if profit != nil { + bbgo.Notify(profit) + } bbgo.Notify(stats) }) From 5a901e929c2bbfe7a30e6db191a467fb9ff55c3f Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 28 Apr 2023 15:58:12 +0800 Subject: [PATCH 0818/1392] grid2: apply defensive programming on the order quantity --- pkg/strategy/grid2/strategy.go | 36 +++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index bd6743009e..827b127696 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -344,13 +344,32 @@ func (s *Strategy) calculateProfit(o types.Order, buyPrice, buyQuantity fixedpoi func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { tq := aggregateTradesQuantity(trades) - if tq.Compare(o.Quantity) != 0 { + // on MAX: if order.status == filled, it does not mean order.executedQuantity == order.quantity + // order.executedQuantity can be less than order.quantity + // so here we use executed quantity to check if the total trade quantity matches to order.executedQuantity + executedQuantity := o.ExecutedQuantity + if executedQuantity.IsZero() { + // fall back to the original quantity if the executed quantity is zero + executedQuantity = o.Quantity + } + + // early return here if it matches + c := tq.Compare(executedQuantity) + if c == 0 { + return true + } + + if c < 0 { s.logger.Warnf("order trades missing. expected: %s got: %s", o.Quantity.String(), tq.String()) return false + } else if c > 0 { + s.logger.Errorf("aggregated trade quantity > order.quantity, something is wrong, please check") + return true } + // shouldn't reach here return true } @@ -408,9 +427,20 @@ func (s *Strategy) processFilledOrder(o types.Order) { // check order fee newSide := types.SideTypeSell newPrice := o.Price - newQuantity := o.Quantity + + executedQuantity := o.ExecutedQuantity + // A safeguard check, fallback to the original quantity + if executedQuantity.IsZero() { + executedQuantity = o.Quantity + } + + newQuantity := executedQuantity executedPrice := o.Price + if o.ExecutedQuantity.Compare(o.Quantity) != 0 { + s.logger.Warnf("order executed quantity %s != order quantity %s, something is wrong", o.ExecutedQuantity, o.Quantity) + } + /* if o.AveragePrice.Sign() > 0 { executedPrice = o.AveragePrice @@ -418,7 +448,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { */ // will be used for calculating quantity - orderExecutedQuoteAmount := o.Quantity.Mul(executedPrice) + orderExecutedQuoteAmount := executedQuantity.Mul(executedPrice) // collect trades for fee // fee calculation is used to reduce the order quantity From 717de67d5abc0b612d7628135e746dd7111dc19a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 28 Apr 2023 16:07:03 +0800 Subject: [PATCH 0819/1392] grid2: improve log and try best to return the order fee --- pkg/strategy/grid2/strategy.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 827b127696..b2c5c7b6dc 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -361,11 +361,11 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { if c < 0 { s.logger.Warnf("order trades missing. expected: %s got: %s", - o.Quantity.String(), + executedQuantity.String(), tq.String()) return false } else if c > 0 { - s.logger.Errorf("aggregated trade quantity > order.quantity, something is wrong, please check") + s.logger.Errorf("aggregated trade quantity %s > order executed quantity %s, something is wrong, please check", tq.String(), executedQuantity.String()) return true } @@ -379,7 +379,7 @@ func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { // try to get the received trades (websocket trades) orderTrades := s.historicalTrades.GetOrderTrades(o) if len(orderTrades) > 0 { - s.logger.Infof("found filled order trades: %+v", orderTrades) + s.logger.Infof("GRID: found filled order trades: %+v", orderTrades) } feeCurrency := s.Market.BaseCurrency @@ -403,7 +403,7 @@ func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { return fixedpoint.Zero, feeCurrency } - s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") + s.logger.Warnf("GRID: missing #%d order trades or missing trade fee, pulling order trades from API", o.OrderID) // if orderQueryService is supported, use it to query the trades of the filled order apiOrderTrades, err := s.orderQueryService.QueryOrderTrades(context.Background(), types.OrderQuery{ @@ -411,13 +411,21 @@ func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { OrderID: strconv.FormatUint(o.OrderID, 10), }) if err != nil { - s.logger.WithError(err).Errorf("query order trades error") + s.logger.WithError(err).Errorf("query #%d order trades error", o.OrderID) } else { - s.logger.Infof("fetched api trades: %+v", apiOrderTrades) + s.logger.Infof("GRID: fetched api #%d order trades: %+v", o.OrderID, apiOrderTrades) orderTrades = apiOrderTrades } } + // still try to aggregate the trades quantity if we can: + if len(orderTrades) > 0 { + fees := collectTradeFee(orderTrades) + if fee, ok := fees[feeCurrency]; ok { + return fee, feeCurrency + } + } + return fixedpoint.Zero, feeCurrency } From f958120fb5a8401c3b6a2a3bae089dd411e7e435 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 28 Apr 2023 16:12:57 +0800 Subject: [PATCH 0820/1392] grid2: remove the len check since we just iterate --- pkg/strategy/grid2/strategy.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b2c5c7b6dc..216929fa7b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -419,11 +419,9 @@ func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { } // still try to aggregate the trades quantity if we can: - if len(orderTrades) > 0 { - fees := collectTradeFee(orderTrades) - if fee, ok := fees[feeCurrency]; ok { - return fee, feeCurrency - } + fees := collectTradeFee(orderTrades) + if fee, ok := fees[feeCurrency]; ok { + return fee, feeCurrency } return fixedpoint.Zero, feeCurrency From 829edeb401e29a95f4d791ea8ef57c055a4cfa97 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 28 Apr 2023 16:16:23 +0800 Subject: [PATCH 0821/1392] grid2: improve warning message --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 216929fa7b..6192c63ddc 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -444,7 +444,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { executedPrice := o.Price if o.ExecutedQuantity.Compare(o.Quantity) != 0 { - s.logger.Warnf("order executed quantity %s != order quantity %s, something is wrong", o.ExecutedQuantity, o.Quantity) + s.logger.Warnf("order #%d is filled, but order executed quantity %s != order quantity %s, something is wrong", o.OrderID, o.ExecutedQuantity, o.Quantity) } /* From 38a7f18b3272c78b806998e4b870a50ce39ef14f Mon Sep 17 00:00:00 2001 From: ricotoothless Date: Sat, 29 Apr 2023 01:59:01 +0800 Subject: [PATCH 0822/1392] FIX: supertrend config type error --- config/supertrend.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 5eb57087cc..e51af4c98a 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -90,7 +90,7 @@ exchangeStrategies: closePosition: 100% - higherHighLowerLowStopLoss: # interval is the kline interval used by this exit - interval: 15 + interval: 15m # window is used as the range to determining higher highs and lower lows window: 5 # highLowWindow is the range to calculate the number of higher highs and lower lows From b8f599c8c321b22f2b611bfb72852fe25cc86ab1 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 May 2023 13:53:20 +0800 Subject: [PATCH 0823/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index 91ebbfd821..42d672c319 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -60,4 +60,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 2374db5e3c..c953a66713 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -43,4 +43,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index ca859ad1b7..7da5698553 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -52,4 +52,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index 4ba2f64a01..d906870f13 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -42,4 +42,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index fc6080b076..5ab8436544 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -41,4 +41,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index c7c57c33f8..6c2c94ddd4 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -51,4 +51,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index 697f6b84d0..45cbc970b0 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -43,4 +43,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 1361793a4b..692e6f45c4 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -50,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index a9f7fbcef4..2977e92976 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -44,4 +44,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index 38d45b14df..1492b6e8ca 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -47,4 +47,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index a6f0911ecf..48348add62 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -44,4 +44,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index 2be614ea7a..4051b2c713 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -43,4 +43,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index d217693c58..f7f9cb7363 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -40,4 +40,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index 52a7a9b50e..786d1b45e4 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -43,4 +43,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 8b5656c629..4e77b1b754 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -43,4 +43,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index ab62182f45..105d7e150d 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -43,4 +43,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index 90d3321428..c5f9259812 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -42,4 +42,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index ef920d7e27..aa157f8b79 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -46,4 +46,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index da8a3cefde..48793d720c 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -44,4 +44,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index 523e6f842c..5bac1ea81e 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -42,4 +42,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index ef4579edd4..a384086592 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -51,4 +51,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 93a34a0a08..042a5e5e06 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -53,4 +53,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 1a2f0df0d3..98e0d7b11b 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -48,4 +48,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index 0cf5d7bcf5..99a74fb914 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -44,4 +44,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index c6346d2963..96f165273d 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -44,4 +44,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 5d727619c2..4b2509fce5 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -42,4 +42,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 4218f97209..48190176b7 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -44,4 +44,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index b7e90fbed9..472ab06c78 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -42,4 +42,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index 0125e8a218..3d6779e5c8 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -41,4 +41,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Apr-2023 +###### Auto generated by spf13/cobra on 4-May-2023 From b31a2994dee90cfb731f615eb8b231c63e9c3c2a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 May 2023 13:53:20 +0800 Subject: [PATCH 0824/1392] bump version to v1.47.0 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 83b3cfbf76..683a8beb67 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.46.0-a06ed9e5-dev" +const Version = "v1.47.0-30d82b1e-dev" -const VersionGitRef = "a06ed9e5" +const VersionGitRef = "30d82b1e" diff --git a/pkg/version/version.go b/pkg/version/version.go index 4a2adebc2b..0c978dba98 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.46.0-a06ed9e5" +const Version = "v1.47.0-30d82b1e" -const VersionGitRef = "a06ed9e5" +const VersionGitRef = "30d82b1e" From f686a6de83a64be437c4c638ee8e5327705a6b51 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 May 2023 13:53:20 +0800 Subject: [PATCH 0825/1392] add v1.47.0 release note --- doc/release/v1.47.0.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 doc/release/v1.47.0.md diff --git a/doc/release/v1.47.0.md b/doc/release/v1.47.0.md new file mode 100644 index 0000000000..f339e02497 --- /dev/null +++ b/doc/release/v1.47.0.md @@ -0,0 +1,11 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.46.0...main) + + - [#1165](https://github.com/c9s/bbgo/pull/1165): FIX: [supertrend] fix config type error + - [#1164](https://github.com/c9s/bbgo/pull/1164): FIX: [grid2] apply defensive programming on the order quantity + - [#1162](https://github.com/c9s/bbgo/pull/1162): FIX: [grid2] emit grid profit after profit stats fix + - [#1158](https://github.com/c9s/bbgo/pull/1158): IMPROVE: types: improve order struct json size + - [#1161](https://github.com/c9s/bbgo/pull/1161): FIX: [grid2] add initialOrderID check and try to fix profitStats.Since + - [#1160](https://github.com/c9s/bbgo/pull/1160): FIX: [grid2] add profit fixer and options + - [#1159](https://github.com/c9s/bbgo/pull/1159): FIX: avoid global persistenceServiceFacade concurrent write + - [#1157](https://github.com/c9s/bbgo/pull/1157): FIX: add context to LoadState + - [#1155](https://github.com/c9s/bbgo/pull/1155): FIX: [supertrend] adding opposite position amount to the order amount From 70e3f8ec5fd2b936a3c9e115f7d83561beb7a563 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 May 2023 14:37:19 +0800 Subject: [PATCH 0826/1392] max: split v3 api into files --- pkg/exchange/max/exchange.go | 36 +++---- .../max/maxapi/v3/cancel_order_request.go | 8 +- .../v3/cancel_wallet_order_all_request.go | 2 +- .../maxapi/v3/create_wallet_order_request.go | 4 +- .../maxapi/v3/get_margin_ad_ratio_request.go | 8 +- .../get_margin_liquidation_history_request.go | 38 +++++++ .../v3/get_margin_loan_history_request.go | 33 +++++++ .../get_margin_repayment_history_request.go | 30 ++++++ .../max/maxapi/v3/get_order_request.go | 2 +- .../max/maxapi/v3/get_order_trades_request.go | 2 +- .../maxapi/v3/get_wallet_accounts_request.go | 2 +- .../v3/get_wallet_open_orders_request.go | 2 +- .../v3/get_wallet_order_history_request.go | 2 +- .../maxapi/v3/get_wallet_trades_request.go | 2 +- pkg/exchange/max/maxapi/v3/margin.go | 99 +------------------ .../max/maxapi/v3/margin_loan_request.go | 18 ++++ .../max/maxapi/v3/margin_repay_request.go | 18 ++++ pkg/exchange/max/maxapi/v3/order.go | 7 +- 18 files changed, 176 insertions(+), 137 deletions(-) create mode 100644 pkg/exchange/max/maxapi/v3/get_margin_liquidation_history_request.go create mode 100644 pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go create mode 100644 pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request.go create mode 100644 pkg/exchange/max/maxapi/v3/margin_loan_request.go create mode 100644 pkg/exchange/max/maxapi/v3/margin_repay_request.go diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index a2fac55be0..70d6ddcafc 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -28,7 +28,7 @@ type Exchange struct { key, secret string client *maxapi.RestClient - v3order *v3.OrderService + v3client *v3.Client v3margin *v3.MarginService submitOrderLimiter, queryTradeLimiter, accountQueryLimiter, closedOrderQueryLimiter, marketDataLimiter *rate.Limiter @@ -47,7 +47,7 @@ func New(key, secret string) *Exchange { key: key, // pragma: allowlist nextline secret secret: secret, - v3order: &v3.OrderService{Client: client}, + v3client: &v3.Client{Client: client}, v3margin: &v3.MarginService{Client: client}, queryTradeLimiter: rate.NewLimiter(rate.Every(1*time.Second), 2), @@ -182,7 +182,7 @@ func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([] return nil, err } - maxTrades, err := e.v3order.NewGetOrderTradesRequest().OrderID(uint64(orderID)).Do(ctx) + maxTrades, err := e.v3client.NewGetOrderTradesRequest().OrderID(uint64(orderID)).Do(ctx) if err != nil { return nil, err } @@ -218,7 +218,7 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O return nil, errors.New("max.QueryOrder: only accept one parameter of OrderID/ClientOrderID") } - request := e.v3order.NewGetOrderRequest() + request := e.v3client.NewGetOrderRequest() if len(q.OrderID) != 0 { orderID, err := strconv.ParseInt(q.OrderID, 10, 64) @@ -248,7 +248,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ walletType = maxapi.WalletTypeMargin } - maxOrders, err := e.v3order.NewGetWalletOpenOrdersRequest(walletType).Market(market).Do(ctx) + maxOrders, err := e.v3client.NewGetWalletOpenOrdersRequest(walletType).Market(market).Do(ctx) if err != nil { return orders, err } @@ -282,7 +282,7 @@ func (e *Exchange) queryClosedOrdersByLastOrderID(ctx context.Context, symbol st walletType = maxapi.WalletTypeMargin } - req := e.v3order.NewGetWalletOrderHistoryRequest(walletType).Market(market) + req := e.v3client.NewGetWalletOrderHistoryRequest(walletType).Market(market) if lastOrderID == 0 { lastOrderID = 1 } @@ -315,7 +315,7 @@ func (e *Exchange) CancelAllOrders(ctx context.Context) ([]types.Order, error) { walletType = maxapi.WalletTypeMargin } - req := e.v3order.NewCancelWalletOrderAllRequest(walletType) + req := e.v3client.NewCancelWalletOrderAllRequest(walletType) var orderResponses, err = req.Do(ctx) if err != nil { return nil, err @@ -338,7 +338,7 @@ func (e *Exchange) CancelOrdersBySymbol(ctx context.Context, symbol string) ([]t walletType = maxapi.WalletTypeMargin } - req := e.v3order.NewCancelWalletOrderAllRequest(walletType) + req := e.v3client.NewCancelWalletOrderAllRequest(walletType) req.Market(market) var orderResponses, err = req.Do(ctx) @@ -362,7 +362,7 @@ func (e *Exchange) CancelOrdersByGroupID(ctx context.Context, groupID uint32) ([ walletType = maxapi.WalletTypeMargin } - req := e.v3order.NewCancelWalletOrderAllRequest(walletType) + req := e.v3client.NewCancelWalletOrderAllRequest(walletType) req.GroupID(groupID) var orderResponses, err = req.Do(ctx) @@ -398,7 +398,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err if len(groupIDs) > 0 { for groupID := range groupIDs { - req := e.v3order.NewCancelWalletOrderAllRequest(walletType) + req := e.v3client.NewCancelWalletOrderAllRequest(walletType) req.GroupID(groupID) if _, err := req.Do(ctx); err != nil { @@ -409,7 +409,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err } for _, o := range orphanOrders { - req := e.v3order.NewCancelOrderRequest() + req := e.v3client.NewCancelOrderRequest() if o.OrderID > 0 { req.Id(o.OrderID) } else if len(o.ClientOrderID) > 0 && o.ClientOrderID != types.NoClientOrderID { @@ -498,7 +498,7 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (cr clientOrderID := NewClientOrderID(o.ClientOrderID) - req := e.v3order.NewCreateWalletOrderRequest(walletType) + req := e.v3client.NewCreateWalletOrderRequest(walletType) req.Market(toLocalSymbol(o.Symbol)). Side(toLocalSideType(o.Side)). Volume(quantityString). @@ -590,7 +590,7 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { if e.MarginSettings.IsMargin { a.AccountType = types.AccountTypeMargin - req := e.v3margin.NewGetMarginADRatioRequest() + req := e.v3client.NewGetMarginADRatioRequest() adRatio, err := req.Do(ctx) if err != nil { return a, err @@ -613,7 +613,7 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, walletType = maxapi.WalletTypeMargin } - req := e.v3order.NewGetWalletAccountsRequest(walletType) + req := e.v3client.NewGetWalletAccountsRequest(walletType) accounts, err := req.Do(ctx) if err != nil { return nil, err @@ -818,7 +818,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type walletType = maxapi.WalletTypeMargin } - req := e.v3order.NewGetWalletTradesRequest(walletType) + req := e.v3client.NewGetWalletTradesRequest(walletType) req.Market(market) if options.Limit > 0 { @@ -977,7 +977,7 @@ func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (fixedp } func (e *Exchange) RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { - req := e.v3margin.NewMarginRepayRequest() + req := e.v3client.NewMarginRepayRequest() req.Currency(toLocalCurrency(asset)) req.Amount(amount.String()) resp, err := req.Do(ctx) @@ -990,7 +990,7 @@ func (e *Exchange) RepayMarginAsset(ctx context.Context, asset string, amount fi } func (e *Exchange) BorrowMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { - req := e.v3margin.NewMarginLoanRequest() + req := e.v3client.NewMarginLoanRequest() req.Currency(toLocalCurrency(asset)) req.Amount(amount.String()) resp, err := req.Do(ctx) @@ -1003,7 +1003,7 @@ func (e *Exchange) BorrowMarginAsset(ctx context.Context, asset string, amount f } func (e *Exchange) QueryMarginAssetMaxBorrowable(ctx context.Context, asset string) (amount fixedpoint.Value, err error) { - req := e.v3margin.NewGetMarginBorrowingLimitsRequest() + req := e.v3client.NewGetMarginBorrowingLimitsRequest() resp, err := req.Do(ctx) if err != nil { return fixedpoint.Zero, err diff --git a/pkg/exchange/max/maxapi/v3/cancel_order_request.go b/pkg/exchange/max/maxapi/v3/cancel_order_request.go index 67bbaa52c8..875759f93b 100644 --- a/pkg/exchange/max/maxapi/v3/cancel_order_request.go +++ b/pkg/exchange/max/maxapi/v3/cancel_order_request.go @@ -6,10 +6,6 @@ package v3 import "github.com/c9s/requestgen" -func (s *OrderService) NewCancelOrderRequest() *CancelOrderRequest { - return &CancelOrderRequest{client: s.Client} -} - //go:generate DeleteRequest -url "/api/v3/order" -type CancelOrderRequest -responseType .Order type CancelOrderRequest struct { client requestgen.AuthenticatedAPIClient @@ -17,3 +13,7 @@ type CancelOrderRequest struct { id *uint64 `param:"id,omitempty"` clientOrderID *string `param:"client_oid,omitempty"` } + +func (s *Client) NewCancelOrderRequest() *CancelOrderRequest { + return &CancelOrderRequest{client: s.Client} +} diff --git a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go index f8608e79bc..773d10024d 100644 --- a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go +++ b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go @@ -11,7 +11,7 @@ type OrderCancelResponse struct { Error *string } -func (s *OrderService) NewCancelWalletOrderAllRequest(walletType WalletType) *CancelWalletOrderAllRequest { +func (s *Client) NewCancelWalletOrderAllRequest(walletType WalletType) *CancelWalletOrderAllRequest { return &CancelWalletOrderAllRequest{client: s.Client, walletType: walletType} } diff --git a/pkg/exchange/max/maxapi/v3/create_wallet_order_request.go b/pkg/exchange/max/maxapi/v3/create_wallet_order_request.go index 1c9c589912..762a007c2a 100644 --- a/pkg/exchange/max/maxapi/v3/create_wallet_order_request.go +++ b/pkg/exchange/max/maxapi/v3/create_wallet_order_request.go @@ -14,7 +14,7 @@ type CreateWalletOrderRequest struct { market string `param:"market,required"` side string `param:"side,required"` volume string `param:"volume,required"` - orderType OrderType `param:"ord_type"` + orderType OrderType `param:"ord_type"` price *string `param:"price"` stopPrice *string `param:"stop_price"` @@ -22,6 +22,6 @@ type CreateWalletOrderRequest struct { groupID *string `param:"group_id"` } -func (s *OrderService) NewCreateWalletOrderRequest(walletType WalletType) *CreateWalletOrderRequest { +func (s *Client) NewCreateWalletOrderRequest(walletType WalletType) *CreateWalletOrderRequest { return &CreateWalletOrderRequest{client: s.Client, walletType: walletType} } diff --git a/pkg/exchange/max/maxapi/v3/get_margin_ad_ratio_request.go b/pkg/exchange/max/maxapi/v3/get_margin_ad_ratio_request.go index f01cc7c2bb..c851bda19d 100644 --- a/pkg/exchange/max/maxapi/v3/get_margin_ad_ratio_request.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_ad_ratio_request.go @@ -10,10 +10,6 @@ import ( //go:generate -command PostRequest requestgen -method POST //go:generate -command DeleteRequest requestgen -method DELETE -func (s *MarginService) NewGetMarginADRatioRequest() *GetMarginADRatioRequest { - return &GetMarginADRatioRequest{client: s.Client} -} - type ADRatio struct { AdRatio fixedpoint.Value `json:"ad_ratio"` AssetInUsdt fixedpoint.Value `json:"asset_in_usdt"` @@ -24,3 +20,7 @@ type ADRatio struct { type GetMarginADRatioRequest struct { client requestgen.AuthenticatedAPIClient } + +func (s *Client) NewGetMarginADRatioRequest() *GetMarginADRatioRequest { + return &GetMarginADRatioRequest{client: s.Client} +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_liquidation_history_request.go b/pkg/exchange/max/maxapi/v3/get_margin_liquidation_history_request.go new file mode 100644 index 0000000000..bdcbcedea9 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_liquidation_history_request.go @@ -0,0 +1,38 @@ +package v3 + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func (s *Client) NewGetMarginLiquidationHistoryRequest() *GetMarginLiquidationHistoryRequest { + return &GetMarginLiquidationHistoryRequest{client: s.Client} +} + +type LiquidationRecord struct { + SN string `json:"sn"` + AdRatio fixedpoint.Value `json:"ad_ratio"` + ExpectedAdRatio fixedpoint.Value `json:"expected_ad_ratio"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + State LiquidationState `json:"state"` +} + +type LiquidationState string + +const ( + LiquidationStateProcessing LiquidationState = "processing" + LiquidationStateDebt LiquidationState = "debt" + LiquidationStateLiquidated LiquidationState = "liquidated" +) + +//go:generate GetRequest -url "/api/v3/wallet/m/liquidations" -type GetMarginLiquidationHistoryRequest -responseType []LiquidationRecord +type GetMarginLiquidationHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + limit *int `param:"limit"` +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go b/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go new file mode 100644 index 0000000000..f4589d4c1a --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func (s *Client) NewGetMarginLoanHistoryRequest() *GetMarginLoanHistoryRequest { + return &GetMarginLoanHistoryRequest{client: s.Client} +} + +type LoanRecord struct { + SN string `json:"sn"` + Currency string `json:"currency"` + Amount fixedpoint.Value `json:"amount"` + State string `json:"state"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + InterestRate fixedpoint.Value `json:"interest_rate"` +} + +//go:generate GetRequest -url "/api/v3/wallet/m/loans/:currency" -type GetMarginLoanHistoryRequest -responseType []LoanRecord +type GetMarginLoanHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + currency string `param:"currency,slug,required"` + + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + limit *int `param:"limit"` +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request.go b/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request.go new file mode 100644 index 0000000000..f2af0374a6 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request.go @@ -0,0 +1,30 @@ +package v3 + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type RepaymentRecord struct { + SN string `json:"sn"` + Currency string `json:"currency"` + Amount fixedpoint.Value `json:"amount"` + Principal fixedpoint.Value `json:"principal"` + Interest fixedpoint.Value `json:"interest"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + State string `json:"state"` +} + +//go:generate GetRequest -url "/api/v3/wallet/m/repayments/:currency" -type GetMarginRepaymentHistoryRequest -responseType []RepaymentRecord +type GetMarginRepaymentHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + currency string `param:"currency,slug,required"` + + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + limit *int `param:"limit"` +} diff --git a/pkg/exchange/max/maxapi/v3/get_order_request.go b/pkg/exchange/max/maxapi/v3/get_order_request.go index 94de554210..ca90c34ea4 100644 --- a/pkg/exchange/max/maxapi/v3/get_order_request.go +++ b/pkg/exchange/max/maxapi/v3/get_order_request.go @@ -6,7 +6,7 @@ package v3 import "github.com/c9s/requestgen" -func (s *OrderService) NewGetOrderRequest() *GetOrderRequest { +func (s *Client) NewGetOrderRequest() *GetOrderRequest { return &GetOrderRequest{client: s.Client} } diff --git a/pkg/exchange/max/maxapi/v3/get_order_trades_request.go b/pkg/exchange/max/maxapi/v3/get_order_trades_request.go index 4ab982b02f..a734dae614 100644 --- a/pkg/exchange/max/maxapi/v3/get_order_trades_request.go +++ b/pkg/exchange/max/maxapi/v3/get_order_trades_request.go @@ -6,7 +6,7 @@ package v3 import "github.com/c9s/requestgen" -func (s *OrderService) NewGetOrderTradesRequest() *GetOrderTradesRequest { +func (s *Client) NewGetOrderTradesRequest() *GetOrderTradesRequest { return &GetOrderTradesRequest{client: s.Client} } diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go index 20b2ebdd8c..37479cd1bc 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go @@ -6,7 +6,7 @@ import "github.com/c9s/requestgen" //go:generate -command PostRequest requestgen -method POST //go:generate -command DeleteRequest requestgen -method DELETE -func (s *OrderService) NewGetWalletAccountsRequest(walletType WalletType) *GetWalletAccountsRequest { +func (s *Client) NewGetWalletAccountsRequest(walletType WalletType) *GetWalletAccountsRequest { return &GetWalletAccountsRequest{client: s.Client, walletType: walletType} } diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_open_orders_request.go b/pkg/exchange/max/maxapi/v3/get_wallet_open_orders_request.go index 3841becabc..b909f04917 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_open_orders_request.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_open_orders_request.go @@ -6,7 +6,7 @@ import "github.com/c9s/requestgen" //go:generate -command PostRequest requestgen -method POST //go:generate -command DeleteRequest requestgen -method DELETE -func (s *OrderService) NewGetWalletOpenOrdersRequest(walletType WalletType) *GetWalletOpenOrdersRequest { +func (s *Client) NewGetWalletOpenOrdersRequest(walletType WalletType) *GetWalletOpenOrdersRequest { return &GetWalletOpenOrdersRequest{client: s.Client, walletType: walletType} } diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_order_history_request.go b/pkg/exchange/max/maxapi/v3/get_wallet_order_history_request.go index cf6f804575..fe8b538d63 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_order_history_request.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_order_history_request.go @@ -6,7 +6,7 @@ import "github.com/c9s/requestgen" //go:generate -command PostRequest requestgen -method POST //go:generate -command DeleteRequest requestgen -method DELETE -func (s *OrderService) NewGetWalletOrderHistoryRequest(walletType WalletType) *GetWalletOrderHistoryRequest { +func (s *Client) NewGetWalletOrderHistoryRequest(walletType WalletType) *GetWalletOrderHistoryRequest { return &GetWalletOrderHistoryRequest{client: s.Client, walletType: walletType} } diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_trades_request.go b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request.go index e4804a7c14..46acf4dfc0 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_trades_request.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request.go @@ -10,7 +10,7 @@ import ( "github.com/c9s/requestgen" ) -func (s *OrderService) NewGetWalletTradesRequest(walletType WalletType) *GetWalletTradesRequest { +func (s *Client) NewGetWalletTradesRequest(walletType WalletType) *GetWalletTradesRequest { return &GetWalletTradesRequest{client: s.Client, walletType: walletType} } diff --git a/pkg/exchange/max/maxapi/v3/margin.go b/pkg/exchange/max/maxapi/v3/margin.go index e69422a268..863cb8c065 100644 --- a/pkg/exchange/max/maxapi/v3/margin.go +++ b/pkg/exchange/max/maxapi/v3/margin.go @@ -18,34 +18,18 @@ type MarginService struct { Client *maxapi.RestClient } -func (s *MarginService) NewGetMarginInterestRatesRequest() *GetMarginInterestRatesRequest { +func (s *Client) NewGetMarginInterestRatesRequest() *GetMarginInterestRatesRequest { return &GetMarginInterestRatesRequest{client: s.Client} } -func (s *MarginService) NewGetMarginBorrowingLimitsRequest() *GetMarginBorrowingLimitsRequest { +func (s *Client) NewGetMarginBorrowingLimitsRequest() *GetMarginBorrowingLimitsRequest { return &GetMarginBorrowingLimitsRequest{client: s.Client} } -func (s *MarginService) NewGetMarginInterestHistoryRequest(currency string) *GetMarginInterestHistoryRequest { +func (s *Client) NewGetMarginInterestHistoryRequest(currency string) *GetMarginInterestHistoryRequest { return &GetMarginInterestHistoryRequest{client: s.Client, currency: currency} } -func (s *MarginService) NewGetMarginLiquidationHistoryRequest() *GetMarginLiquidationHistoryRequest { - return &GetMarginLiquidationHistoryRequest{client: s.Client} -} - -func (s *MarginService) NewGetMarginLoanHistoryRequest() *GetMarginLoanHistoryRequest { - return &GetMarginLoanHistoryRequest{client: s.Client} -} - -func (s *MarginService) NewMarginRepayRequest() *MarginRepayRequest { - return &MarginRepayRequest{client: s.Client} -} - -func (s *MarginService) NewMarginLoanRequest() *MarginLoanRequest { - return &MarginLoanRequest{client: s.Client} -} - type MarginInterestRate struct { HourlyInterestRate fixedpoint.Value `json:"hourly_interest_rate"` NextHourlyInterestRate fixedpoint.Value `json:"next_hourly_interest_rate"` @@ -81,80 +65,3 @@ type GetMarginInterestHistoryRequest struct { endTime *time.Time `param:"endTime,milliseconds"` limit *int `param:"limit"` } - -type LiquidationRecord struct { - SN string `json:"sn"` - AdRatio fixedpoint.Value `json:"ad_ratio"` - ExpectedAdRatio fixedpoint.Value `json:"expected_ad_ratio"` - CreatedAt types.MillisecondTimestamp `json:"created_at"` - State LiquidationState `json:"state"` -} - -type LiquidationState string - -const ( - LiquidationStateProcessing LiquidationState = "processing" - LiquidationStateDebt LiquidationState = "debt" - LiquidationStateLiquidated LiquidationState = "liquidated" -) - -//go:generate GetRequest -url "/api/v3/wallet/m/liquidations" -type GetMarginLiquidationHistoryRequest -responseType []LiquidationRecord -type GetMarginLiquidationHistoryRequest struct { - client requestgen.AuthenticatedAPIClient - startTime *time.Time `param:"startTime,milliseconds"` - endTime *time.Time `param:"endTime,milliseconds"` - limit *int `param:"limit"` -} - -type RepaymentRecord struct { - SN string `json:"sn"` - Currency string `json:"currency"` - Amount fixedpoint.Value `json:"amount"` - Principal fixedpoint.Value `json:"principal"` - Interest fixedpoint.Value `json:"interest"` - CreatedAt types.MillisecondTimestamp `json:"created_at"` - State string `json:"state"` -} - -//go:generate GetRequest -url "/api/v3/wallet/m/repayments/:currency" -type GetMarginRepaymentHistoryRequest -responseType []RepaymentRecord -type GetMarginRepaymentHistoryRequest struct { - client requestgen.AuthenticatedAPIClient - currency string `param:"currency,slug,required"` - - startTime *time.Time `param:"startTime,milliseconds"` - endTime *time.Time `param:"endTime,milliseconds"` - limit *int `param:"limit"` -} - -type LoanRecord struct { - SN string `json:"sn"` - Currency string `json:"currency"` - Amount fixedpoint.Value `json:"amount"` - State string `json:"state"` - CreatedAt types.MillisecondTimestamp `json:"created_at"` - InterestRate fixedpoint.Value `json:"interest_rate"` -} - -//go:generate GetRequest -url "/api/v3/wallet/m/loans/:currency" -type GetMarginLoanHistoryRequest -responseType []LoanRecord -type GetMarginLoanHistoryRequest struct { - client requestgen.AuthenticatedAPIClient - currency string `param:"currency,slug,required"` - - startTime *time.Time `param:"startTime,milliseconds"` - endTime *time.Time `param:"endTime,milliseconds"` - limit *int `param:"limit"` -} - -//go:generate PostRequest -url "/api/v3/wallet/m/loan/:currency" -type MarginLoanRequest -responseType .LoanRecord -type MarginLoanRequest struct { - client requestgen.AuthenticatedAPIClient - currency string `param:"currency,slug,required"` - amount string `param:"amount"` -} - -//go:generate PostRequest -url "/api/v3/wallet/m/repayment/:currency" -type MarginRepayRequest -responseType .RepaymentRecord -type MarginRepayRequest struct { - client requestgen.AuthenticatedAPIClient - currency string `param:"currency,slug,required"` - amount string `param:"amount"` -} diff --git a/pkg/exchange/max/maxapi/v3/margin_loan_request.go b/pkg/exchange/max/maxapi/v3/margin_loan_request.go new file mode 100644 index 0000000000..5738e119e9 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/margin_loan_request.go @@ -0,0 +1,18 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import "github.com/c9s/requestgen" + +func (s *Client) NewMarginLoanRequest() *MarginLoanRequest { + return &MarginLoanRequest{client: s.Client} +} + +//go:generate PostRequest -url "/api/v3/wallet/m/loan/:currency" -type MarginLoanRequest -responseType .LoanRecord +type MarginLoanRequest struct { + client requestgen.AuthenticatedAPIClient + currency string `param:"currency,slug,required"` + amount string `param:"amount"` +} diff --git a/pkg/exchange/max/maxapi/v3/margin_repay_request.go b/pkg/exchange/max/maxapi/v3/margin_repay_request.go new file mode 100644 index 0000000000..2cc7501152 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/margin_repay_request.go @@ -0,0 +1,18 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import "github.com/c9s/requestgen" + +func (s *Client) NewMarginRepayRequest() *MarginRepayRequest { + return &MarginRepayRequest{client: s.Client} +} + +//go:generate PostRequest -url "/api/v3/wallet/m/repayment/:currency" -type MarginRepayRequest -responseType .RepaymentRecord +type MarginRepayRequest struct { + client requestgen.AuthenticatedAPIClient + currency string `param:"currency,slug,required"` + amount string `param:"amount"` +} diff --git a/pkg/exchange/max/maxapi/v3/order.go b/pkg/exchange/max/maxapi/v3/order.go index 22486c112e..4244857b6f 100644 --- a/pkg/exchange/max/maxapi/v3/order.go +++ b/pkg/exchange/max/maxapi/v3/order.go @@ -1,9 +1,5 @@ package v3 -//go:generate -command GetRequest requestgen -method GET -//go:generate -command PostRequest requestgen -method POST -//go:generate -command DeleteRequest requestgen -method DELETE - import ( "github.com/c9s/requestgen" @@ -17,7 +13,6 @@ type OrderType = maxapi.OrderType type Order = maxapi.Order type Account = maxapi.Account -// OrderService manages the Order endpoint. -type OrderService struct { +type Client struct { Client requestgen.AuthenticatedAPIClient } From 2a462c8e32f4c2208438ab3ca4ed8a7c03bed440 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 May 2023 14:43:19 +0800 Subject: [PATCH 0827/1392] maxapi: update margin repay/load apis --- .../v3/get_margin_loan_history_request.go | 11 +++-- ..._margin_loan_history_request_requestgen.go | 42 ++++++++----------- .../get_margin_repayment_history_request.go | 10 +++-- ...in_repayment_history_request_requestgen.go | 42 ++++++++----------- .../max/maxapi/v3/margin_loan_request.go | 7 ++-- .../v3/margin_loan_request_requestgen.go | 40 ++++++++---------- .../max/maxapi/v3/margin_repay_request.go | 7 ++-- .../v3/margin_repay_request_requestgen.go | 40 ++++++++---------- 8 files changed, 93 insertions(+), 106 deletions(-) diff --git a/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go b/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go index f4589d4c1a..2904ecc12b 100644 --- a/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go @@ -1,5 +1,9 @@ package v3 +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + import ( "time" @@ -22,10 +26,11 @@ type LoanRecord struct { InterestRate fixedpoint.Value `json:"interest_rate"` } -//go:generate GetRequest -url "/api/v3/wallet/m/loans/:currency" -type GetMarginLoanHistoryRequest -responseType []LoanRecord +//go:generate GetRequest -url "/api/v3/wallet/m/loans" -type GetMarginLoanHistoryRequest -responseType []LoanRecord type GetMarginLoanHistoryRequest struct { - client requestgen.AuthenticatedAPIClient - currency string `param:"currency,slug,required"` + client requestgen.AuthenticatedAPIClient + + currency string `param:"currency,required"` startTime *time.Time `param:"startTime,milliseconds"` endTime *time.Time `param:"endTime,milliseconds"` diff --git a/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request_requestgen.go index e0ca63db00..b4efb53601 100644 --- a/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method GET -url /api/v3/wallet/m/loans/:currency -type GetMarginLoanHistoryRequest -responseType []LoanRecord"; DO NOT EDIT. +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/loans -type GetMarginLoanHistoryRequest -responseType []LoanRecord"; DO NOT EDIT. package v3 @@ -13,6 +13,11 @@ import ( "time" ) +func (g *GetMarginLoanHistoryRequest) Currency(currency string) *GetMarginLoanHistoryRequest { + g.currency = currency + return g +} + func (g *GetMarginLoanHistoryRequest) StartTime(startTime time.Time) *GetMarginLoanHistoryRequest { g.startTime = &startTime return g @@ -28,11 +33,6 @@ func (g *GetMarginLoanHistoryRequest) Limit(limit int) *GetMarginLoanHistoryRequ return g } -func (g *GetMarginLoanHistoryRequest) Currency(currency string) *GetMarginLoanHistoryRequest { - g.currency = currency - return g -} - // GetQueryParameters builds and checks the query parameters and returns url.Values func (g *GetMarginLoanHistoryRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} @@ -48,6 +48,17 @@ func (g *GetMarginLoanHistoryRequest) GetQueryParameters() (url.Values, error) { // GetParameters builds and checks the parameters and return the result in a map object func (g *GetMarginLoanHistoryRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} + // check currency field -> json key currency + currency := g.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency // check startTime field -> json key startTime if g.startTime != nil { startTime := *g.startTime @@ -113,17 +124,6 @@ func (g *GetMarginLoanHistoryRequest) GetParametersJSON() ([]byte, error) { // GetSlugParameters builds and checks the slug parameters and return the result in a map object func (g *GetMarginLoanHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} - // check currency field -> json key currency - currency := g.currency - - // TEMPLATE check-required - if len(currency) == 0 { - return nil, fmt.Errorf("currency is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of currency - params["currency"] = currency return params, nil } @@ -177,13 +177,7 @@ func (g *GetMarginLoanHistoryRequest) Do(ctx context.Context) ([]LoanRecord, err return nil, err } - apiURL := "/api/v3/wallet/m/loans/:currency" - slugs, err := g.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = g.applySlugsToUrl(apiURL, slugs) + apiURL := "/api/v3/wallet/m/loans" req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) if err != nil { diff --git a/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request.go b/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request.go index f2af0374a6..5afec3d38e 100644 --- a/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request.go @@ -1,5 +1,9 @@ package v3 +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + import ( "time" @@ -19,11 +23,11 @@ type RepaymentRecord struct { State string `json:"state"` } -//go:generate GetRequest -url "/api/v3/wallet/m/repayments/:currency" -type GetMarginRepaymentHistoryRequest -responseType []RepaymentRecord +//go:generate GetRequest -url "/api/v3/wallet/m/repayments" -type GetMarginRepaymentHistoryRequest -responseType []RepaymentRecord type GetMarginRepaymentHistoryRequest struct { - client requestgen.AuthenticatedAPIClient - currency string `param:"currency,slug,required"` + client requestgen.AuthenticatedAPIClient + currency string `param:"currency,required"` startTime *time.Time `param:"startTime,milliseconds"` endTime *time.Time `param:"endTime,milliseconds"` limit *int `param:"limit"` diff --git a/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request_requestgen.go index 83edcd7fdb..ce441cf3e8 100644 --- a/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method GET -url /api/v3/wallet/m/repayments/:currency -type GetMarginRepaymentHistoryRequest -responseType []RepaymentRecord"; DO NOT EDIT. +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/repayments -type GetMarginRepaymentHistoryRequest -responseType []RepaymentRecord"; DO NOT EDIT. package v3 @@ -13,6 +13,11 @@ import ( "time" ) +func (g *GetMarginRepaymentHistoryRequest) Currency(currency string) *GetMarginRepaymentHistoryRequest { + g.currency = currency + return g +} + func (g *GetMarginRepaymentHistoryRequest) StartTime(startTime time.Time) *GetMarginRepaymentHistoryRequest { g.startTime = &startTime return g @@ -28,11 +33,6 @@ func (g *GetMarginRepaymentHistoryRequest) Limit(limit int) *GetMarginRepaymentH return g } -func (g *GetMarginRepaymentHistoryRequest) Currency(currency string) *GetMarginRepaymentHistoryRequest { - g.currency = currency - return g -} - // GetQueryParameters builds and checks the query parameters and returns url.Values func (g *GetMarginRepaymentHistoryRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} @@ -48,6 +48,17 @@ func (g *GetMarginRepaymentHistoryRequest) GetQueryParameters() (url.Values, err // GetParameters builds and checks the parameters and return the result in a map object func (g *GetMarginRepaymentHistoryRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} + // check currency field -> json key currency + currency := g.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency // check startTime field -> json key startTime if g.startTime != nil { startTime := *g.startTime @@ -113,17 +124,6 @@ func (g *GetMarginRepaymentHistoryRequest) GetParametersJSON() ([]byte, error) { // GetSlugParameters builds and checks the slug parameters and return the result in a map object func (g *GetMarginRepaymentHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} - // check currency field -> json key currency - currency := g.currency - - // TEMPLATE check-required - if len(currency) == 0 { - return nil, fmt.Errorf("currency is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of currency - params["currency"] = currency return params, nil } @@ -177,13 +177,7 @@ func (g *GetMarginRepaymentHistoryRequest) Do(ctx context.Context) ([]RepaymentR return nil, err } - apiURL := "/api/v3/wallet/m/repayments/:currency" - slugs, err := g.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = g.applySlugsToUrl(apiURL, slugs) + apiURL := "/api/v3/wallet/m/repayments" req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) if err != nil { diff --git a/pkg/exchange/max/maxapi/v3/margin_loan_request.go b/pkg/exchange/max/maxapi/v3/margin_loan_request.go index 5738e119e9..6551309e5e 100644 --- a/pkg/exchange/max/maxapi/v3/margin_loan_request.go +++ b/pkg/exchange/max/maxapi/v3/margin_loan_request.go @@ -10,9 +10,10 @@ func (s *Client) NewMarginLoanRequest() *MarginLoanRequest { return &MarginLoanRequest{client: s.Client} } -//go:generate PostRequest -url "/api/v3/wallet/m/loan/:currency" -type MarginLoanRequest -responseType .LoanRecord +//go:generate PostRequest -url "/api/v3/wallet/m/loan" -type MarginLoanRequest -responseType .LoanRecord type MarginLoanRequest struct { - client requestgen.AuthenticatedAPIClient - currency string `param:"currency,slug,required"` + client requestgen.AuthenticatedAPIClient + + currency string `param:"currency,required"` amount string `param:"amount"` } diff --git a/pkg/exchange/max/maxapi/v3/margin_loan_request_requestgen.go b/pkg/exchange/max/maxapi/v3/margin_loan_request_requestgen.go index 8f3e73466c..a92a73cdc5 100644 --- a/pkg/exchange/max/maxapi/v3/margin_loan_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/margin_loan_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method POST -url /api/v3/wallet/m/loan/:currency -type MarginLoanRequest -responseType .LoanRecord"; DO NOT EDIT. +// Code generated by "requestgen -method POST -url /api/v3/wallet/m/loan -type MarginLoanRequest -responseType .LoanRecord"; DO NOT EDIT. package v3 @@ -11,13 +11,13 @@ import ( "regexp" ) -func (m *MarginLoanRequest) Amount(amount string) *MarginLoanRequest { - m.amount = amount +func (m *MarginLoanRequest) Currency(currency string) *MarginLoanRequest { + m.currency = currency return m } -func (m *MarginLoanRequest) Currency(currency string) *MarginLoanRequest { - m.currency = currency +func (m *MarginLoanRequest) Amount(amount string) *MarginLoanRequest { + m.amount = amount return m } @@ -36,6 +36,17 @@ func (m *MarginLoanRequest) GetQueryParameters() (url.Values, error) { // GetParameters builds and checks the parameters and return the result in a map object func (m *MarginLoanRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} + // check currency field -> json key currency + currency := m.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency // check amount field -> json key amount amount := m.amount @@ -80,17 +91,6 @@ func (m *MarginLoanRequest) GetParametersJSON() ([]byte, error) { // GetSlugParameters builds and checks the slug parameters and return the result in a map object func (m *MarginLoanRequest) GetSlugParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} - // check currency field -> json key currency - currency := m.currency - - // TEMPLATE check-required - if len(currency) == 0 { - return nil, fmt.Errorf("currency is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of currency - params["currency"] = currency return params, nil } @@ -143,13 +143,7 @@ func (m *MarginLoanRequest) Do(ctx context.Context) (*LoanRecord, error) { } query := url.Values{} - apiURL := "/api/v3/wallet/m/loan/:currency" - slugs, err := m.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = m.applySlugsToUrl(apiURL, slugs) + apiURL := "/api/v3/wallet/m/loan" req, err := m.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) if err != nil { diff --git a/pkg/exchange/max/maxapi/v3/margin_repay_request.go b/pkg/exchange/max/maxapi/v3/margin_repay_request.go index 2cc7501152..45d1ba6639 100644 --- a/pkg/exchange/max/maxapi/v3/margin_repay_request.go +++ b/pkg/exchange/max/maxapi/v3/margin_repay_request.go @@ -10,9 +10,10 @@ func (s *Client) NewMarginRepayRequest() *MarginRepayRequest { return &MarginRepayRequest{client: s.Client} } -//go:generate PostRequest -url "/api/v3/wallet/m/repayment/:currency" -type MarginRepayRequest -responseType .RepaymentRecord +//go:generate PostRequest -url "/api/v3/wallet/m/repayment" -type MarginRepayRequest -responseType .RepaymentRecord type MarginRepayRequest struct { - client requestgen.AuthenticatedAPIClient - currency string `param:"currency,slug,required"` + client requestgen.AuthenticatedAPIClient + + currency string `param:"currency,required"` amount string `param:"amount"` } diff --git a/pkg/exchange/max/maxapi/v3/margin_repay_request_requestgen.go b/pkg/exchange/max/maxapi/v3/margin_repay_request_requestgen.go index a84beb4756..0070e52ab6 100644 --- a/pkg/exchange/max/maxapi/v3/margin_repay_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/margin_repay_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method POST -url /api/v3/wallet/m/repayment/:currency -type MarginRepayRequest -responseType .RepaymentRecord"; DO NOT EDIT. +// Code generated by "requestgen -method POST -url /api/v3/wallet/m/repayment -type MarginRepayRequest -responseType .RepaymentRecord"; DO NOT EDIT. package v3 @@ -11,13 +11,13 @@ import ( "regexp" ) -func (m *MarginRepayRequest) Amount(amount string) *MarginRepayRequest { - m.amount = amount +func (m *MarginRepayRequest) Currency(currency string) *MarginRepayRequest { + m.currency = currency return m } -func (m *MarginRepayRequest) Currency(currency string) *MarginRepayRequest { - m.currency = currency +func (m *MarginRepayRequest) Amount(amount string) *MarginRepayRequest { + m.amount = amount return m } @@ -36,6 +36,17 @@ func (m *MarginRepayRequest) GetQueryParameters() (url.Values, error) { // GetParameters builds and checks the parameters and return the result in a map object func (m *MarginRepayRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} + // check currency field -> json key currency + currency := m.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency // check amount field -> json key amount amount := m.amount @@ -80,17 +91,6 @@ func (m *MarginRepayRequest) GetParametersJSON() ([]byte, error) { // GetSlugParameters builds and checks the slug parameters and return the result in a map object func (m *MarginRepayRequest) GetSlugParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} - // check currency field -> json key currency - currency := m.currency - - // TEMPLATE check-required - if len(currency) == 0 { - return nil, fmt.Errorf("currency is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of currency - params["currency"] = currency return params, nil } @@ -143,13 +143,7 @@ func (m *MarginRepayRequest) Do(ctx context.Context) (*RepaymentRecord, error) { } query := url.Values{} - apiURL := "/api/v3/wallet/m/repayment/:currency" - slugs, err := m.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = m.applySlugsToUrl(apiURL, slugs) + apiURL := "/api/v3/wallet/m/repayment" req, err := m.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) if err != nil { From e9f711278ea36e53b3047ba7cef44dee2081a7d1 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 May 2023 16:38:20 +0800 Subject: [PATCH 0828/1392] maxapi: fix margin interest history api --- .../v3/get_margin_interest_history_request.go | 35 ++++++++++++++++ ...gin_interest_history_request_requestgen.go | 42 ++++++++----------- pkg/exchange/max/maxapi/v3/margin.go | 24 ----------- 3 files changed, 53 insertions(+), 48 deletions(-) create mode 100644 pkg/exchange/max/maxapi/v3/get_margin_interest_history_request.go diff --git a/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request.go b/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request.go new file mode 100644 index 0000000000..673c508a85 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request.go @@ -0,0 +1,35 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type MarginInterestRecord struct { + Currency string `json:"currency"` + Amount fixedpoint.Value `json:"amount"` + InterestRate fixedpoint.Value `json:"interest_rate"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` +} + +//go:generate GetRequest -url "/api/v3/wallet/m/interests/history" -type GetMarginInterestHistoryRequest -responseType []MarginInterestRecord +type GetMarginInterestHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + currency string `param:"currency,required"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + limit *int `param:"limit"` +} + +func (s *Client) NewGetMarginInterestHistoryRequest(currency string) *GetMarginInterestHistoryRequest { + return &GetMarginInterestHistoryRequest{client: s.Client, currency: currency} +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go index 60002d36bc..8acd24558a 100644 --- a/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method GET -url /api/v3/wallet/m/interests/history/:currency -type GetMarginInterestHistoryRequest -responseType []MarginInterestRecord"; DO NOT EDIT. +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/interests/history -type GetMarginInterestHistoryRequest -responseType []MarginInterestRecord"; DO NOT EDIT. package v3 @@ -13,6 +13,11 @@ import ( "time" ) +func (g *GetMarginInterestHistoryRequest) Currency(currency string) *GetMarginInterestHistoryRequest { + g.currency = currency + return g +} + func (g *GetMarginInterestHistoryRequest) StartTime(startTime time.Time) *GetMarginInterestHistoryRequest { g.startTime = &startTime return g @@ -28,11 +33,6 @@ func (g *GetMarginInterestHistoryRequest) Limit(limit int) *GetMarginInterestHis return g } -func (g *GetMarginInterestHistoryRequest) Currency(currency string) *GetMarginInterestHistoryRequest { - g.currency = currency - return g -} - // GetQueryParameters builds and checks the query parameters and returns url.Values func (g *GetMarginInterestHistoryRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} @@ -48,6 +48,17 @@ func (g *GetMarginInterestHistoryRequest) GetQueryParameters() (url.Values, erro // GetParameters builds and checks the parameters and return the result in a map object func (g *GetMarginInterestHistoryRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} + // check currency field -> json key currency + currency := g.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency // check startTime field -> json key startTime if g.startTime != nil { startTime := *g.startTime @@ -113,17 +124,6 @@ func (g *GetMarginInterestHistoryRequest) GetParametersJSON() ([]byte, error) { // GetSlugParameters builds and checks the slug parameters and return the result in a map object func (g *GetMarginInterestHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} - // check currency field -> json key currency - currency := g.currency - - // TEMPLATE check-required - if len(currency) == 0 { - return nil, fmt.Errorf("currency is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of currency - params["currency"] = currency return params, nil } @@ -177,13 +177,7 @@ func (g *GetMarginInterestHistoryRequest) Do(ctx context.Context) ([]MarginInter return nil, err } - apiURL := "/api/v3/wallet/m/interests/history/:currency" - slugs, err := g.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = g.applySlugsToUrl(apiURL, slugs) + apiURL := "/api/v3/wallet/m/interests/history" req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) if err != nil { diff --git a/pkg/exchange/max/maxapi/v3/margin.go b/pkg/exchange/max/maxapi/v3/margin.go index 863cb8c065..022b9d084a 100644 --- a/pkg/exchange/max/maxapi/v3/margin.go +++ b/pkg/exchange/max/maxapi/v3/margin.go @@ -5,13 +5,10 @@ package v3 //go:generate -command DeleteRequest requestgen -method DELETE import ( - "time" - "github.com/c9s/requestgen" maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" ) type MarginService struct { @@ -26,10 +23,6 @@ func (s *Client) NewGetMarginBorrowingLimitsRequest() *GetMarginBorrowingLimitsR return &GetMarginBorrowingLimitsRequest{client: s.Client} } -func (s *Client) NewGetMarginInterestHistoryRequest(currency string) *GetMarginInterestHistoryRequest { - return &GetMarginInterestHistoryRequest{client: s.Client, currency: currency} -} - type MarginInterestRate struct { HourlyInterestRate fixedpoint.Value `json:"hourly_interest_rate"` NextHourlyInterestRate fixedpoint.Value `json:"next_hourly_interest_rate"` @@ -48,20 +41,3 @@ type MarginBorrowingLimitMap map[string]fixedpoint.Value type GetMarginBorrowingLimitsRequest struct { client requestgen.APIClient } - -type MarginInterestRecord struct { - Currency string `json:"currency"` - Amount fixedpoint.Value `json:"amount"` - InterestRate fixedpoint.Value `json:"interest_rate"` - CreatedAt types.MillisecondTimestamp `json:"created_at"` -} - -//go:generate GetRequest -url "/api/v3/wallet/m/interests/history/:currency" -type GetMarginInterestHistoryRequest -responseType []MarginInterestRecord -type GetMarginInterestHistoryRequest struct { - client requestgen.AuthenticatedAPIClient - - currency string `param:"currency,slug,required"` - startTime *time.Time `param:"startTime,milliseconds"` - endTime *time.Time `param:"endTime,milliseconds"` - limit *int `param:"limit"` -} From 40f6295d9125a638605fc1d30ea24831e7604eaf Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 May 2023 17:18:42 +0800 Subject: [PATCH 0829/1392] maxapi: move GetMarginInterestRatesRequest api to a file --- .../v3/get_margin_interest_rates_request.go | 27 +++++++++++++++++++ ...argin_interest_rates_request_requestgen.go | 4 +-- pkg/exchange/max/maxapi/v3/margin.go | 16 ----------- .../max/maxapi/v3/margin_repay_request.go | 8 +++--- 4 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request.go diff --git a/pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request.go b/pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request.go new file mode 100644 index 0000000000..e3a8a5d5dc --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request.go @@ -0,0 +1,27 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type MarginInterestRate struct { + HourlyInterestRate fixedpoint.Value `json:"hourly_interest_rate"` + NextHourlyInterestRate fixedpoint.Value `json:"next_hourly_interest_rate"` +} + +type MarginInterestRateMap map[string]MarginInterestRate + +//go:generate GetRequest -url "/api/v3/wallet/m/interest_rates" -type GetMarginInterestRatesRequest -responseType .MarginInterestRateMap +type GetMarginInterestRatesRequest struct { + client requestgen.APIClient +} + +func (s *Client) NewGetMarginInterestRatesRequest() *GetMarginInterestRatesRequest { + return &GetMarginInterestRatesRequest{client: s.Client} +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request_requestgen.go index 6de0e5eaa0..f6c3bf85a8 100644 --- a/pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request_requestgen.go @@ -109,7 +109,7 @@ func (g *GetMarginInterestRatesRequest) GetSlugsMap() (map[string]string, error) return slugs, nil } -func (g *GetMarginInterestRatesRequest) Do(ctx context.Context) (*MarginInterestRateMap, error) { +func (g *GetMarginInterestRatesRequest) Do(ctx context.Context) (MarginInterestRateMap, error) { // no body params var params interface{} @@ -131,5 +131,5 @@ func (g *GetMarginInterestRatesRequest) Do(ctx context.Context) (*MarginInterest if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } - return &apiResponse, nil + return apiResponse, nil } diff --git a/pkg/exchange/max/maxapi/v3/margin.go b/pkg/exchange/max/maxapi/v3/margin.go index 022b9d084a..e4d3fa046a 100644 --- a/pkg/exchange/max/maxapi/v3/margin.go +++ b/pkg/exchange/max/maxapi/v3/margin.go @@ -15,26 +15,10 @@ type MarginService struct { Client *maxapi.RestClient } -func (s *Client) NewGetMarginInterestRatesRequest() *GetMarginInterestRatesRequest { - return &GetMarginInterestRatesRequest{client: s.Client} -} - func (s *Client) NewGetMarginBorrowingLimitsRequest() *GetMarginBorrowingLimitsRequest { return &GetMarginBorrowingLimitsRequest{client: s.Client} } -type MarginInterestRate struct { - HourlyInterestRate fixedpoint.Value `json:"hourly_interest_rate"` - NextHourlyInterestRate fixedpoint.Value `json:"next_hourly_interest_rate"` -} - -type MarginInterestRateMap map[string]MarginInterestRate - -//go:generate GetRequest -url "/api/v3/wallet/m/interest_rates" -type GetMarginInterestRatesRequest -responseType .MarginInterestRateMap -type GetMarginInterestRatesRequest struct { - client requestgen.APIClient -} - type MarginBorrowingLimitMap map[string]fixedpoint.Value //go:generate GetRequest -url "/api/v3/wallet/m/limits" -type GetMarginBorrowingLimitsRequest -responseType .MarginBorrowingLimitMap diff --git a/pkg/exchange/max/maxapi/v3/margin_repay_request.go b/pkg/exchange/max/maxapi/v3/margin_repay_request.go index 45d1ba6639..8d6150f4b8 100644 --- a/pkg/exchange/max/maxapi/v3/margin_repay_request.go +++ b/pkg/exchange/max/maxapi/v3/margin_repay_request.go @@ -6,10 +6,6 @@ package v3 import "github.com/c9s/requestgen" -func (s *Client) NewMarginRepayRequest() *MarginRepayRequest { - return &MarginRepayRequest{client: s.Client} -} - //go:generate PostRequest -url "/api/v3/wallet/m/repayment" -type MarginRepayRequest -responseType .RepaymentRecord type MarginRepayRequest struct { client requestgen.AuthenticatedAPIClient @@ -17,3 +13,7 @@ type MarginRepayRequest struct { currency string `param:"currency,required"` amount string `param:"amount"` } + +func (s *Client) NewMarginRepayRequest() *MarginRepayRequest { + return &MarginRepayRequest{client: s.Client} +} From 1ca81e11e6d2b6e1ca2c5478d5b60cfcd2fc9a9d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 May 2023 17:20:42 +0800 Subject: [PATCH 0830/1392] maxapi: add currency field to the accounts api --- .../maxapi/v3/get_wallet_accounts_request.go | 9 +++++---- .../get_wallet_accounts_request_requestgen.go | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go index 37479cd1bc..9fbf54da4a 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go @@ -6,13 +6,14 @@ import "github.com/c9s/requestgen" //go:generate -command PostRequest requestgen -method POST //go:generate -command DeleteRequest requestgen -method DELETE -func (s *Client) NewGetWalletAccountsRequest(walletType WalletType) *GetWalletAccountsRequest { - return &GetWalletAccountsRequest{client: s.Client, walletType: walletType} -} - //go:generate GetRequest -url "/api/v3/wallet/:walletType/accounts" -type GetWalletAccountsRequest -responseType []Account type GetWalletAccountsRequest struct { client requestgen.AuthenticatedAPIClient walletType WalletType `param:"walletType,slug,required"` + currency string `param:"currency"` +} + +func (s *Client) NewGetWalletAccountsRequest(walletType WalletType) *GetWalletAccountsRequest { + return &GetWalletAccountsRequest{client: s.Client, walletType: walletType} } diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go index 7c5c1ff04a..dc4b0daa88 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go @@ -12,6 +12,11 @@ import ( "regexp" ) +func (g *GetWalletAccountsRequest) Currency(currency string) *GetWalletAccountsRequest { + g.currency = currency + return g +} + func (g *GetWalletAccountsRequest) WalletType(walletType max.WalletType) *GetWalletAccountsRequest { g.walletType = walletType return g @@ -32,6 +37,11 @@ func (g *GetWalletAccountsRequest) GetQueryParameters() (url.Values, error) { // GetParameters builds and checks the parameters and return the result in a map object func (g *GetWalletAccountsRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} + // check currency field -> json key currency + currency := g.currency + + // assign parameter of currency + params["currency"] = currency return params, nil } @@ -128,9 +138,12 @@ func (g *GetWalletAccountsRequest) GetSlugsMap() (map[string]string, error) { func (g *GetWalletAccountsRequest) Do(ctx context.Context) ([]max.Account, error) { - // no body params + // empty params for GET operation var params interface{} - query := url.Values{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } apiURL := "/api/v3/wallet/:walletType/accounts" slugs, err := g.GetSlugsMap() From 7cf80473e55b5ab786f484b3f49e940d02a0648e Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 May 2023 17:23:04 +0800 Subject: [PATCH 0831/1392] maxapi: fix margin interest history request --- .../max/maxapi/v3/get_margin_interest_history_request.go | 2 +- .../v3/get_margin_interest_history_request_requestgen.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request.go b/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request.go index 673c508a85..f64a7f6ba9 100644 --- a/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request.go @@ -20,7 +20,7 @@ type MarginInterestRecord struct { CreatedAt types.MillisecondTimestamp `json:"created_at"` } -//go:generate GetRequest -url "/api/v3/wallet/m/interests/history" -type GetMarginInterestHistoryRequest -responseType []MarginInterestRecord +//go:generate GetRequest -url "/api/v3/wallet/m/interests" -type GetMarginInterestHistoryRequest -responseType []MarginInterestRecord type GetMarginInterestHistoryRequest struct { client requestgen.AuthenticatedAPIClient diff --git a/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go index 8acd24558a..8f9ad40f01 100644 --- a/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method GET -url /api/v3/wallet/m/interests/history -type GetMarginInterestHistoryRequest -responseType []MarginInterestRecord"; DO NOT EDIT. +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/interests -type GetMarginInterestHistoryRequest -responseType []MarginInterestRecord"; DO NOT EDIT. package v3 @@ -177,7 +177,7 @@ func (g *GetMarginInterestHistoryRequest) Do(ctx context.Context) ([]MarginInter return nil, err } - apiURL := "/api/v3/wallet/m/interests/history" + apiURL := "/api/v3/wallet/m/interests" req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) if err != nil { From 174fd7b8e7d589eea6b9e562e66b863101c4cb00 Mon Sep 17 00:00:00 2001 From: narumi Date: Fri, 5 May 2023 14:27:26 +0800 Subject: [PATCH 0832/1392] support binance futures trading data --- ...res_get_global_long_short_account_ratio.go | 33 +++ .../binanceapi/futures_get_open_interest.go | 24 ++ ...es_get_open_interest_request_requestgen.go | 148 ++++++++++++ .../futures_get_open_interest_statistics.go | 32 +++ ..._interest_statistics_request_requestgen.go | 214 ++++++++++++++++++ .../futures_get_taker_buy_sell_volume.go | 32 +++ ...get_top_trader_long_short_account_ratio.go | 33 +++ ...et_top_trader_long_short_position_ratio.go | 33 +++ ..._short_account_ratio_request_requestgen.go | 202 +++++++++++++++++ ...aker_buy_sell_volume_request_requestgen.go | 202 +++++++++++++++++ ..._short_account_ratio_request_requestgen.go | 202 +++++++++++++++++ ...short_position_ratio_request_requestgen.go | 202 +++++++++++++++++ 12 files changed, 1357 insertions(+) create mode 100644 pkg/exchange/binance/binanceapi/futures_get_global_long_short_account_ratio.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_open_interest.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_open_interest_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_open_interest_statistics.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_open_interest_statistics_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_taker_buy_sell_volume.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_top_trader_long_short_account_ratio.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_top_trader_long_short_position_ratio.go create mode 100644 pkg/exchange/binance/binanceapi/futures_global_long_short_account_ratio_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/futures_taker_buy_sell_volume_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/futures_top_trader_long_short_account_ratio_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/futures_top_trader_long_short_position_ratio_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/futures_get_global_long_short_account_ratio.go b/pkg/exchange/binance/binanceapi/futures_get_global_long_short_account_ratio.go new file mode 100644 index 0000000000..9613dfe438 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_global_long_short_account_ratio.go @@ -0,0 +1,33 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +type FuturesGlobalLongShortAccountRatio struct { + Symbol string `json:"symbol"` + LongShortRatio fixedpoint.Value `json:"longShortRatio"` + LongAccount fixedpoint.Value `json:"longAccount"` + ShortAccount fixedpoint.Value `json:"shortAccount"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` +} + +//go:generate requestgen -method GET -url "/futures/data/globalLongShortAccountRatio" -type FuturesGlobalLongShortAccountRatioRequest -responseType []FuturesGlobalLongShortAccountRatio +type FuturesGlobalLongShortAccountRatioRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` + period types.Interval `param:"period"` + + limit *uint64 `param:"limit"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` +} + +func (c *FuturesRestClient) NewFuturesGlobalLongShortAccountRatioRequest() *FuturesGlobalLongShortAccountRatioRequest { + return &FuturesGlobalLongShortAccountRatioRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_open_interest.go b/pkg/exchange/binance/binanceapi/futures_get_open_interest.go new file mode 100644 index 0000000000..0ea317c094 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_open_interest.go @@ -0,0 +1,24 @@ +package binanceapi + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +type FuturesOpenInterest struct { + OpenInterest fixedpoint.Value `json:"openInterest"` + Symbol string `json:"symbol"` + Time types.MillisecondTimestamp `json:"time"` +} + +//go:generate requestgen -method GET -url "/fapi/v1/openInterest" -type FuturesGetOpenInterestRequest -responseType FuturesOpenInterest +type FuturesGetOpenInterestRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` +} + +func (c *FuturesRestClient) NewFuturesGetOpenInterestRequest() *FuturesGetOpenInterestRequest { + return &FuturesGetOpenInterestRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_open_interest_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_get_open_interest_request_requestgen.go new file mode 100644 index 0000000000..0af266f9f1 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_open_interest_request_requestgen.go @@ -0,0 +1,148 @@ +// Code generated by "requestgen -method GET -url /fapi/v1/openInterest -type FuturesGetOpenInterestRequest -responseType FuturesOpenInterest"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (f *FuturesGetOpenInterestRequest) Symbol(symbol string) *FuturesGetOpenInterestRequest { + f.symbol = symbol + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGetOpenInterestRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGetOpenInterestRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := f.symbol + + // assign parameter of symbol + params["symbol"] = symbol + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGetOpenInterestRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGetOpenInterestRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGetOpenInterestRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGetOpenInterestRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGetOpenInterestRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGetOpenInterestRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGetOpenInterestRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesGetOpenInterestRequest) Do(ctx context.Context) (*FuturesOpenInterest, error) { + + // empty params for GET operation + var params interface{} + query, err := f.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/fapi/v1/openInterest" + + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse FuturesOpenInterest + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_open_interest_statistics.go b/pkg/exchange/binance/binanceapi/futures_get_open_interest_statistics.go new file mode 100644 index 0000000000..eeafa3bbd3 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_open_interest_statistics.go @@ -0,0 +1,32 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +type FuturesOpenInterestStatistics struct { + Symbol string `json:"symbol"` + SumOpenInterest fixedpoint.Value `json:"sumOpenInterest"` + SumOpenInterestValue fixedpoint.Value `json:"sumOpenInterestValue"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` +} + +//go:generate requestgen -method GET -url "/futures/data/openInterestHist" -type FuturesGetOpenInterestStatisticsRequest -responseType []FuturesOpenInterestStatistics +type FuturesGetOpenInterestStatisticsRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol,required"` + period types.Interval `param:"period,required"` + + limit *uint64 `param:"limit"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` +} + +func (c *FuturesRestClient) NewFuturesGetOpenInterestStatisticsRequest() *FuturesGetOpenInterestStatisticsRequest { + return &FuturesGetOpenInterestStatisticsRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_open_interest_statistics_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_get_open_interest_statistics_request_requestgen.go new file mode 100644 index 0000000000..0cdedaeca0 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_open_interest_statistics_request_requestgen.go @@ -0,0 +1,214 @@ +// Code generated by "requestgen -method GET -url /futures/data/openInterestHist -type FuturesGetOpenInterestStatisticsRequest -responseType []FuturesOpenInterestStatistics"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/types" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (f *FuturesGetOpenInterestStatisticsRequest) Symbol(symbol string) *FuturesGetOpenInterestStatisticsRequest { + f.symbol = symbol + return f +} + +func (f *FuturesGetOpenInterestStatisticsRequest) Period(period types.Interval) *FuturesGetOpenInterestStatisticsRequest { + f.period = period + return f +} + +func (f *FuturesGetOpenInterestStatisticsRequest) Limit(limit uint64) *FuturesGetOpenInterestStatisticsRequest { + f.limit = &limit + return f +} + +func (f *FuturesGetOpenInterestStatisticsRequest) StartTime(startTime time.Time) *FuturesGetOpenInterestStatisticsRequest { + f.startTime = &startTime + return f +} + +func (f *FuturesGetOpenInterestStatisticsRequest) EndTime(endTime time.Time) *FuturesGetOpenInterestStatisticsRequest { + f.endTime = &endTime + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGetOpenInterestStatisticsRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGetOpenInterestStatisticsRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := f.symbol + + // TEMPLATE check-required + if len(symbol) == 0 { + return nil, fmt.Errorf("symbol is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of symbol + params["symbol"] = symbol + // check period field -> json key period + period := f.period + + // TEMPLATE check-required + if len(period) == 0 { + return nil, fmt.Errorf("period is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of period + params["period"] = period + // check limit field -> json key limit + if f.limit != nil { + limit := *f.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check startTime field -> json key startTime + if f.startTime != nil { + startTime := *f.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if f.endTime != nil { + endTime := *f.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGetOpenInterestStatisticsRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGetOpenInterestStatisticsRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGetOpenInterestStatisticsRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGetOpenInterestStatisticsRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGetOpenInterestStatisticsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGetOpenInterestStatisticsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGetOpenInterestStatisticsRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesGetOpenInterestStatisticsRequest) Do(ctx context.Context) ([]FuturesOpenInterestStatistics, error) { + + // empty params for GET operation + var params interface{} + query, err := f.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/futures/data/openInterestHist" + + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []FuturesOpenInterestStatistics + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_taker_buy_sell_volume.go b/pkg/exchange/binance/binanceapi/futures_get_taker_buy_sell_volume.go new file mode 100644 index 0000000000..b7754a1150 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_taker_buy_sell_volume.go @@ -0,0 +1,32 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +type FuturesTakerBuySellVolume struct { + BuySellRatio fixedpoint.Value `json:"buySellRatio"` + BuyVol fixedpoint.Value `json:"buyVol"` + SellVol fixedpoint.Value `json:"sellVol"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` +} + +//go:generate requestgen -method GET -url "/futures/data/takerlongshortRatio" -type FuturesTakerBuySellVolumeRequest -responseType []FuturesTakerBuySellVolume +type FuturesTakerBuySellVolumeRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` + period types.Interval `param:"period"` + + limit *uint64 `param:"limit"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` +} + +func (c *FuturesRestClient) NewFuturesTakerBuySellVolumeRequest() *FuturesTakerBuySellVolumeRequest { + return &FuturesTakerBuySellVolumeRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_top_trader_long_short_account_ratio.go b/pkg/exchange/binance/binanceapi/futures_get_top_trader_long_short_account_ratio.go new file mode 100644 index 0000000000..8d1c7e697f --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_top_trader_long_short_account_ratio.go @@ -0,0 +1,33 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +type FuturesTopTraderLongShortAccountRatio struct { + Symbol string `json:"symbol"` + LongShortRatio fixedpoint.Value `json:"longShortRatio"` + LongAccount fixedpoint.Value `json:"longAccount"` + ShortAccount fixedpoint.Value `json:"shortAccount"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` +} + +//go:generate requestgen -method GET -url "/futures/data/topLongShortAccountRatio" -type FuturesTopTraderLongShortAccountRatioRequest -responseType []FuturesTopTraderLongShortAccountRatio +type FuturesTopTraderLongShortAccountRatioRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` + period types.Interval `param:"period"` + + limit *uint64 `param:"limit"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` +} + +func (c *FuturesRestClient) NewFuturesTopTraderLongShortAccountRatioRequest() *FuturesTopTraderLongShortAccountRatioRequest { + return &FuturesTopTraderLongShortAccountRatioRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_top_trader_long_short_position_ratio.go b/pkg/exchange/binance/binanceapi/futures_get_top_trader_long_short_position_ratio.go new file mode 100644 index 0000000000..fce5da2059 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_top_trader_long_short_position_ratio.go @@ -0,0 +1,33 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +type FuturesTopTraderLongShortPositionRatio struct { + Symbol string `json:"symbol"` + LongShortRatio fixedpoint.Value `json:"longShortRatio"` + LongAccount fixedpoint.Value `json:"longAccount"` + ShortAccount fixedpoint.Value `json:"shortAccount"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` +} + +//go:generate requestgen -method GET -url "/futures/data/topLongShortPositionRatio" -type FuturesTopTraderLongShortPositionRatioRequest -responseType []FuturesTopTraderLongShortPositionRatio +type FuturesTopTraderLongShortPositionRatioRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` + period types.Interval `param:"period"` + + limit *uint64 `param:"limit"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` +} + +func (c *FuturesRestClient) NewFuturesTopTraderLongShortPositionRatioRequest() *FuturesTopTraderLongShortPositionRatioRequest { + return &FuturesTopTraderLongShortPositionRatioRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_global_long_short_account_ratio_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_global_long_short_account_ratio_request_requestgen.go new file mode 100644 index 0000000000..e4d795919d --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_global_long_short_account_ratio_request_requestgen.go @@ -0,0 +1,202 @@ +// Code generated by "requestgen -method GET -url /futures/data/globalLongShortAccountRatio -type FuturesGlobalLongShortAccountRatioRequest -responseType []FuturesGlobalLongShortAccountRatio"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/types" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (f *FuturesGlobalLongShortAccountRatioRequest) Symbol(symbol string) *FuturesGlobalLongShortAccountRatioRequest { + f.symbol = symbol + return f +} + +func (f *FuturesGlobalLongShortAccountRatioRequest) Period(period types.Interval) *FuturesGlobalLongShortAccountRatioRequest { + f.period = period + return f +} + +func (f *FuturesGlobalLongShortAccountRatioRequest) Limit(limit uint64) *FuturesGlobalLongShortAccountRatioRequest { + f.limit = &limit + return f +} + +func (f *FuturesGlobalLongShortAccountRatioRequest) StartTime(startTime time.Time) *FuturesGlobalLongShortAccountRatioRequest { + f.startTime = &startTime + return f +} + +func (f *FuturesGlobalLongShortAccountRatioRequest) EndTime(endTime time.Time) *FuturesGlobalLongShortAccountRatioRequest { + f.endTime = &endTime + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGlobalLongShortAccountRatioRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGlobalLongShortAccountRatioRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := f.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check period field -> json key period + period := f.period + + // assign parameter of period + params["period"] = period + // check limit field -> json key limit + if f.limit != nil { + limit := *f.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check startTime field -> json key startTime + if f.startTime != nil { + startTime := *f.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if f.endTime != nil { + endTime := *f.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGlobalLongShortAccountRatioRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGlobalLongShortAccountRatioRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGlobalLongShortAccountRatioRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGlobalLongShortAccountRatioRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGlobalLongShortAccountRatioRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGlobalLongShortAccountRatioRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGlobalLongShortAccountRatioRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesGlobalLongShortAccountRatioRequest) Do(ctx context.Context) ([]FuturesGlobalLongShortAccountRatio, error) { + + // empty params for GET operation + var params interface{} + query, err := f.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/futures/data/globalLongShortAccountRatio" + + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []FuturesGlobalLongShortAccountRatio + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/futures_taker_buy_sell_volume_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_taker_buy_sell_volume_request_requestgen.go new file mode 100644 index 0000000000..59f615ccd1 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_taker_buy_sell_volume_request_requestgen.go @@ -0,0 +1,202 @@ +// Code generated by "requestgen -method GET -url /futures/data/takerlongshortRatio -type FuturesTakerBuySellVolumeRequest -responseType []FuturesTakerBuySellVolume"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/types" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (f *FuturesTakerBuySellVolumeRequest) Symbol(symbol string) *FuturesTakerBuySellVolumeRequest { + f.symbol = symbol + return f +} + +func (f *FuturesTakerBuySellVolumeRequest) Period(period types.Interval) *FuturesTakerBuySellVolumeRequest { + f.period = period + return f +} + +func (f *FuturesTakerBuySellVolumeRequest) Limit(limit uint64) *FuturesTakerBuySellVolumeRequest { + f.limit = &limit + return f +} + +func (f *FuturesTakerBuySellVolumeRequest) StartTime(startTime time.Time) *FuturesTakerBuySellVolumeRequest { + f.startTime = &startTime + return f +} + +func (f *FuturesTakerBuySellVolumeRequest) EndTime(endTime time.Time) *FuturesTakerBuySellVolumeRequest { + f.endTime = &endTime + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesTakerBuySellVolumeRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesTakerBuySellVolumeRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := f.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check period field -> json key period + period := f.period + + // assign parameter of period + params["period"] = period + // check limit field -> json key limit + if f.limit != nil { + limit := *f.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check startTime field -> json key startTime + if f.startTime != nil { + startTime := *f.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if f.endTime != nil { + endTime := *f.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesTakerBuySellVolumeRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesTakerBuySellVolumeRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesTakerBuySellVolumeRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesTakerBuySellVolumeRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesTakerBuySellVolumeRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesTakerBuySellVolumeRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesTakerBuySellVolumeRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesTakerBuySellVolumeRequest) Do(ctx context.Context) ([]FuturesTakerBuySellVolume, error) { + + // empty params for GET operation + var params interface{} + query, err := f.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/futures/data/takerlongshortRatio" + + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []FuturesTakerBuySellVolume + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/futures_top_trader_long_short_account_ratio_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_top_trader_long_short_account_ratio_request_requestgen.go new file mode 100644 index 0000000000..a6645f15d3 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_top_trader_long_short_account_ratio_request_requestgen.go @@ -0,0 +1,202 @@ +// Code generated by "requestgen -method GET -url /futures/data/topLongShortAccountRatio -type FuturesTopTraderLongShortAccountRatioRequest -responseType []FuturesTopTraderLongShortAccountRatio"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/types" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (f *FuturesTopTraderLongShortAccountRatioRequest) Symbol(symbol string) *FuturesTopTraderLongShortAccountRatioRequest { + f.symbol = symbol + return f +} + +func (f *FuturesTopTraderLongShortAccountRatioRequest) Period(period types.Interval) *FuturesTopTraderLongShortAccountRatioRequest { + f.period = period + return f +} + +func (f *FuturesTopTraderLongShortAccountRatioRequest) Limit(limit uint64) *FuturesTopTraderLongShortAccountRatioRequest { + f.limit = &limit + return f +} + +func (f *FuturesTopTraderLongShortAccountRatioRequest) StartTime(startTime time.Time) *FuturesTopTraderLongShortAccountRatioRequest { + f.startTime = &startTime + return f +} + +func (f *FuturesTopTraderLongShortAccountRatioRequest) EndTime(endTime time.Time) *FuturesTopTraderLongShortAccountRatioRequest { + f.endTime = &endTime + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesTopTraderLongShortAccountRatioRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesTopTraderLongShortAccountRatioRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := f.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check period field -> json key period + period := f.period + + // assign parameter of period + params["period"] = period + // check limit field -> json key limit + if f.limit != nil { + limit := *f.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check startTime field -> json key startTime + if f.startTime != nil { + startTime := *f.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if f.endTime != nil { + endTime := *f.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesTopTraderLongShortAccountRatioRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesTopTraderLongShortAccountRatioRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesTopTraderLongShortAccountRatioRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesTopTraderLongShortAccountRatioRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesTopTraderLongShortAccountRatioRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesTopTraderLongShortAccountRatioRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesTopTraderLongShortAccountRatioRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesTopTraderLongShortAccountRatioRequest) Do(ctx context.Context) ([]FuturesTopTraderLongShortAccountRatio, error) { + + // empty params for GET operation + var params interface{} + query, err := f.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/futures/data/topLongShortAccountRatio" + + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []FuturesTopTraderLongShortAccountRatio + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/futures_top_trader_long_short_position_ratio_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_top_trader_long_short_position_ratio_request_requestgen.go new file mode 100644 index 0000000000..d98e585225 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_top_trader_long_short_position_ratio_request_requestgen.go @@ -0,0 +1,202 @@ +// Code generated by "requestgen -method GET -url /futures/data/topLongShortPositionRatio -type FuturesTopTraderLongShortPositionRatioRequest -responseType []FuturesTopTraderLongShortPositionRatio"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/types" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (f *FuturesTopTraderLongShortPositionRatioRequest) Symbol(symbol string) *FuturesTopTraderLongShortPositionRatioRequest { + f.symbol = symbol + return f +} + +func (f *FuturesTopTraderLongShortPositionRatioRequest) Period(period types.Interval) *FuturesTopTraderLongShortPositionRatioRequest { + f.period = period + return f +} + +func (f *FuturesTopTraderLongShortPositionRatioRequest) Limit(limit uint64) *FuturesTopTraderLongShortPositionRatioRequest { + f.limit = &limit + return f +} + +func (f *FuturesTopTraderLongShortPositionRatioRequest) StartTime(startTime time.Time) *FuturesTopTraderLongShortPositionRatioRequest { + f.startTime = &startTime + return f +} + +func (f *FuturesTopTraderLongShortPositionRatioRequest) EndTime(endTime time.Time) *FuturesTopTraderLongShortPositionRatioRequest { + f.endTime = &endTime + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesTopTraderLongShortPositionRatioRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesTopTraderLongShortPositionRatioRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := f.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check period field -> json key period + period := f.period + + // assign parameter of period + params["period"] = period + // check limit field -> json key limit + if f.limit != nil { + limit := *f.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check startTime field -> json key startTime + if f.startTime != nil { + startTime := *f.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if f.endTime != nil { + endTime := *f.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesTopTraderLongShortPositionRatioRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesTopTraderLongShortPositionRatioRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesTopTraderLongShortPositionRatioRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesTopTraderLongShortPositionRatioRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesTopTraderLongShortPositionRatioRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesTopTraderLongShortPositionRatioRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesTopTraderLongShortPositionRatioRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesTopTraderLongShortPositionRatioRequest) Do(ctx context.Context) ([]FuturesTopTraderLongShortPositionRatio, error) { + + // empty params for GET operation + var params interface{} + query, err := f.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/futures/data/topLongShortPositionRatio" + + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []FuturesTopTraderLongShortPositionRatio + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} From b148a02491aec893cd5857c27ae4af1843afbb48 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 8 May 2023 13:43:25 +0800 Subject: [PATCH 0833/1392] strategy/supertrend: add net profit --- pkg/strategy/supertrend/strategy.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 88a3895df1..b75099a0ec 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -139,7 +139,7 @@ func (r *AccumulatedProfitReport) Output(symbol string) { } defer tsvwiter.Close() // Output symbol, total acc. profit, acc. profit 60MA, interval acc. profit, fee, win rate, profit factor - _ = tsvwiter.Write([]string{"#", "Symbol", "accumulatedProfit", "accumulatedProfitMA", fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), "accumulatedFee", "winRatio", "profitFactor", "60D trades", "Window", "Multiplier", "FastDEMA", "SlowDEMA", "LinReg"}) + _ = tsvwiter.Write([]string{"#", "Symbol", "accumulatedProfit", "accumulatedProfitMA", fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), "accumulatedFee", "accumulatedNetProfit", "winRatio", "profitFactor", "60D trades", "Window", "Multiplier", "FastDEMA", "SlowDEMA", "LinReg"}) for i := 0; i <= r.NumberOfInterval-1; i++ { accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i) accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit) @@ -148,6 +148,7 @@ func (r *AccumulatedProfitReport) Output(symbol string) { intervalAccumulatedProfit := r.dailyProfit.Tail(r.AccumulatedDailyProfitWindow+r.IntervalWindow*i).Sum() - r.dailyProfit.Tail(r.IntervalWindow*i).Sum() intervalAccumulatedProfitStr := fmt.Sprintf("%f", intervalAccumulatedProfit) accumulatedFee := fmt.Sprintf("%f", r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) + accumulatedNetProfit := fmt.Sprintf("%f", accumulatedProfit-r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) winRatio := fmt.Sprintf("%f", r.winRatioPerDay.Index(r.IntervalWindow*i)) profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i)) trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum() @@ -158,7 +159,7 @@ func (r *AccumulatedProfitReport) Output(symbol string) { slowDEMAStr := fmt.Sprintf("%d", r.s.SlowDEMAWindow) linRegStr := fmt.Sprintf("%d", r.s.LinearRegression.Window) - _ = tsvwiter.Write([]string{fmt.Sprintf("%d", i+1), symbol, accumulatedProfitStr, accumulatedProfitMAStr, intervalAccumulatedProfitStr, accumulatedFee, winRatio, profitFactor, tradesStr, windowStr, multiplierStr, fastDEMAStr, slowDEMAStr, linRegStr}) + _ = tsvwiter.Write([]string{fmt.Sprintf("%d", i+1), symbol, accumulatedProfitStr, accumulatedProfitMAStr, intervalAccumulatedProfitStr, accumulatedFee, accumulatedNetProfit, winRatio, profitFactor, tradesStr, windowStr, multiplierStr, fastDEMAStr, slowDEMAStr, linRegStr}) } } } From f864cc895cabe8f89a189210c95650f4cf41787d Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 11 May 2023 14:54:45 +0800 Subject: [PATCH 0834/1392] feature/profitReport: accumulated profit report as a package --- pkg/report/profit_report.go | 197 ++++++++++++++++++++++++++++ pkg/strategy/supertrend/strategy.go | 176 +++---------------------- 2 files changed, 214 insertions(+), 159 deletions(-) create mode 100644 pkg/report/profit_report.go diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go new file mode 100644 index 0000000000..e3faaa50f8 --- /dev/null +++ b/pkg/report/profit_report.go @@ -0,0 +1,197 @@ +package report + +import ( + "fmt" + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// AccumulatedProfitReport For accumulated profit report output +type AccumulatedProfitReport struct { + // AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades + AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"` + + // IntervalWindow interval window, in days + IntervalWindow int `json:"intervalWindow"` + + // NumberOfInterval How many intervals to output to TSV + NumberOfInterval int `json:"NumberOfInterval"` + + // TsvReportPath The path to output report to + TsvReportPath string `json:"tsvReportPath"` + + // AccumulatedDailyProfitWindow The window to sum up the daily profit, in days + AccumulatedDailyProfitWindow int `json:"accumulatedDailyProfitWindow"` + + Symbol string + + // Accumulated profit + accumulatedProfit fixedpoint.Value + accumulatedProfitPerDay floats.Slice + previousAccumulatedProfit fixedpoint.Value + + // Accumulated profit MA + accumulatedProfitMA *indicator.SMA + accumulatedProfitMAPerDay floats.Slice + + // Daily profit + dailyProfit floats.Slice + + // Accumulated fee + accumulatedFee fixedpoint.Value + accumulatedFeePerDay floats.Slice + + // Win ratio + winRatioPerDay floats.Slice + + // Profit factor + profitFactorPerDay floats.Slice + + // Trade number + dailyTrades floats.Slice + accumulatedTrades int + previousAccumulatedTrades int + + // Extra values + extraValues [][2]string +} + +func (r *AccumulatedProfitReport) Initialize(Symbol string, session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor, TradeStats *types.TradeStats) { + r.Symbol = Symbol + + if r.AccumulatedProfitMAWindow <= 0 { + r.AccumulatedProfitMAWindow = 60 + } + if r.IntervalWindow <= 0 { + r.IntervalWindow = 7 + } + if r.AccumulatedDailyProfitWindow <= 0 { + r.AccumulatedDailyProfitWindow = 7 + } + if r.NumberOfInterval <= 0 { + r.NumberOfInterval = 1 + } + r.accumulatedProfitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1d, Window: r.AccumulatedProfitMAWindow}} + + session.Subscribe(types.KLineChannel, r.Symbol, types.SubscribeOptions{Interval: types.Interval1d}) + + // Record profit + orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) { + if profit == nil { + return + } + + r.RecordProfit(profit.Profit) + }) + + // Record trade + orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + r.RecordTrade(trade.Fee) + }) + + // Record daily status + session.MarketDataStream.OnKLineClosed(types.KLineWith(r.Symbol, types.Interval1d, func(kline types.KLine) { + r.DailyUpdate(TradeStats) + })) +} + +func (r *AccumulatedProfitReport) AddExtraValue(valueAndTitle [2]string) { + r.extraValues = append(r.extraValues, valueAndTitle) +} + +func (r *AccumulatedProfitReport) RecordProfit(profit fixedpoint.Value) { + r.accumulatedProfit = r.accumulatedProfit.Add(profit) +} + +func (r *AccumulatedProfitReport) RecordTrade(fee fixedpoint.Value) { + r.accumulatedFee = r.accumulatedFee.Add(fee) + r.accumulatedTrades += 1 +} + +func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { + // Daily profit + r.dailyProfit.Update(r.accumulatedProfit.Sub(r.previousAccumulatedProfit).Float64()) + r.previousAccumulatedProfit = r.accumulatedProfit + + // Accumulated profit + r.accumulatedProfitPerDay.Update(r.accumulatedProfit.Float64()) + + // Accumulated profit MA + r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) + r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last()) + + // Accumulated Fee + r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) + + // Win ratio + r.winRatioPerDay.Update(tradeStats.WinningRatio.Float64()) + + // Profit factor + r.profitFactorPerDay.Update(tradeStats.ProfitFactor.Float64()) + + // Daily trades + r.dailyTrades.Update(float64(r.accumulatedTrades - r.previousAccumulatedTrades)) + r.previousAccumulatedTrades = r.accumulatedTrades +} + +// Output Accumulated profit report to a TSV file +func (r *AccumulatedProfitReport) Output(symbol string) { + if r.TsvReportPath != "" { + tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath) + if err != nil { + panic(err) + } + defer tsvwiter.Close() + // Output title row + titles := []string{ + "#", + "Symbol", + "accumulatedProfit", + "accumulatedProfitMA", + fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), + "accumulatedFee", + "accumulatedNetProfit", + "winRatio", + "profitFactor", + "60D trades", + } + for i := 0; i < len(r.extraValues); i++ { + titles = append(titles, r.extraValues[i][0]) + } + _ = tsvwiter.Write(titles) + + // Output data row + for i := 0; i <= r.NumberOfInterval-1; i++ { + accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i) + accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit) + accumulatedProfitMA := r.accumulatedProfitMAPerDay.Index(r.IntervalWindow * i) + accumulatedProfitMAStr := fmt.Sprintf("%f", accumulatedProfitMA) + intervalAccumulatedProfit := r.dailyProfit.Tail(r.AccumulatedDailyProfitWindow+r.IntervalWindow*i).Sum() - r.dailyProfit.Tail(r.IntervalWindow*i).Sum() + intervalAccumulatedProfitStr := fmt.Sprintf("%f", intervalAccumulatedProfit) + accumulatedFee := fmt.Sprintf("%f", r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) + accumulatedNetProfit := fmt.Sprintf("%f", accumulatedProfit-r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) + winRatio := fmt.Sprintf("%f", r.winRatioPerDay.Index(r.IntervalWindow*i)) + profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i)) + trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum() + tradesStr := fmt.Sprintf("%f", trades) + values := []string{ + fmt.Sprintf("%d", i+1), + symbol, accumulatedProfitStr, + accumulatedProfitMAStr, + intervalAccumulatedProfitStr, + accumulatedFee, + accumulatedNetProfit, + winRatio, profitFactor, + tradesStr, + } + for j := 0; j < len(r.extraValues); j++ { + values = append(values, r.extraValues[j][1]) + } + _ = tsvwiter.Write(values) + } + } +} diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index b75099a0ec..978d724a58 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -9,12 +9,12 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" - "github.com/c9s/bbgo/pkg/data/tsv" "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/report" "github.com/c9s/bbgo/pkg/types" ) @@ -30,140 +30,6 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } -// AccumulatedProfitReport For accumulated profit report output -type AccumulatedProfitReport struct { - s *Strategy - - // AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades - AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"` - - // IntervalWindow interval window, in days - IntervalWindow int `json:"intervalWindow"` - - // NumberOfInterval How many intervals to output to TSV - NumberOfInterval int `json:"NumberOfInterval"` - - // TsvReportPath The path to output report to - TsvReportPath string `json:"tsvReportPath"` - - // AccumulatedDailyProfitWindow The window to sum up the daily profit, in days - AccumulatedDailyProfitWindow int `json:"accumulatedDailyProfitWindow"` - - // Accumulated profit - accumulatedProfit fixedpoint.Value - accumulatedProfitPerDay floats.Slice - previousAccumulatedProfit fixedpoint.Value - - // Accumulated profit MA - accumulatedProfitMA *indicator.SMA - accumulatedProfitMAPerDay floats.Slice - - // Daily profit - dailyProfit floats.Slice - - // Accumulated fee - accumulatedFee fixedpoint.Value - accumulatedFeePerDay floats.Slice - - // Win ratio - winRatioPerDay floats.Slice - - // Profit factor - profitFactorPerDay floats.Slice - - // Trade number - dailyTrades floats.Slice - accumulatedTrades int - previousAccumulatedTrades int -} - -func (r *AccumulatedProfitReport) Initialize(strategy *Strategy) { - r.s = strategy - - if r.AccumulatedProfitMAWindow <= 0 { - r.AccumulatedProfitMAWindow = 60 - } - if r.IntervalWindow <= 0 { - r.IntervalWindow = 7 - } - if r.AccumulatedDailyProfitWindow <= 0 { - r.AccumulatedDailyProfitWindow = 7 - } - if r.NumberOfInterval <= 0 { - r.NumberOfInterval = 1 - } - r.accumulatedProfitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1d, Window: r.AccumulatedProfitMAWindow}} -} - -func (r *AccumulatedProfitReport) RecordProfit(profit fixedpoint.Value) { - r.accumulatedProfit = r.accumulatedProfit.Add(profit) -} - -func (r *AccumulatedProfitReport) RecordTrade(fee fixedpoint.Value) { - r.accumulatedFee = r.accumulatedFee.Add(fee) - r.accumulatedTrades += 1 -} - -func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { - // Daily profit - r.dailyProfit.Update(r.accumulatedProfit.Sub(r.previousAccumulatedProfit).Float64()) - r.previousAccumulatedProfit = r.accumulatedProfit - - // Accumulated profit - r.accumulatedProfitPerDay.Update(r.accumulatedProfit.Float64()) - - // Accumulated profit MA - r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) - r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last()) - - // Accumulated Fee - r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) - - // Win ratio - r.winRatioPerDay.Update(tradeStats.WinningRatio.Float64()) - - // Profit factor - r.profitFactorPerDay.Update(tradeStats.ProfitFactor.Float64()) - - // Daily trades - r.dailyTrades.Update(float64(r.accumulatedTrades - r.previousAccumulatedTrades)) - r.previousAccumulatedTrades = r.accumulatedTrades -} - -// Output Accumulated profit report to a TSV file -func (r *AccumulatedProfitReport) Output(symbol string) { - if r.TsvReportPath != "" { - tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath) - if err != nil { - panic(err) - } - defer tsvwiter.Close() - // Output symbol, total acc. profit, acc. profit 60MA, interval acc. profit, fee, win rate, profit factor - _ = tsvwiter.Write([]string{"#", "Symbol", "accumulatedProfit", "accumulatedProfitMA", fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), "accumulatedFee", "accumulatedNetProfit", "winRatio", "profitFactor", "60D trades", "Window", "Multiplier", "FastDEMA", "SlowDEMA", "LinReg"}) - for i := 0; i <= r.NumberOfInterval-1; i++ { - accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i) - accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit) - accumulatedProfitMA := r.accumulatedProfitMAPerDay.Index(r.IntervalWindow * i) - accumulatedProfitMAStr := fmt.Sprintf("%f", accumulatedProfitMA) - intervalAccumulatedProfit := r.dailyProfit.Tail(r.AccumulatedDailyProfitWindow+r.IntervalWindow*i).Sum() - r.dailyProfit.Tail(r.IntervalWindow*i).Sum() - intervalAccumulatedProfitStr := fmt.Sprintf("%f", intervalAccumulatedProfit) - accumulatedFee := fmt.Sprintf("%f", r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) - accumulatedNetProfit := fmt.Sprintf("%f", accumulatedProfit-r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) - winRatio := fmt.Sprintf("%f", r.winRatioPerDay.Index(r.IntervalWindow*i)) - profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i)) - trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum() - tradesStr := fmt.Sprintf("%f", trades) - windowStr := fmt.Sprintf("%d", r.s.Window) - multiplierStr := fmt.Sprintf("%f", r.s.SupertrendMultiplier) - fastDEMAStr := fmt.Sprintf("%d", r.s.FastDEMAWindow) - slowDEMAStr := fmt.Sprintf("%d", r.s.SlowDEMAWindow) - linRegStr := fmt.Sprintf("%d", r.s.LinearRegression.Window) - - _ = tsvwiter.Write([]string{fmt.Sprintf("%d", i+1), symbol, accumulatedProfitStr, accumulatedProfitMAStr, intervalAccumulatedProfitStr, accumulatedFee, accumulatedNetProfit, winRatio, profitFactor, tradesStr, windowStr, multiplierStr, fastDEMAStr, slowDEMAStr, linRegStr}) - } - } -} - type Strategy struct { Environment *bbgo.Environment Market types.Market @@ -237,7 +103,7 @@ type Strategy struct { bbgo.StrategyController // Accumulated profit report - AccumulatedProfitReport *AccumulatedProfitReport `json:"accumulatedProfitReport"` + AccumulatedProfitReport *report.AccumulatedProfitReport `json:"accumulatedProfitReport"` } func (s *Strategy) ID() string { @@ -265,9 +131,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LinearRegression.Interval}) s.ExitMethods.SetAndSubscribe(session, s) - - // Accumulated profit report - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1d}) } // Position control @@ -494,22 +357,20 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Accumulated profit report if bbgo.IsBackTesting { if s.AccumulatedProfitReport == nil { - s.AccumulatedProfitReport = &AccumulatedProfitReport{} + s.AccumulatedProfitReport = &report.AccumulatedProfitReport{} } - s.AccumulatedProfitReport.Initialize(s) - s.orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) { - if profit == nil { - return - } - - s.AccumulatedProfitReport.RecordProfit(profit.Profit) - }) - // s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - // s.AccumulatedProfitReport.RecordTrade(trade.Fee) - // }) - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1d, func(kline types.KLine) { - s.AccumulatedProfitReport.DailyUpdate(s.TradeStats) - })) + s.AccumulatedProfitReport.Initialize(s.Symbol, session, s.orderExecutor, s.TradeStats) + + // Add strategy parameters to report + s.AccumulatedProfitReport.AddExtraValue([2]string{"window", fmt.Sprintf("%d", s.Window)}) + s.AccumulatedProfitReport.AddExtraValue([2]string{"multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)}) + s.AccumulatedProfitReport.AddExtraValue([2]string{"fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)}) + s.AccumulatedProfitReport.AddExtraValue([2]string{"slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)}) + s.AccumulatedProfitReport.AddExtraValue([2]string{"takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)}) + s.AccumulatedProfitReport.AddExtraValue([2]string{"stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)}) + s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)}) + s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)}) + s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)}) } // For drawing @@ -519,10 +380,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se cumProfitSlice := floats.Slice{initAsset, initAsset} s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - if bbgo.IsBackTesting { - s.AccumulatedProfitReport.RecordTrade(trade.Fee) - } - // For drawing/charting price := trade.Price.Float64() if s.buyPrice > 0 { @@ -663,10 +520,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - // Output accumulated profit report if bbgo.IsBackTesting { + // Output accumulated profit report defer s.AccumulatedProfitReport.Output(s.Symbol) + // Draw graph if s.DrawGraph { if err := s.Draw(&profitSlice, &cumProfitSlice); err != nil { log.WithError(err).Errorf("cannot draw graph") From 7aa673c673f43741cefe7e15cc8ee6d05a9ccbac Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 15 May 2023 20:10:52 +0800 Subject: [PATCH 0835/1392] max: add currency parameter to /api/v3/wallet/:walletType/accounts api --- .../maxapi/v3/get_wallet_accounts_request.go | 2 +- .../get_wallet_accounts_request_requestgen.go | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go index 9fbf54da4a..0b5b951006 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go @@ -11,7 +11,7 @@ type GetWalletAccountsRequest struct { client requestgen.AuthenticatedAPIClient walletType WalletType `param:"walletType,slug,required"` - currency string `param:"currency"` + currency *string `param:"currency,query"` } func (s *Client) NewGetWalletAccountsRequest(walletType WalletType) *GetWalletAccountsRequest { diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go index dc4b0daa88..05061e0c79 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go @@ -13,7 +13,7 @@ import ( ) func (g *GetWalletAccountsRequest) Currency(currency string) *GetWalletAccountsRequest { - g.currency = currency + g.currency = ¤cy return g } @@ -25,6 +25,14 @@ func (g *GetWalletAccountsRequest) WalletType(walletType max.WalletType) *GetWal // GetQueryParameters builds and checks the query parameters and returns url.Values func (g *GetWalletAccountsRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} + // check currency field -> json key currency + if g.currency != nil { + currency := *g.currency + + // assign parameter of currency + params["currency"] = currency + } else { + } query := url.Values{} for _k, _v := range params { @@ -37,11 +45,6 @@ func (g *GetWalletAccountsRequest) GetQueryParameters() (url.Values, error) { // GetParameters builds and checks the parameters and return the result in a map object func (g *GetWalletAccountsRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} - // check currency field -> json key currency - currency := g.currency - - // assign parameter of currency - params["currency"] = currency return params, nil } @@ -138,9 +141,9 @@ func (g *GetWalletAccountsRequest) GetSlugsMap() (map[string]string, error) { func (g *GetWalletAccountsRequest) Do(ctx context.Context) ([]max.Account, error) { - // empty params for GET operation + // no body params var params interface{} - query, err := g.GetParametersQuery() + query, err := g.GetQueryParameters() if err != nil { return nil, err } From 9b9d7455ec2b7198041dc29fdf136624c3c54a03 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Apr 2023 16:02:03 +0800 Subject: [PATCH 0836/1392] bbgo: move Fast* methods to the FastOrderExecutor --- pkg/bbgo/order_executor_fast.go | 67 ++++++++++++++++++++++++++++++ pkg/bbgo/order_executor_general.go | 45 -------------------- 2 files changed, 67 insertions(+), 45 deletions(-) create mode 100644 pkg/bbgo/order_executor_fast.go diff --git a/pkg/bbgo/order_executor_fast.go b/pkg/bbgo/order_executor_fast.go new file mode 100644 index 0000000000..ef8d407628 --- /dev/null +++ b/pkg/bbgo/order_executor_fast.go @@ -0,0 +1,67 @@ +package bbgo + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/types" +) + +// FastOrderExecutor provides shorter submit order / cancel order round-trip time +// for strategies that need to response more faster, e.g. 1s kline or market trades related strategies. +type FastOrderExecutor struct { + *GeneralOrderExecutor +} + +func NewFastOrderExecutor(session *ExchangeSession, symbol, strategy, strategyInstanceID string, position *types.Position) *FastOrderExecutor { + oe := NewGeneralOrderExecutor(session, symbol, strategy, strategyInstanceID, position) + return &FastOrderExecutor{ + GeneralOrderExecutor: oe, + } +} + +// SubmitOrders sends []types.SubmitOrder directly to the exchange without blocking wait on the status update. +// This is a faster version of GeneralOrderExecutor.SubmitOrders(). Created orders will be consumed in newly created goroutine (in non-backteset session). +// @param ctx: golang context type. +// @param submitOrders: Lists of types.SubmitOrder to be sent to the exchange. +// @return *types.SubmitOrder: SubmitOrder with calculated quantity and price. +// @return error: Error message. +func (e *FastOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { + formattedOrders, err := e.session.FormatOrders(submitOrders) + if err != nil { + return nil, err + } + + createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, nil, formattedOrders...) + if len(errIdx) > 0 { + return nil, err + } + + if IsBackTesting { + e.orderStore.Add(createdOrders...) + e.activeMakerOrders.Add(createdOrders...) + e.tradeCollector.Process() + } else { + go func() { + e.orderStore.Add(createdOrders...) + e.activeMakerOrders.Add(createdOrders...) + e.tradeCollector.Process() + }() + } + return createdOrders, err + +} + +// Cancel cancels all active maker orders if orders is not given, otherwise cancel the given orders +func (e *FastOrderExecutor) Cancel(ctx context.Context, orders ...types.Order) error { + if e.activeMakerOrders.NumOfOrders() == 0 { + return nil + } + + if err := e.activeMakerOrders.FastCancel(ctx, e.session.Exchange, orders...); err != nil { + return errors.Wrap(err, "fast cancel order error") + } + + return nil +} diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index a845ef4999..8faaaaa1f9 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -190,38 +190,6 @@ func (e *GeneralOrderExecutor) CancelOrders(ctx context.Context, orders ...types return err } -// FastSubmitOrders send []types.SubmitOrder directly to the exchange without blocking wait on the status update. -// This is a faster version of SubmitOrders(). Created orders will be consumed in newly created goroutine (in non-backteset session). -// @param ctx: golang context type. -// @param submitOrders: Lists of types.SubmitOrder to be sent to the exchange. -// @return *types.SubmitOrder: SubmitOrder with calculated quantity and price. -// @return error: Error message. -func (e *GeneralOrderExecutor) FastSubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { - formattedOrders, err := e.session.FormatOrders(submitOrders) - if err != nil { - return nil, err - } - - createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, nil, formattedOrders...) - if len(errIdx) > 0 { - return nil, err - } - - if IsBackTesting { - e.orderStore.Add(createdOrders...) - e.activeMakerOrders.Add(createdOrders...) - e.tradeCollector.Process() - } else { - go func() { - e.orderStore.Add(createdOrders...) - e.activeMakerOrders.Add(createdOrders...) - e.tradeCollector.Process() - }() - } - return createdOrders, err - -} - func (e *GeneralOrderExecutor) SetLogger(logger log.FieldLogger) { e.logger = logger } @@ -467,19 +435,6 @@ func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context, orders ...typ return nil } -// FastCancel cancels all active maker orders if orders is not given, otherwise cancel the given orders -func (e *GeneralOrderExecutor) FastCancel(ctx context.Context, orders ...types.Order) error { - if e.activeMakerOrders.NumOfOrders() == 0 { - return nil - } - - if err := e.activeMakerOrders.FastCancel(ctx, e.session.Exchange, orders...); err != nil { - return errors.Wrap(err, "fast cancel order error") - } - - return nil -} - // ClosePosition closes the current position by a percentage. // percentage 0.1 means close 10% position // tag is the order tag you want to attach, you may pass multiple tags, the tags will be combined into one tag string by commas. From 027fe9f5e12bc30ce846886b7ebf7098034de0ce Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 4 May 2023 13:50:01 +0800 Subject: [PATCH 0837/1392] drift: adopt the fastOrderExecutor --- pkg/strategy/drift/strategy.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index b3519442ec..21f748e4cd 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -121,7 +121,8 @@ type Strategy struct { ExitMethods bbgo.ExitMethodSet `json:"exits"` Session *bbgo.ExchangeSession - *bbgo.GeneralOrderExecutor + + *bbgo.FastOrderExecutor getLastPrice func() fixedpoint.Value } @@ -283,7 +284,7 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { } func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64, syscounter int) (int, error) { - nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders() + nonTraded := s.FastOrderExecutor.ActiveMakerOrders().Orders() if len(nonTraded) > 0 { if len(nonTraded) > 1 { log.Errorf("should only have one order to cancel, got %d", len(nonTraded)) @@ -316,7 +317,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64, syscoun } } if toCancel { - err := s.GeneralOrderExecutor.FastCancel(ctx) + err := s.FastOrderExecutor.Cancel(ctx) s.pendingLock.Lock() counters := s.orderPendingCounter s.orderPendingCounter = make(map[uint64]int) @@ -424,7 +425,7 @@ func (s *Strategy) Rebalance(ctx context.Context) { if math.Abs(beta) > s.RebalanceFilter && math.Abs(s.beta) > s.RebalanceFilter || math.Abs(s.beta) < s.RebalanceFilter && math.Abs(beta) < s.RebalanceFilter { return } - balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + balances := s.FastOrderExecutor.Session().GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Total() quoteBalance := balances[s.Market.QuoteCurrency].Total() total := baseBalance.Add(quoteBalance.Div(price)) @@ -578,7 +579,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter } if s.Debug { - balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + balances := s.FastOrderExecutor.Session().GetAccount().Balances() bbgo.Notify("source: %.4f, price: %.4f, drift[0]: %.4f, ddrift[0]: %.4f, lowf %.4f, highf: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", sourcef, pricef, drift[0], ddrift[0], atr, lowf, highf, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) // Notify will parse args to strings and process separately @@ -640,7 +641,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter opt.Price = source opt.Tags = []string{"long"} - submitOrder, err := s.GeneralOrderExecutor.NewOrderFromOpenPosition(ctx, &opt) + submitOrder, err := s.FastOrderExecutor.NewOrderFromOpenPosition(ctx, &opt) if err != nil { errs := filterErrors(multierr.Errors(err)) if len(errs) > 0 { @@ -690,7 +691,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter } opt.Price = source opt.Tags = []string{"short"} - submitOrder, err := s.GeneralOrderExecutor.NewOrderFromOpenPosition(ctx, &opt) + submitOrder, err := s.FastOrderExecutor.NewOrderFromOpenPosition(ctx, &opt) if err != nil { errs := filterErrors(multierr.Errors(err)) if len(errs) > 0 { @@ -745,11 +746,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Status = types.StrategyStatusRunning s.OnSuspend(func() { - _ = s.GeneralOrderExecutor.GracefulCancel(ctx) + _ = s.FastOrderExecutor.GracefulCancel(ctx) }) s.OnEmergencyStop(func() { - _ = s.GeneralOrderExecutor.GracefulCancel(ctx) + _ = s.FastOrderExecutor.GracefulCancel(ctx) _ = s.ClosePosition(ctx, fixedpoint.One) }) @@ -766,14 +767,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } - s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) - s.GeneralOrderExecutor.DisableNotify() - orderStore := s.GeneralOrderExecutor.OrderStore() + s.FastOrderExecutor = bbgo.NewFastOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.FastOrderExecutor.DisableNotify() + orderStore := s.FastOrderExecutor.OrderStore() orderStore.AddOrderUpdate = true orderStore.RemoveCancelled = true orderStore.RemoveFilled = true - activeOrders := s.GeneralOrderExecutor.ActiveMakerOrders() - tradeCollector := s.GeneralOrderExecutor.TradeCollector() + activeOrders := s.FastOrderExecutor.ActiveMakerOrders() + tradeCollector := s.FastOrderExecutor.TradeCollector() tradeStore := tradeCollector.TradeStore() syscounter := 0 From 17b05b61ba059a0660916ec38413645989e62745 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 16:44:40 +0800 Subject: [PATCH 0838/1392] strategy: fix fastCancel api calls --- pkg/strategy/elliottwave/strategy.go | 2 +- pkg/strategy/irr/strategy.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go index 3c42cfd6a9..feabe13b55 100644 --- a/pkg/strategy/elliottwave/strategy.go +++ b/pkg/strategy/elliottwave/strategy.go @@ -211,7 +211,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int { panic("not supported side for the order") } if toCancel { - err := s.GeneralOrderExecutor.FastCancel(ctx, order) + err := s.GeneralOrderExecutor.CancelOrders(ctx, order) if err == nil { delete(s.orderPendingCounter, order.OrderID) } else { diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index 6a7698eef3..679c50ef52 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -343,7 +343,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se diffQty := targetBase.Sub(s.Position.Base) log.Info(alphaNrr.Float64(), s.Position.Base, diffQty.Float64()) - s.orderExecutor.FastCancel(ctx) + if err := s.orderExecutor.CancelOrders(ctx); err != nil { + log.WithError(err).Errorf("cancel order error") + } + if diffQty.Sign() > 0 { _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, From 7146ce9c8b3ce3c794f886c9764b959de026fd44 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 17:14:23 +0800 Subject: [PATCH 0839/1392] bitget: add basic bitget api client --- pkg/exchange/bitget/bitgetapi/client.go | 162 ++++++++++++++++++++++++ pkg/exchange/bitget/bitgetapi/types.go | 36 ++++++ 2 files changed, 198 insertions(+) create mode 100644 pkg/exchange/bitget/bitgetapi/client.go create mode 100644 pkg/exchange/bitget/bitgetapi/types.go diff --git a/pkg/exchange/bitget/bitgetapi/client.go b/pkg/exchange/bitget/bitgetapi/client.go new file mode 100644 index 0000000000..b8de1bf53b --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/client.go @@ -0,0 +1,162 @@ +package bitgetapi + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "net/http" + "net/url" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/util" +) + +const defaultHTTPTimeout = time.Second * 15 +const RestBaseURL = "https://api.bitget.com" +const PublicWebSocketURL = "wss://ws.bitget.com/spot/v1/stream" +const PrivateWebSocketURL = "wss://ws.bitget.com/spot/v1/stream" + +type RestClient struct { + BaseURL *url.URL + client *http.Client + + Key, Secret, Passphrase string +} + +func NewClient() *RestClient { + u, err := url.Parse(RestBaseURL) + if err != nil { + panic(err) + } + + return &RestClient{ + BaseURL: u, + client: &http.Client{ + Timeout: defaultHTTPTimeout, + }, + } +} + +func (c *RestClient) Auth(key, secret, passphrase string) { + c.Key = key + // pragma: allowlist nextline secret + c.Secret = secret + c.Passphrase = passphrase +} + +// NewRequest create new API request. Relative url can be provided in refURL. +func (c *RestClient) newRequest(ctx context.Context, method, refURL string, params url.Values, body []byte) (*http.Request, error) { + rel, err := url.Parse(refURL) + if err != nil { + return nil, err + } + + if params != nil { + rel.RawQuery = params.Encode() + } + + pathURL := c.BaseURL.ResolveReference(rel) + return http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) +} + +// sendRequest sends the request to the API server and handle the response +func (c *RestClient) sendRequest(req *http.Request) (*util.Response, error) { + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + // newResponse reads the response body and return a new Response object + response, err := util.NewResponse(resp) + if err != nil { + return response, err + } + + // Check error, if there is an error, return the ErrorResponse struct type + if response.IsError() { + return response, errors.New(string(response.Body)) + } + + return response, nil +} + +// newAuthenticatedRequest creates new http request for authenticated routes. +func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { + if len(c.Key) == 0 { + return nil, errors.New("empty api key") + } + + if len(c.Secret) == 0 { + return nil, errors.New("empty api secret") + } + + rel, err := url.Parse(refURL) + if err != nil { + return nil, err + } + + if params != nil { + rel.RawQuery = params.Encode() + } + + pathURL := c.BaseURL.ResolveReference(rel) + path := pathURL.Path + if rel.RawQuery != "" { + path += "?" + rel.RawQuery + } + + // set location to UTC so that it outputs "2020-12-08T09:08:57.715Z" + t := time.Now().In(time.UTC) + timestamp := t.Format("2006-01-02T15:04:05.999Z07:00") + + var body []byte + + if payload != nil { + switch v := payload.(type) { + case string: + body = []byte(v) + + case []byte: + body = v + + default: + body, err = json.Marshal(v) + if err != nil { + return nil, err + } + } + } + + signKey := timestamp + strings.ToUpper(method) + path + string(body) + signature := Sign(signKey, c.Secret) + + req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + req.Header.Add("ACCESS-KEY", c.Key) + req.Header.Add("ACCESS-SIGN", signature) + req.Header.Add("ACCESS-TIMESTAMP", timestamp) + req.Header.Add("ACCESS-PASSPHRASE", c.Passphrase) + return req, nil +} + +func Sign(payload string, secret string) string { + var sig = hmac.New(sha256.New, []byte(secret)) + _, err := sig.Write([]byte(payload)) + if err != nil { + return "" + } + + return base64.StdEncoding.EncodeToString(sig.Sum(nil)) + // return hex.EncodeToString(sig.Sum(nil)) +} diff --git a/pkg/exchange/bitget/bitgetapi/types.go b/pkg/exchange/bitget/bitgetapi/types.go new file mode 100644 index 0000000000..59153cb8dd --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/types.go @@ -0,0 +1,36 @@ +package bitgetapi + +type SideType string + +const ( + SideTypeBuy SideType = "buy" + SideTypeSell SideType = "sell" +) + +type OrderType string + +const ( + OrderTypeMarket OrderType = "market" + OrderTypeLimit OrderType = "limit" + OrderTypePostOnly OrderType = "post_only" + OrderTypeFOK OrderType = "fok" + OrderTypeIOC OrderType = "ioc" +) + +type InstrumentType string + +const ( + InstrumentTypeSpot InstrumentType = "SPOT" + InstrumentTypeSwap InstrumentType = "SWAP" + InstrumentTypeFutures InstrumentType = "FUTURES" + InstrumentTypeOption InstrumentType = "OPTION" +) + +type OrderState string + +const ( + OrderStateCanceled OrderState = "canceled" + OrderStateLive OrderState = "live" + OrderStatePartiallyFilled OrderState = "partially_filled" + OrderStateFilled OrderState = "filled" +) From 177610266dbb5b88a323debe1d723aa8725c17c8 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 18:15:27 +0800 Subject: [PATCH 0840/1392] cmd: add exchangetest cmd and document NewWithEnvVarPrefix --- pkg/cmd/exchangetest.go | 107 ++++++++++++++++++++++++++++++++++++++++ pkg/exchange/factory.go | 2 + 2 files changed, 109 insertions(+) create mode 100644 pkg/cmd/exchangetest.go diff --git a/pkg/cmd/exchangetest.go b/pkg/cmd/exchangetest.go new file mode 100644 index 0000000000..40ed41c57c --- /dev/null +++ b/pkg/cmd/exchangetest.go @@ -0,0 +1,107 @@ +package cmd + +import ( + "context" + "fmt" + "syscall" + "time" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/types" +) + +// go run ./cmd/bbgo kline --exchange=binance --symbol=BTCUSDT +var exchangeTestCmd = &cobra.Command{ + Use: "exchange-test", + Short: "test the exchange", + PreRunE: cobraInitRequired([]string{ + "session", + "symbol", + "interval", + }), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + environ := bbgo.NewEnvironment() + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { + return err + } + + sessionName, err := cmd.Flags().GetString("session") + if err != nil { + return err + } + + session, ok := environ.Session(sessionName) + if !ok { + return fmt.Errorf("session %s not found", sessionName) + } + + symbol, err := cmd.Flags().GetString("symbol") + if err != nil { + return fmt.Errorf("can not get the symbol from flags: %w", err) + } + + if symbol == "" { + return fmt.Errorf("--symbol option is required") + } + + interval, err := cmd.Flags().GetString("interval") + if err != nil { + return err + } + + now := time.Now() + kLines, err := session.Exchange.QueryKLines(ctx, symbol, types.Interval(interval), types.KLineQueryOptions{ + Limit: 50, + EndTime: &now, + }) + if err != nil { + return err + } + log.Infof("kLines from RESTful API") + for _, k := range kLines { + log.Info(k.String()) + } + + s := session.Exchange.NewStream() + s.SetPublicOnly() + s.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: types.Interval(interval)}) + + s.OnKLineClosed(func(kline types.KLine) { + log.Infof("kline closed: %s", kline.String()) + }) + + s.OnKLine(func(kline types.KLine) { + log.Infof("kline: %s", kline.String()) + }) + + log.Infof("connecting...") + if err := s.Connect(ctx); err != nil { + return err + } + + log.Infof("connected") + defer func() { + log.Infof("closing connection...") + if err := s.Close(); err != nil { + log.WithError(err).Errorf("connection close error") + } + }() + + cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) + return nil + }, +} + +func init() { + // since the public data does not require trading authentication, we use --exchange option here. + exchangeTestCmd.Flags().String("session", "", "session name") + exchangeTestCmd.Flags().String("symbol", "", "the trading pair. e.g, BTCUSDT, LTCUSDT...") + exchangeTestCmd.Flags().String("interval", "1m", "interval of the kline (candle), .e.g, 1m, 3m, 15m") + RootCmd.AddCommand(exchangeTestCmd) +} diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index 436b03b5bb..370331ab97 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -37,6 +37,8 @@ func NewStandard(n types.ExchangeName, key, secret, passphrase, subAccount strin } } +// NewWithEnvVarPrefix allocate and initialize the exchange instance with the given environment variable prefix +// When the varPrefix is a empty string, the default exchange name will be used as the prefix func NewWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange, error) { if len(varPrefix) == 0 { varPrefix = n.String() From 0b6dc410912c81b83751cd3041a58c549e2b5f19 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 18:17:11 +0800 Subject: [PATCH 0841/1392] types: split exchange interface for ExchangeMinimal --- pkg/types/exchange.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index 800ac5be62..f4a3c11e7e 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -70,11 +70,14 @@ func ValidExchangeName(a string) (ExchangeName, error) { return "", fmt.Errorf("invalid exchange name: %s", a) } -//go:generate mockgen -destination=mocks/mock_exchange.go -package=mocks . Exchange -type Exchange interface { +type ExchangeMinimal interface { Name() ExchangeName - PlatformFeeCurrency() string +} + +//go:generate mockgen -destination=mocks/mock_exchange.go -package=mocks . Exchange +type Exchange interface { + ExchangeMinimal ExchangeMarketDataService @@ -82,6 +85,7 @@ type Exchange interface { } // ExchangeOrderQueryService provides an interface for querying the order status via order ID or client order ID +// //go:generate mockgen -destination=mocks/mock_exchange_order_query.go -package=mocks . ExchangeOrderQueryService type ExchangeOrderQueryService interface { QueryOrder(ctx context.Context, q OrderQuery) (*Order, error) From 983707b56aaff219d6622c25b38ed2e5cc235729 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 18:21:18 +0800 Subject: [PATCH 0842/1392] exchange: drop unused function --- pkg/exchange/factory.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index 370331ab97..6f517aa092 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -56,8 +56,3 @@ func NewWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange subAccount := os.Getenv(varPrefix + "_SUBACCOUNT") return NewStandard(n, key, secret, passphrase, subAccount) } - -// New constructor exchange object from viper config. -func New(n types.ExchangeName) (types.Exchange, error) { - return NewWithEnvVarPrefix(n, "") -} From 5e8f8b492a5da6ba97dacc4e765c07cea1b38f86 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 18:21:47 +0800 Subject: [PATCH 0843/1392] all: remove unused subAccount parameter since it was designed for ftx --- pkg/bbgo/session.go | 2 +- pkg/exchange/factory.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 7aa3ba8b15..d72f853a47 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -714,7 +714,7 @@ func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) err ex, err = exchange2.NewPublic(exchangeName) } else { if session.Key != "" && session.Secret != "" { - ex, err = exchange2.NewStandard(exchangeName, session.Key, session.Secret, session.Passphrase, session.SubAccount) + ex, err = exchange2.NewStandard(exchangeName, session.Key, session.Secret, session.Passphrase) } else { ex, err = exchange2.NewWithEnvVarPrefix(exchangeName, session.EnvVarPrefix) } diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index 6f517aa092..bd351a8ec7 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -13,10 +13,10 @@ import ( ) func NewPublic(exchangeName types.ExchangeName) (types.Exchange, error) { - return NewStandard(exchangeName, "", "", "", "") + return NewStandard(exchangeName, "", "", "") } -func NewStandard(n types.ExchangeName, key, secret, passphrase, subAccount string) (types.Exchange, error) { +func NewStandard(n types.ExchangeName, key, secret, passphrase string) (types.Exchange, error) { switch n { case types.ExchangeBinance: @@ -54,5 +54,5 @@ func NewWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange passphrase := os.Getenv(varPrefix + "_API_PASSPHRASE") subAccount := os.Getenv(varPrefix + "_SUBACCOUNT") - return NewStandard(n, key, secret, passphrase, subAccount) + return NewStandard(n, key, secret, passphrase) } From 420654c5ed4e0b01dbecaa2db20edea40206725e Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 18:24:06 +0800 Subject: [PATCH 0844/1392] bbgo: rename NewStandard to just New --- pkg/bbgo/session.go | 2 +- pkg/exchange/factory.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index d72f853a47..3df5a75308 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -714,7 +714,7 @@ func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) err ex, err = exchange2.NewPublic(exchangeName) } else { if session.Key != "" && session.Secret != "" { - ex, err = exchange2.NewStandard(exchangeName, session.Key, session.Secret, session.Passphrase) + ex, err = exchange2.New(exchangeName, session.Key, session.Secret, session.Passphrase) } else { ex, err = exchange2.NewWithEnvVarPrefix(exchangeName, session.EnvVarPrefix) } diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index bd351a8ec7..b507e10a5b 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -13,10 +13,10 @@ import ( ) func NewPublic(exchangeName types.ExchangeName) (types.Exchange, error) { - return NewStandard(exchangeName, "", "", "") + return New(exchangeName, "", "", "") } -func NewStandard(n types.ExchangeName, key, secret, passphrase string) (types.Exchange, error) { +func New(n types.ExchangeName, key, secret, passphrase string) (types.Exchange, error) { switch n { case types.ExchangeBinance: @@ -54,5 +54,5 @@ func NewWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange passphrase := os.Getenv(varPrefix + "_API_PASSPHRASE") subAccount := os.Getenv(varPrefix + "_SUBACCOUNT") - return NewStandard(n, key, secret, passphrase) + return New(n, key, secret, passphrase) } From 9f1c2f9ae434fcdf9701b822f6b3535a4dff5df2 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 18:26:55 +0800 Subject: [PATCH 0845/1392] types: update exchange name constants --- pkg/types/exchange.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index f4a3c11e7e..d69b51442d 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -44,6 +44,7 @@ const ( ExchangeBinance ExchangeName = "binance" ExchangeOKEx ExchangeName = "okex" ExchangeKucoin ExchangeName = "kucoin" + ExchangeBitget ExchangeName = "bitget" ExchangeBacktest ExchangeName = "backtest" ) @@ -52,6 +53,7 @@ var SupportedExchanges = []ExchangeName{ ExchangeBinance, ExchangeOKEx, ExchangeKucoin, + ExchangeBitget, // note: we are not using "backtest" } From ad502f67e985f9e7a7c8184bb4280073c30aea08 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 18:32:08 +0800 Subject: [PATCH 0846/1392] types: simplify ValidExchangeName function --- pkg/types/exchange.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index d69b51442d..1eb0887424 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -58,15 +58,11 @@ var SupportedExchanges = []ExchangeName{ } func ValidExchangeName(a string) (ExchangeName, error) { - switch strings.ToLower(a) { - case "max": - return ExchangeMax, nil - case "binance", "bn": - return ExchangeBinance, nil - case "okex": - return ExchangeOKEx, nil - case "kucoin": - return ExchangeKucoin, nil + aa := strings.ToLower(a) + for _, n := range SupportedExchanges { + if string(n) == aa { + return n, nil + } } return "", fmt.Errorf("invalid exchange name: %s", a) From 70ed672e6fb26851d95fc0969d8d5a7d627b5086 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 19:26:05 +0800 Subject: [PATCH 0847/1392] exchange: remove subAccount var --- pkg/exchange/factory.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index b507e10a5b..f8863f173a 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -31,6 +31,9 @@ func New(n types.ExchangeName, key, secret, passphrase string) (types.Exchange, case types.ExchangeKucoin: return kucoin.New(key, secret, passphrase), nil + // case types.ExchangeBitget: + // return bitget.New(key, secret, passphrase), nil + default: return nil, fmt.Errorf("unsupported exchange: %v", n) @@ -53,6 +56,5 @@ func NewWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange } passphrase := os.Getenv(varPrefix + "_API_PASSPHRASE") - subAccount := os.Getenv(varPrefix + "_SUBACCOUNT") return New(n, key, secret, passphrase) } From ee0dcc788bd9762b8edd0da2192e6f9ae6b77f73 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 16 May 2023 19:26:46 +0800 Subject: [PATCH 0848/1392] add github.com/dmarkham/enumer --- go.mod | 2 ++ go.sum | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/go.mod b/go.mod index d24c248dc0..205e413b4e 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/denisenkom/go-mssqldb v0.12.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dmarkham/enumer v1.5.8 // indirect github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -110,6 +111,7 @@ require ( github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pascaldekloe/name v1.0.0 // indirect github.com/pelletier/go-toml v1.8.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect diff --git a/go.sum b/go.sum index a6f2dc205d..6561eb306f 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM= +github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -532,6 +534,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U= +github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= From b544d51772740a0944f0d3feda7006c8db200563 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 13:24:04 +0800 Subject: [PATCH 0849/1392] types: split exchange interface --- pkg/types/exchange.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index 1eb0887424..f3d8e6eb74 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -76,9 +76,8 @@ type ExchangeMinimal interface { //go:generate mockgen -destination=mocks/mock_exchange.go -package=mocks . Exchange type Exchange interface { ExchangeMinimal - ExchangeMarketDataService - + ExchangeAccountService ExchangeTradeService } @@ -90,11 +89,13 @@ type ExchangeOrderQueryService interface { QueryOrderTrades(ctx context.Context, q OrderQuery) ([]Trade, error) } -type ExchangeTradeService interface { +type ExchangeAccountService interface { QueryAccount(ctx context.Context) (*Account, error) QueryAccountBalances(ctx context.Context) (BalanceMap, error) +} +type ExchangeTradeService interface { SubmitOrder(ctx context.Context, order SubmitOrder) (createdOrder *Order, err error) QueryOpenOrders(ctx context.Context, symbol string) (orders []Order, err error) From 6bed2a31f6c6584d0507cd553d4136a1c0771c18 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 13:43:00 +0800 Subject: [PATCH 0850/1392] all: refactor exchange factory to return the minimal implementation --- pkg/bbgo/environment.go | 8 ++++++-- pkg/bbgo/session.go | 28 +++++++++++++++++++++++----- pkg/exchange/factory.go | 20 +++++++++++++++----- pkg/types/exchange.go | 3 +++ 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index 6bdad5e424..f89f71dbb5 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -228,12 +228,16 @@ func (environ *Environment) ConfigureExchangeSessions(userConfig *Config) error func (environ *Environment) AddExchangesByViperKeys() error { for _, n := range types.SupportedExchanges { if viper.IsSet(string(n) + "-api-key") { - ex, err := exchange.NewWithEnvVarPrefix(n, "") + exMinimal, err := exchange.NewWithEnvVarPrefix(n, "") if err != nil { return err } - environ.AddExchange(n.String(), ex) + if ex, ok := exMinimal.(types.Exchange); ok { + environ.AddExchange(n.String(), ex) + } else { + log.Errorf("exchange %T does not implement types.Exchange", exMinimal) + } } } diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 3df5a75308..895226f69d 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -703,21 +703,39 @@ func (session *ExchangeSession) FindPossibleSymbols() (symbols []string, err err return symbols, nil } +// newBasicPrivateExchange allocates a basic exchange instance with the user private credentials +func (session *ExchangeSession) newBasicPrivateExchange(exchangeName types.ExchangeName) (types.Exchange, error) { + var err error + var exMinimal types.ExchangeMinimal + if session.Key != "" && session.Secret != "" { + exMinimal, err = exchange2.New(exchangeName, session.Key, session.Secret, session.Passphrase) + } else { + exMinimal, err = exchange2.NewWithEnvVarPrefix(exchangeName, session.EnvVarPrefix) + } + + if err != nil { + return nil, err + } + + if ex, ok := exMinimal.(types.Exchange); ok { + return ex, nil + } + + return nil, fmt.Errorf("exchange %T does not implement types.Exchange", exMinimal) +} + // InitExchange initialize the exchange instance and allocate memory for fields // In this stage, the session var could be loaded from the JSON config, so the pointer fields are still nil // The Init method will be called after this stage, environment.Init will call the session.Init method later. func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) error { var err error var exchangeName = session.ExchangeName + if ex == nil { if session.PublicOnly { ex, err = exchange2.NewPublic(exchangeName) } else { - if session.Key != "" && session.Secret != "" { - ex, err = exchange2.New(exchangeName, session.Key, session.Secret, session.Passphrase) - } else { - ex, err = exchange2.NewWithEnvVarPrefix(exchangeName, session.EnvVarPrefix) - } + ex, err = session.newBasicPrivateExchange(exchangeName) } } diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index f8863f173a..95bd53b879 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/exchange/bitget" "github.com/c9s/bbgo/pkg/exchange/kucoin" "github.com/c9s/bbgo/pkg/exchange/max" "github.com/c9s/bbgo/pkg/exchange/okex" @@ -13,10 +14,19 @@ import ( ) func NewPublic(exchangeName types.ExchangeName) (types.Exchange, error) { - return New(exchangeName, "", "", "") + exMinimal, err := New(exchangeName, "", "", "") + if err != nil { + return nil, err + } + + if ex, ok := exMinimal.(types.Exchange); ok { + return ex, nil + } + + return nil, fmt.Errorf("exchange %T does not implement types.Exchange", exMinimal) } -func New(n types.ExchangeName, key, secret, passphrase string) (types.Exchange, error) { +func New(n types.ExchangeName, key, secret, passphrase string) (types.ExchangeMinimal, error) { switch n { case types.ExchangeBinance: @@ -31,8 +41,8 @@ func New(n types.ExchangeName, key, secret, passphrase string) (types.Exchange, case types.ExchangeKucoin: return kucoin.New(key, secret, passphrase), nil - // case types.ExchangeBitget: - // return bitget.New(key, secret, passphrase), nil + case types.ExchangeBitget: + return bitget.New(key, secret, passphrase), nil default: return nil, fmt.Errorf("unsupported exchange: %v", n) @@ -42,7 +52,7 @@ func New(n types.ExchangeName, key, secret, passphrase string) (types.Exchange, // NewWithEnvVarPrefix allocate and initialize the exchange instance with the given environment variable prefix // When the varPrefix is a empty string, the default exchange name will be used as the prefix -func NewWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange, error) { +func NewWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.ExchangeMinimal, error) { if len(varPrefix) == 0 { varPrefix = n.String() } diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index f3d8e6eb74..36670c35e7 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -81,6 +81,9 @@ type Exchange interface { ExchangeTradeService } +// ExchangeBasic is the new type for replacing the original Exchange interface +type ExchangeBasic = Exchange + // ExchangeOrderQueryService provides an interface for querying the order status via order ID or client order ID // //go:generate mockgen -destination=mocks/mock_exchange_order_query.go -package=mocks . ExchangeOrderQueryService From 5f8bda7d72933772b200bd746e5923361ec0e5eb Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 13:43:21 +0800 Subject: [PATCH 0851/1392] bitget: add minimal bitget exchange --- pkg/exchange/bitget/exchange.go | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 pkg/exchange/bitget/exchange.go diff --git a/pkg/exchange/bitget/exchange.go b/pkg/exchange/bitget/exchange.go new file mode 100644 index 0000000000..0bf7d11f50 --- /dev/null +++ b/pkg/exchange/bitget/exchange.go @@ -0,0 +1,45 @@ +package bitget + +import ( + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "bitget" + +const PlatformToken = "BGB" + +var log = logrus.WithFields(logrus.Fields{ + "exchange": ID, +}) + +type Exchange struct { + key, secret, passphrase string + + client *bitgetapi.RestClient +} + +func New(key, secret, passphrase string) *Exchange { + client := bitgetapi.NewClient() + + if len(key) > 0 && len(secret) > 0 { + client.Auth(key, secret, passphrase) + } + + return &Exchange{ + key: key, + secret: secret, + passphrase: passphrase, + client: client, + } +} + +func (e *Exchange) Name() types.ExchangeName { + return types.ExchangeBitget +} + +func (e *Exchange) PlatformFeeCurrency() string { + return PlatformToken +} From f942f7afd8890f276ebc78577a8a811371ef16f2 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 13:45:38 +0800 Subject: [PATCH 0852/1392] okex: rename constant names --- pkg/exchange/okex/exchange.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 0bebbc62f7..0ad66181f0 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -17,11 +17,13 @@ import ( var marketDataLimiter = rate.NewLimiter(rate.Every(time.Second/10), 1) -// OKB is the platform currency of OKEx, pre-allocate static string here -const OKB = "OKB" +const ID = "okex" + +// PlatformToken is the platform currency of OKEx, pre-allocate static string here +const PlatformToken = "OKB" var log = logrus.WithFields(logrus.Fields{ - "exchange": "okex", + "exchange": ID, }) type Exchange struct { @@ -38,8 +40,7 @@ func New(key, secret, passphrase string) *Exchange { } return &Exchange{ - key: key, - // pragma: allowlist nextline secret + key: key, secret: secret, passphrase: passphrase, client: client, @@ -131,7 +132,7 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[str } func (e *Exchange) PlatformFeeCurrency() string { - return OKB + return PlatformToken } func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { From e23f4b5114c1e28ef23fe7cc3ef917c03df7a2b4 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 14:26:25 +0800 Subject: [PATCH 0853/1392] bitget: minimize api client code --- pkg/cmd/exchangetest.go | 86 ++++++----------------- pkg/exchange/bitget/bitgetapi/client.go | 91 ++++++++----------------- 2 files changed, 49 insertions(+), 128 deletions(-) diff --git a/pkg/cmd/exchangetest.go b/pkg/cmd/exchangetest.go index 40ed41c57c..df756a68aa 100644 --- a/pkg/cmd/exchangetest.go +++ b/pkg/cmd/exchangetest.go @@ -1,16 +1,15 @@ +//go:build exchangetest +// +build exchangetest + package cmd import ( "context" - "fmt" - "syscall" - "time" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/types" ) @@ -18,90 +17,47 @@ import ( var exchangeTestCmd = &cobra.Command{ Use: "exchange-test", Short: "test the exchange", - PreRunE: cobraInitRequired([]string{ - "session", - "symbol", - "interval", - }), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - environ := bbgo.NewEnvironment() - if err := environ.ConfigureExchangeSessions(userConfig); err != nil { - return err - } - - sessionName, err := cmd.Flags().GetString("session") + exchangeNameStr, err := cmd.Flags().GetString("exchange") if err != nil { return err } - session, ok := environ.Session(sessionName) - if !ok { - return fmt.Errorf("session %s not found", sessionName) - } - - symbol, err := cmd.Flags().GetString("symbol") - if err != nil { - return fmt.Errorf("can not get the symbol from flags: %w", err) - } - - if symbol == "" { - return fmt.Errorf("--symbol option is required") - } - - interval, err := cmd.Flags().GetString("interval") + exchangeName, err := types.ValidExchangeName(exchangeNameStr) if err != nil { return err } - now := time.Now() - kLines, err := session.Exchange.QueryKLines(ctx, symbol, types.Interval(interval), types.KLineQueryOptions{ - Limit: 50, - EndTime: &now, - }) + exMinimal, err := exchange.NewWithEnvVarPrefix(exchangeName, "") if err != nil { return err } - log.Infof("kLines from RESTful API") - for _, k := range kLines { - log.Info(k.String()) - } - - s := session.Exchange.NewStream() - s.SetPublicOnly() - s.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: types.Interval(interval)}) - s.OnKLineClosed(func(kline types.KLine) { - log.Infof("kline closed: %s", kline.String()) - }) + log.Infof("types.ExchangeMinimal: ✅") - s.OnKLine(func(kline types.KLine) { - log.Infof("kline: %s", kline.String()) - }) + if service, ok := exMinimal.(types.ExchangeAccountService); ok { + log.Infof("types.ExchangeAccountService: ✅ (%T)", service) + } - log.Infof("connecting...") - if err := s.Connect(ctx); err != nil { - return err + if service, ok := exMinimal.(types.ExchangeMarketDataService); ok { + log.Infof("types.ExchangeMarketDataService: ✅ (%T)", service) } - log.Infof("connected") - defer func() { - log.Infof("closing connection...") - if err := s.Close(); err != nil { - log.WithError(err).Errorf("connection close error") - } - }() + if ex, ok := exMinimal.(types.Exchange); ok { + log.Infof("types.Exchange: ✅ (%T)", ex) + } - cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) + _ = ctx + // cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) return nil }, } func init() { - // since the public data does not require trading authentication, we use --exchange option here. - exchangeTestCmd.Flags().String("session", "", "session name") - exchangeTestCmd.Flags().String("symbol", "", "the trading pair. e.g, BTCUSDT, LTCUSDT...") - exchangeTestCmd.Flags().String("interval", "1m", "interval of the kline (candle), .e.g, 1m, 3m, 15m") + exchangeTestCmd.Flags().String("exchange", "", "session name") + exchangeTestCmd.MarkFlagRequired("exchange") + RootCmd.AddCommand(exchangeTestCmd) } diff --git a/pkg/exchange/bitget/bitgetapi/client.go b/pkg/exchange/bitget/bitgetapi/client.go index b8de1bf53b..eefb72af2c 100644 --- a/pkg/exchange/bitget/bitgetapi/client.go +++ b/pkg/exchange/bitget/bitgetapi/client.go @@ -12,9 +12,8 @@ import ( "strings" "time" + "github.com/c9s/requestgen" "github.com/pkg/errors" - - "github.com/c9s/bbgo/pkg/util" ) const defaultHTTPTimeout = time.Second * 15 @@ -23,8 +22,7 @@ const PublicWebSocketURL = "wss://ws.bitget.com/spot/v1/stream" const PrivateWebSocketURL = "wss://ws.bitget.com/spot/v1/stream" type RestClient struct { - BaseURL *url.URL - client *http.Client + requestgen.BaseAPIClient Key, Secret, Passphrase string } @@ -36,56 +34,21 @@ func NewClient() *RestClient { } return &RestClient{ - BaseURL: u, - client: &http.Client{ - Timeout: defaultHTTPTimeout, + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: &http.Client{ + Timeout: defaultHTTPTimeout, + }, }, } } func (c *RestClient) Auth(key, secret, passphrase string) { c.Key = key - // pragma: allowlist nextline secret c.Secret = secret c.Passphrase = passphrase } -// NewRequest create new API request. Relative url can be provided in refURL. -func (c *RestClient) newRequest(ctx context.Context, method, refURL string, params url.Values, body []byte) (*http.Request, error) { - rel, err := url.Parse(refURL) - if err != nil { - return nil, err - } - - if params != nil { - rel.RawQuery = params.Encode() - } - - pathURL := c.BaseURL.ResolveReference(rel) - return http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) -} - -// sendRequest sends the request to the API server and handle the response -func (c *RestClient) sendRequest(req *http.Request) (*util.Response, error) { - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - // newResponse reads the response body and return a new Response object - response, err := util.NewResponse(resp) - if err != nil { - return response, err - } - - // Check error, if there is an error, return the ErrorResponse struct type - if response.IsError() { - return response, errors.New(string(response.Body)) - } - - return response, nil -} - // newAuthenticatedRequest creates new http request for authenticated routes. func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { if len(c.Key) == 0 { @@ -115,26 +78,13 @@ func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL t := time.Now().In(time.UTC) timestamp := t.Format("2006-01-02T15:04:05.999Z07:00") - var body []byte - - if payload != nil { - switch v := payload.(type) { - case string: - body = []byte(v) - - case []byte: - body = v - - default: - body, err = json.Marshal(v) - if err != nil { - return nil, err - } - } + body, err := castPayload(payload) + if err != nil { + return nil, err } signKey := timestamp + strings.ToUpper(method) + path + string(body) - signature := Sign(signKey, c.Secret) + signature := sign(signKey, c.Secret) req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) if err != nil { @@ -150,7 +100,7 @@ func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL return req, nil } -func Sign(payload string, secret string) string { +func sign(payload string, secret string) string { var sig = hmac.New(sha256.New, []byte(secret)) _, err := sig.Write([]byte(payload)) if err != nil { @@ -158,5 +108,20 @@ func Sign(payload string, secret string) string { } return base64.StdEncoding.EncodeToString(sig.Sum(nil)) - // return hex.EncodeToString(sig.Sum(nil)) +} + +func castPayload(payload interface{}) ([]byte, error) { + if payload == nil { + return nil, nil + } + + switch v := payload.(type) { + case string: + return []byte(v), nil + + case []byte: + return v, nil + + } + return json.Marshal(payload) } From 0886b287a44638200048e916b27a110310b80eec Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 14:45:53 +0800 Subject: [PATCH 0854/1392] bitget: make credential field in lower case --- pkg/exchange/bitget/bitgetapi/client.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/exchange/bitget/bitgetapi/client.go b/pkg/exchange/bitget/bitgetapi/client.go index eefb72af2c..1637f9c0b2 100644 --- a/pkg/exchange/bitget/bitgetapi/client.go +++ b/pkg/exchange/bitget/bitgetapi/client.go @@ -24,7 +24,7 @@ const PrivateWebSocketURL = "wss://ws.bitget.com/spot/v1/stream" type RestClient struct { requestgen.BaseAPIClient - Key, Secret, Passphrase string + key, secret, passphrase string } func NewClient() *RestClient { @@ -44,18 +44,18 @@ func NewClient() *RestClient { } func (c *RestClient) Auth(key, secret, passphrase string) { - c.Key = key - c.Secret = secret - c.Passphrase = passphrase + c.key = key + c.secret = secret + c.passphrase = passphrase } // newAuthenticatedRequest creates new http request for authenticated routes. func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { - if len(c.Key) == 0 { + if len(c.key) == 0 { return nil, errors.New("empty api key") } - if len(c.Secret) == 0 { + if len(c.secret) == 0 { return nil, errors.New("empty api secret") } @@ -84,7 +84,7 @@ func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL } signKey := timestamp + strings.ToUpper(method) + path + string(body) - signature := sign(signKey, c.Secret) + signature := sign(signKey, c.secret) req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) if err != nil { @@ -93,10 +93,10 @@ func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL req.Header.Add("Content-Type", "application/json") req.Header.Add("Accept", "application/json") - req.Header.Add("ACCESS-KEY", c.Key) + req.Header.Add("ACCESS-KEY", c.key) req.Header.Add("ACCESS-SIGN", signature) req.Header.Add("ACCESS-TIMESTAMP", timestamp) - req.Header.Add("ACCESS-PASSPHRASE", c.Passphrase) + req.Header.Add("ACCESS-PASSPHRASE", c.passphrase) return req, nil } From 71be12bfc3ed975be68210f4a07589bbad385af1 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 16:23:39 +0800 Subject: [PATCH 0855/1392] bitget: adjust sign format --- pkg/exchange/bitget/bitgetapi/client.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/bitget/bitgetapi/client.go b/pkg/exchange/bitget/bitgetapi/client.go index 1637f9c0b2..87520ff8d2 100644 --- a/pkg/exchange/bitget/bitgetapi/client.go +++ b/pkg/exchange/bitget/bitgetapi/client.go @@ -9,6 +9,7 @@ import ( "encoding/json" "net/http" "net/url" + "strconv" "strings" "time" @@ -74,9 +75,18 @@ func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL path += "?" + rel.RawQuery } + // See https://bitgetlimited.github.io/apidoc/en/spot/#signature + // sign( + // timestamp + + // method.toUpperCase() + + // requestPath + "?" + queryString + + // body **string + // ) + // (+ means string concat) encrypt by **HMAC SHA256 **algorithm, and encode the encrypted result through **BASE64. + // set location to UTC so that it outputs "2020-12-08T09:08:57.715Z" t := time.Now().In(time.UTC) - timestamp := t.Format("2006-01-02T15:04:05.999Z07:00") + timestamp := strconv.FormatInt(t.UnixMilli(), 10) body, err := castPayload(payload) if err != nil { From feb20571e94ab877e8857a437f9451fd89f71da8 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 16:27:43 +0800 Subject: [PATCH 0856/1392] kucoin: split request files --- pkg/exchange/kucoin/kucoinapi/account.go | 19 ------------------- .../kucoin/kucoinapi/get_account_request.go | 16 ++++++++++++++++ .../kucoin/kucoinapi/list_accounts_request.go | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 pkg/exchange/kucoin/kucoinapi/get_account_request.go create mode 100644 pkg/exchange/kucoin/kucoinapi/list_accounts_request.go diff --git a/pkg/exchange/kucoin/kucoinapi/account.go b/pkg/exchange/kucoin/kucoinapi/account.go index 81757093df..c7c66c6bf4 100644 --- a/pkg/exchange/kucoin/kucoinapi/account.go +++ b/pkg/exchange/kucoin/kucoinapi/account.go @@ -17,14 +17,6 @@ func (s *AccountService) NewListSubAccountsRequest() *ListSubAccountsRequest { return &ListSubAccountsRequest{client: s.client} } -func (s *AccountService) NewListAccountsRequest() *ListAccountsRequest { - return &ListAccountsRequest{client: s.client} -} - -func (s *AccountService) NewGetAccountRequest(accountID string) *GetAccountRequest { - return &GetAccountRequest{client: s.client, accountID: accountID} -} - type SubAccount struct { UserID string `json:"userId"` Name string `json:"subName"` @@ -45,14 +37,3 @@ type Account struct { Available fixedpoint.Value `json:"available"` Holds fixedpoint.Value `json:"holds"` } - -//go:generate GetRequest -url "/api/v1/accounts" -type ListAccountsRequest -responseDataType []Account -type ListAccountsRequest struct { - client requestgen.AuthenticatedAPIClient -} - -//go:generate GetRequest -url "/api/v1/accounts/:accountID" -type GetAccountRequest -responseDataType .Account -type GetAccountRequest struct { - client requestgen.AuthenticatedAPIClient - accountID string `param:"accountID,slug"` -} diff --git a/pkg/exchange/kucoin/kucoinapi/get_account_request.go b/pkg/exchange/kucoin/kucoinapi/get_account_request.go new file mode 100644 index 0000000000..842dba7da7 --- /dev/null +++ b/pkg/exchange/kucoin/kucoinapi/get_account_request.go @@ -0,0 +1,16 @@ +package kucoinapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import "github.com/c9s/requestgen" + +//go:generate GetRequest -url "/api/v1/accounts/:accountID" -type GetAccountRequest -responseDataType .Account +type GetAccountRequest struct { + client requestgen.AuthenticatedAPIClient + accountID string `param:"accountID,slug"` +} + +func (s *AccountService) NewGetAccountRequest(accountID string) *GetAccountRequest { + return &GetAccountRequest{client: s.client, accountID: accountID} +} diff --git a/pkg/exchange/kucoin/kucoinapi/list_accounts_request.go b/pkg/exchange/kucoin/kucoinapi/list_accounts_request.go new file mode 100644 index 0000000000..a07f493012 --- /dev/null +++ b/pkg/exchange/kucoin/kucoinapi/list_accounts_request.go @@ -0,0 +1,15 @@ +package kucoinapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import "github.com/c9s/requestgen" + +//go:generate GetRequest -url "/api/v1/accounts" -type ListAccountsRequest -responseDataType []Account +type ListAccountsRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (s *AccountService) NewListAccountsRequest() *ListAccountsRequest { + return &ListAccountsRequest{client: s.client} +} From 2c88e197b61b0797090cdb9800baec5c54bc6233 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 16:39:10 +0800 Subject: [PATCH 0857/1392] bitget: add account api --- pkg/exchange/bitget/bitgetapi/client.go | 26 ++++ .../bitgetapi/get_account_assets_request.go | 29 ++++ .../get_account_assets_request_requestgen.go | 139 ++++++++++++++++++ .../bitget/bitgetapi/get_account_request.go | 24 +++ .../get_account_request_requestgen.go | 139 ++++++++++++++++++ 5 files changed, 357 insertions(+) create mode 100644 pkg/exchange/bitget/bitgetapi/get_account_assets_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_account_assets_request_requestgen.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_account_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_account_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/client.go b/pkg/exchange/bitget/bitgetapi/client.go index 87520ff8d2..19b145d5d7 100644 --- a/pkg/exchange/bitget/bitgetapi/client.go +++ b/pkg/exchange/bitget/bitgetapi/client.go @@ -135,3 +135,29 @@ func castPayload(payload interface{}) ([]byte, error) { } return json.Marshal(payload) } + +/* +sample: + + { + "code": "00000", + "msg": "success", + "data": { + "user_id": "714229403", + "inviter_id": "682221498", + "ips": "172.23.88.91", + "authorities": [ + "trade", + "readonly" + ], + "parentId":"566624801", + "trader":false + } + } +*/ + +type APIResponse struct { + Code string `json:"code"` + Message string `json:"msg"` + Data json.RawMessage `json:"data"` +} diff --git a/pkg/exchange/bitget/bitgetapi/get_account_assets_request.go b/pkg/exchange/bitget/bitgetapi/get_account_assets_request.go new file mode 100644 index 0000000000..03cc6fb18c --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_account_assets_request.go @@ -0,0 +1,29 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type AccountAsset struct { + CoinId string `json:"coinId"` + CoinName string `json:"coinName"` + Available fixedpoint.Value `json:"available"` + Frozen fixedpoint.Value `json:"frozen"` + Lock fixedpoint.Value `json:"lock"` + UTime types.MillisecondTimestamp `json:"uTime"` +} + +//go:generate GetRequest -url "/api/spot/v1/account/assets" -type GetAccountAssetsRequest -responseDataType []AccountAsset +type GetAccountAssetsRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *RestClient) NewGetAccountAssetsRequest() *GetAccountAssetsRequest { + return &GetAccountAssetsRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_account_assets_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_account_assets_request_requestgen.go new file mode 100644 index 0000000000..d6c989b5f4 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_account_assets_request_requestgen.go @@ -0,0 +1,139 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/account/assets -type GetAccountAssetsRequest -responseDataType []AccountAsset"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetAccountAssetsRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetAccountAssetsRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetAccountAssetsRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetAccountAssetsRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetAccountAssetsRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetAccountAssetsRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetAccountAssetsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetAccountAssetsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetAccountAssetsRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetAccountAssetsRequest) Do(ctx context.Context) ([]AccountAsset, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/spot/v1/account/assets" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []AccountAsset + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/bitget/bitgetapi/get_account_request.go b/pkg/exchange/bitget/bitgetapi/get_account_request.go new file mode 100644 index 0000000000..a3ad8743ab --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_account_request.go @@ -0,0 +1,24 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import "github.com/c9s/requestgen" + +type Account struct { + UserId string `json:"user_id"` + InviterId string `json:"inviter_id"` + Ips string `json:"ips"` + Authorities []string `json:"authorities"` + ParentId string `json:"parentId"` + Trader bool `json:"trader"` +} + +//go:generate GetRequest -url "/api/spot/v1/account/getInfo" -type GetAccountRequest -responseDataType .Account +type GetAccountRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *RestClient) NewGetAccountRequest() *GetAccountRequest { + return &GetAccountRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_account_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_account_request_requestgen.go new file mode 100644 index 0000000000..a94390dc81 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_account_request_requestgen.go @@ -0,0 +1,139 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/account/getInfo -type GetAccountRequest -responseDataType .Account"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetAccountRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetAccountRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetAccountRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetAccountRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetAccountRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetAccountRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetAccountRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetAccountRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetAccountRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetAccountRequest) Do(ctx context.Context) (*Account, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/spot/v1/account/getInfo" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data Account + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return &data, nil +} From b726a0e51d8221020f0e6c3fd8da3e62dbdd828a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 16:52:15 +0800 Subject: [PATCH 0858/1392] bitget: add get server time request and get symbols request --- .../bitgetapi/get_server_time_request.go | 21 +++ .../get_server_time_request_requestgen.go | 140 ++++++++++++++++++ .../bitget/bitgetapi/get_symbols_request.go | 36 +++++ .../get_symbols_request_requestgen.go | 139 +++++++++++++++++ 4 files changed, 336 insertions(+) create mode 100644 pkg/exchange/bitget/bitgetapi/get_server_time_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_server_time_request_requestgen.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_symbols_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_symbols_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/get_server_time_request.go b/pkg/exchange/bitget/bitgetapi/get_server_time_request.go new file mode 100644 index 0000000000..89a30278f9 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_server_time_request.go @@ -0,0 +1,21 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/types" +) + +type ServerTime = types.MillisecondTimestamp + +//go:generate GetRequest -url "/api/spot/v1/public/time" -type GetServerTimeRequest -responseDataType .ServerTime +type GetServerTimeRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *RestClient) NewGetServerTimeRequest() *GetServerTimeRequest { + return &GetServerTimeRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_server_time_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_server_time_request_requestgen.go new file mode 100644 index 0000000000..7981322d74 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_server_time_request_requestgen.go @@ -0,0 +1,140 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/public/time -type GetServerTimeRequest -responseDataType .ServerTime"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/types" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetServerTimeRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetServerTimeRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetServerTimeRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetServerTimeRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetServerTimeRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetServerTimeRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetServerTimeRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetServerTimeRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetServerTimeRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetServerTimeRequest) Do(ctx context.Context) (*types.MillisecondTimestamp, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/spot/v1/public/time" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data types.MillisecondTimestamp + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bitget/bitgetapi/get_symbols_request.go b/pkg/exchange/bitget/bitgetapi/get_symbols_request.go new file mode 100644 index 0000000000..c720678da7 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_symbols_request.go @@ -0,0 +1,36 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type Symbol struct { + Symbol string `json:"symbol"` + SymbolName string `json:"symbolName"` + BaseCoin string `json:"baseCoin"` + QuoteCoin string `json:"quoteCoin"` + MinTradeAmount fixedpoint.Value `json:"minTradeAmount"` + MaxTradeAmount fixedpoint.Value `json:"maxTradeAmount"` + TakerFeeRate fixedpoint.Value `json:"takerFeeRate"` + MakerFeeRate fixedpoint.Value `json:"makerFeeRate"` + PriceScale fixedpoint.Value `json:"priceScale"` + QuantityScale fixedpoint.Value `json:"quantityScale"` + MinTradeUSDT fixedpoint.Value `json:"minTradeUSDT"` + Status string `json:"status"` + BuyLimitPriceRatio fixedpoint.Value `json:"buyLimitPriceRatio"` + SellLimitPriceRatio fixedpoint.Value `json:"sellLimitPriceRatio"` +} + +//go:generate GetRequest -url "/api/spot/v1/public/products" -type GetSymbolsRequest -responseDataType []Symbol +type GetSymbolsRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *RestClient) NewGetSymbolsRequest() *GetSymbolsRequest { + return &GetSymbolsRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_symbols_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_symbols_request_requestgen.go new file mode 100644 index 0000000000..899545e58d --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_symbols_request_requestgen.go @@ -0,0 +1,139 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/public/products -type GetSymbolsRequest -responseDataType []Symbol"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetSymbolsRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetSymbolsRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetSymbolsRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetSymbolsRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetSymbolsRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetSymbolsRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetSymbolsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetSymbolsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetSymbolsRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetSymbolsRequest) Do(ctx context.Context) ([]Symbol, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/spot/v1/public/products" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []Symbol + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} From 8932da7e3f84f00f21e257de394f10be562bd42b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 16:55:21 +0800 Subject: [PATCH 0859/1392] bitget: add get ticker request --- .../bitget/bitgetapi/get_ticker_request.go | 38 +++++ .../get_ticker_request_requestgen.go | 139 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 pkg/exchange/bitget/bitgetapi/get_ticker_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/get_ticker_request.go b/pkg/exchange/bitget/bitgetapi/get_ticker_request.go new file mode 100644 index 0000000000..19bc125b4c --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_ticker_request.go @@ -0,0 +1,38 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type Ticker struct { + Symbol string `json:"symbol"` + High24H fixedpoint.Value `json:"high24h"` + Low24H fixedpoint.Value `json:"low24h"` + Close fixedpoint.Value `json:"close"` + QuoteVol fixedpoint.Value `json:"quoteVol"` + BaseVol fixedpoint.Value `json:"baseVol"` + UsdtVol fixedpoint.Value `json:"usdtVol"` + Ts types.MillisecondTimestamp `json:"ts"` + BuyOne fixedpoint.Value `json:"buyOne"` + SellOne fixedpoint.Value `json:"sellOne"` + BidSz fixedpoint.Value `json:"bidSz"` + AskSz fixedpoint.Value `json:"askSz"` + OpenUtc0 fixedpoint.Value `json:"openUtc0"` + ChangeUtc fixedpoint.Value `json:"changeUtc"` + Change fixedpoint.Value `json:"change"` +} + +//go:generate GetRequest -url "/api/spot/v1/market/ticker" -type GetTickerRequest -responseDataType .Ticker +type GetTickerRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *RestClient) NewGetTickerRequest() *GetTickerRequest { + return &GetTickerRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go new file mode 100644 index 0000000000..db96de6c7b --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go @@ -0,0 +1,139 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/market/ticker -type GetTickerRequest -responseDataType .Ticker"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetTickerRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetTickerRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetTickerRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetTickerRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetTickerRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetTickerRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetTickerRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetTickerRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetTickerRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetTickerRequest) Do(ctx context.Context) (*Ticker, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/spot/v1/market/ticker" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data Ticker + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return &data, nil +} From e31a6ca3c82465e1f35226cf52f6c2bf07bff660 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 16:56:39 +0800 Subject: [PATCH 0860/1392] bitget: add GetAllTickers request --- .../bitgetapi/get_all_tickers_request.go | 17 +++ .../get_all_tickers_request_requestgen.go | 139 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 pkg/exchange/bitget/bitgetapi/get_all_tickers_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_all_tickers_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/get_all_tickers_request.go b/pkg/exchange/bitget/bitgetapi/get_all_tickers_request.go new file mode 100644 index 0000000000..6e1e157800 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_all_tickers_request.go @@ -0,0 +1,17 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" +) + +//go:generate GetRequest -url "/api/spot/v1/market/tickers" -type GetAllTickersRequest -responseDataType []Ticker +type GetAllTickersRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *RestClient) NewGetAllTickersRequest() *GetAllTickersRequest { + return &GetAllTickersRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_all_tickers_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_all_tickers_request_requestgen.go new file mode 100644 index 0000000000..3ca8c96ecf --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_all_tickers_request_requestgen.go @@ -0,0 +1,139 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/market/tickers -type GetAllTickersRequest -responseDataType []Ticker"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetAllTickersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetAllTickersRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetAllTickersRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetAllTickersRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetAllTickersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetAllTickersRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetAllTickersRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetAllTickersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetAllTickersRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetAllTickersRequest) Do(ctx context.Context) ([]Ticker, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/spot/v1/market/tickers" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []Ticker + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} From c347a2423ad4da6d64a061b5c6bfca9422394c8b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 17:53:24 +0800 Subject: [PATCH 0861/1392] bitget: update generated request files and fix account assets api data type --- pkg/exchange/bitget/bitgetapi/client_test.go | 37 +++++ .../bitgetapi/get_account_assets_request.go | 2 +- .../bitgetapi/get_all_tickers_request.go | 2 +- .../get_all_tickers_request_requestgen.go | 2 +- .../bitget/bitgetapi/get_candles_request.go | 31 ++++ .../get_candles_request_requestgen.go | 139 ++++++++++++++++++ .../bitgetapi/get_server_time_request.go | 2 +- .../get_server_time_request_requestgen.go | 2 +- .../bitget/bitgetapi/get_symbols_request.go | 2 +- .../get_symbols_request_requestgen.go | 2 +- .../bitget/bitgetapi/get_ticker_request.go | 2 +- .../get_ticker_request_requestgen.go | 2 +- 12 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 pkg/exchange/bitget/bitgetapi/client_test.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_candles_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_candles_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/client_test.go b/pkg/exchange/bitget/bitgetapi/client_test.go new file mode 100644 index 0000000000..8b7afda37d --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/client_test.go @@ -0,0 +1,37 @@ +package bitgetapi + +import ( + "context" + "os" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/testutil" +) + +func getTestClientOrSkip(t *testing.T) *RestClient { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + + key, secret, ok := testutil.IntegrationTestConfigured(t, "BITGET") + if !ok { + t.Skip("BITGET_* env vars are not configured") + return nil + } + + client := NewClient() + client.Auth(key, secret, os.Getenv("BITGET_API_PASSPHRASE")) + return client +} + +func TestClient_GetAccountAssetsRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + req := client.NewGetAccountAssetsRequest() + assets, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("assets: %+v", assets) +} diff --git a/pkg/exchange/bitget/bitgetapi/get_account_assets_request.go b/pkg/exchange/bitget/bitgetapi/get_account_assets_request.go index 03cc6fb18c..abd9bca615 100644 --- a/pkg/exchange/bitget/bitgetapi/get_account_assets_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_account_assets_request.go @@ -11,7 +11,7 @@ import ( ) type AccountAsset struct { - CoinId string `json:"coinId"` + CoinId int64 `json:"coinId"` CoinName string `json:"coinName"` Available fixedpoint.Value `json:"available"` Frozen fixedpoint.Value `json:"frozen"` diff --git a/pkg/exchange/bitget/bitgetapi/get_all_tickers_request.go b/pkg/exchange/bitget/bitgetapi/get_all_tickers_request.go index 6e1e157800..c75d2b252d 100644 --- a/pkg/exchange/bitget/bitgetapi/get_all_tickers_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_all_tickers_request.go @@ -9,7 +9,7 @@ import ( //go:generate GetRequest -url "/api/spot/v1/market/tickers" -type GetAllTickersRequest -responseDataType []Ticker type GetAllTickersRequest struct { - client requestgen.AuthenticatedAPIClient + client requestgen.APIClient } func (c *RestClient) NewGetAllTickersRequest() *GetAllTickersRequest { diff --git a/pkg/exchange/bitget/bitgetapi/get_all_tickers_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_all_tickers_request_requestgen.go index 3ca8c96ecf..a1c94e0d16 100644 --- a/pkg/exchange/bitget/bitgetapi/get_all_tickers_request_requestgen.go +++ b/pkg/exchange/bitget/bitgetapi/get_all_tickers_request_requestgen.go @@ -117,7 +117,7 @@ func (g *GetAllTickersRequest) Do(ctx context.Context) ([]Ticker, error) { apiURL := "/api/spot/v1/market/tickers" - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) if err != nil { return nil, err } diff --git a/pkg/exchange/bitget/bitgetapi/get_candles_request.go b/pkg/exchange/bitget/bitgetapi/get_candles_request.go new file mode 100644 index 0000000000..381353710c --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_candles_request.go @@ -0,0 +1,31 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type Candle struct { + Open fixedpoint.Value `json:"open"` + High fixedpoint.Value `json:"high"` + Low fixedpoint.Value `json:"low"` + Close fixedpoint.Value `json:"close"` + QuoteVol fixedpoint.Value `json:"quoteVol"` + BaseVol fixedpoint.Value `json:"baseVol"` + UsdtVol fixedpoint.Value `json:"usdtVol"` + Ts types.MillisecondTimestamp `json:"ts"` +} + +//go:generate GetRequest -url "/api/spot/v1/market/candles" -type GetCandlesRequest -responseDataType []Candle +type GetCandlesRequest struct { + client requestgen.APIClient +} + +func (c *RestClient) NewGetCandlesRequest() *GetCandlesRequest { + return &GetCandlesRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_candles_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_candles_request_requestgen.go new file mode 100644 index 0000000000..cacc444bea --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_candles_request_requestgen.go @@ -0,0 +1,139 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/market/candles -type GetCandlesRequest -responseDataType []Candle"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetCandlesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetCandlesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetCandlesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetCandlesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetCandlesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetCandlesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetCandlesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetCandlesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetCandlesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetCandlesRequest) Do(ctx context.Context) ([]Candle, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/spot/v1/market/candles" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []Candle + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/bitget/bitgetapi/get_server_time_request.go b/pkg/exchange/bitget/bitgetapi/get_server_time_request.go index 89a30278f9..c52c2d89ad 100644 --- a/pkg/exchange/bitget/bitgetapi/get_server_time_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_server_time_request.go @@ -13,7 +13,7 @@ type ServerTime = types.MillisecondTimestamp //go:generate GetRequest -url "/api/spot/v1/public/time" -type GetServerTimeRequest -responseDataType .ServerTime type GetServerTimeRequest struct { - client requestgen.AuthenticatedAPIClient + client requestgen.APIClient } func (c *RestClient) NewGetServerTimeRequest() *GetServerTimeRequest { diff --git a/pkg/exchange/bitget/bitgetapi/get_server_time_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_server_time_request_requestgen.go index 7981322d74..5e3c7db280 100644 --- a/pkg/exchange/bitget/bitgetapi/get_server_time_request_requestgen.go +++ b/pkg/exchange/bitget/bitgetapi/get_server_time_request_requestgen.go @@ -118,7 +118,7 @@ func (g *GetServerTimeRequest) Do(ctx context.Context) (*types.MillisecondTimest apiURL := "/api/spot/v1/public/time" - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) if err != nil { return nil, err } diff --git a/pkg/exchange/bitget/bitgetapi/get_symbols_request.go b/pkg/exchange/bitget/bitgetapi/get_symbols_request.go index c720678da7..648175d9ca 100644 --- a/pkg/exchange/bitget/bitgetapi/get_symbols_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_symbols_request.go @@ -28,7 +28,7 @@ type Symbol struct { //go:generate GetRequest -url "/api/spot/v1/public/products" -type GetSymbolsRequest -responseDataType []Symbol type GetSymbolsRequest struct { - client requestgen.AuthenticatedAPIClient + client requestgen.APIClient } func (c *RestClient) NewGetSymbolsRequest() *GetSymbolsRequest { diff --git a/pkg/exchange/bitget/bitgetapi/get_symbols_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_symbols_request_requestgen.go index 899545e58d..7539542f1e 100644 --- a/pkg/exchange/bitget/bitgetapi/get_symbols_request_requestgen.go +++ b/pkg/exchange/bitget/bitgetapi/get_symbols_request_requestgen.go @@ -117,7 +117,7 @@ func (g *GetSymbolsRequest) Do(ctx context.Context) ([]Symbol, error) { apiURL := "/api/spot/v1/public/products" - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) if err != nil { return nil, err } diff --git a/pkg/exchange/bitget/bitgetapi/get_ticker_request.go b/pkg/exchange/bitget/bitgetapi/get_ticker_request.go index 19bc125b4c..8bbd98ba22 100644 --- a/pkg/exchange/bitget/bitgetapi/get_ticker_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_ticker_request.go @@ -30,7 +30,7 @@ type Ticker struct { //go:generate GetRequest -url "/api/spot/v1/market/ticker" -type GetTickerRequest -responseDataType .Ticker type GetTickerRequest struct { - client requestgen.AuthenticatedAPIClient + client requestgen.APIClient } func (c *RestClient) NewGetTickerRequest() *GetTickerRequest { diff --git a/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go index db96de6c7b..1f6e1bed6b 100644 --- a/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go +++ b/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go @@ -117,7 +117,7 @@ func (g *GetTickerRequest) Do(ctx context.Context) (*Ticker, error) { apiURL := "/api/spot/v1/market/ticker" - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) if err != nil { return nil, err } From 3154961d724c71008a43555d899f93a0058016ff Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 17 May 2023 18:04:24 +0800 Subject: [PATCH 0862/1392] bitget: add more public api tests --- pkg/exchange/bitget/bitgetapi/client_test.go | 10 ++++++++++ .../bitget/bitgetapi/get_ticker_request.go | 2 ++ .../bitgetapi/get_ticker_request_requestgen.go | 17 +++++++++++++++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/bitget/bitgetapi/client_test.go b/pkg/exchange/bitget/bitgetapi/client_test.go index 8b7afda37d..3bf42f08ad 100644 --- a/pkg/exchange/bitget/bitgetapi/client_test.go +++ b/pkg/exchange/bitget/bitgetapi/client_test.go @@ -35,3 +35,13 @@ func TestClient_GetAccountAssetsRequest(t *testing.T) { assert.NoError(t, err) t.Logf("assets: %+v", assets) } + +func TestClient_GetTickerRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + req := client.NewGetTickerRequest() + req.Symbol("BTCUSDT_SPBL") + ticker, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("ticker: %+v", ticker) +} diff --git a/pkg/exchange/bitget/bitgetapi/get_ticker_request.go b/pkg/exchange/bitget/bitgetapi/get_ticker_request.go index 8bbd98ba22..4df022e5f2 100644 --- a/pkg/exchange/bitget/bitgetapi/get_ticker_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_ticker_request.go @@ -31,6 +31,8 @@ type Ticker struct { //go:generate GetRequest -url "/api/spot/v1/market/ticker" -type GetTickerRequest -responseDataType .Ticker type GetTickerRequest struct { client requestgen.APIClient + + symbol string `param:"symbol"` } func (c *RestClient) NewGetTickerRequest() *GetTickerRequest { diff --git a/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go index 1f6e1bed6b..bfb10ddcf7 100644 --- a/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go +++ b/pkg/exchange/bitget/bitgetapi/get_ticker_request_requestgen.go @@ -11,6 +11,11 @@ import ( "regexp" ) +func (g *GetTickerRequest) Symbol(symbol string) *GetTickerRequest { + g.symbol = symbol + return g +} + // GetQueryParameters builds and checks the query parameters and returns url.Values func (g *GetTickerRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} @@ -26,6 +31,11 @@ func (g *GetTickerRequest) GetQueryParameters() (url.Values, error) { // GetParameters builds and checks the parameters and return the result in a map object func (g *GetTickerRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := g.symbol + + // assign parameter of symbol + params["symbol"] = symbol return params, nil } @@ -111,9 +121,12 @@ func (g *GetTickerRequest) GetSlugsMap() (map[string]string, error) { func (g *GetTickerRequest) Do(ctx context.Context) (*Ticker, error) { - // no body params + // empty params for GET operation var params interface{} - query := url.Values{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } apiURL := "/api/spot/v1/market/ticker" From cff98bc141e62c3aa00dfb845e3cd4f1a1d44aa6 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 10:54:00 +0800 Subject: [PATCH 0863/1392] bitgetapi: refactor tests --- pkg/exchange/bitget/bitgetapi/client_test.go | 43 +++++++++++++------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/pkg/exchange/bitget/bitgetapi/client_test.go b/pkg/exchange/bitget/bitgetapi/client_test.go index 3bf42f08ad..0b4914aa49 100644 --- a/pkg/exchange/bitget/bitgetapi/client_test.go +++ b/pkg/exchange/bitget/bitgetapi/client_test.go @@ -27,21 +27,36 @@ func getTestClientOrSkip(t *testing.T) *RestClient { return client } -func TestClient_GetAccountAssetsRequest(t *testing.T) { +func TestClient(t *testing.T) { client := getTestClientOrSkip(t) ctx := context.Background() - req := client.NewGetAccountAssetsRequest() - assets, err := req.Do(ctx) - assert.NoError(t, err) - t.Logf("assets: %+v", assets) -} -func TestClient_GetTickerRequest(t *testing.T) { - client := getTestClientOrSkip(t) - ctx := context.Background() - req := client.NewGetTickerRequest() - req.Symbol("BTCUSDT_SPBL") - ticker, err := req.Do(ctx) - assert.NoError(t, err) - t.Logf("ticker: %+v", ticker) + t.Run("GetAllTickersRequest", func(t *testing.T) { + req := client.NewGetAllTickersRequest() + tickers, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("tickers: %+v", tickers) + }) + + t.Run("GetTickerRequest", func(t *testing.T) { + req := client.NewGetTickerRequest() + req.Symbol("BTCUSDT_SPBL") + ticker, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("ticker: %+v", ticker) + }) + + t.Run("GetServerTime", func(t *testing.T) { + req := client.NewGetServerTimeRequest() + serverTime, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("time: %+v", serverTime) + }) + + t.Run("GetAccountAssetsRequest", func(t *testing.T) { + req := client.NewGetAccountAssetsRequest() + assets, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("assets: %+v", assets) + }) } From a5a64fa6d439518d2e962e19619b15b426efb55e Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 11:13:06 +0800 Subject: [PATCH 0864/1392] bitgetapi: add getDepthRequest --- pkg/exchange/bitget/bitgetapi/client_test.go | 10 +- .../bitget/bitgetapi/get_depth_request.go | 30 +++ .../bitgetapi/get_depth_request_requestgen.go | 175 ++++++++++++++++++ 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 pkg/exchange/bitget/bitgetapi/get_depth_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_depth_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/client_test.go b/pkg/exchange/bitget/bitgetapi/client_test.go index 0b4914aa49..2472a59543 100644 --- a/pkg/exchange/bitget/bitgetapi/client_test.go +++ b/pkg/exchange/bitget/bitgetapi/client_test.go @@ -46,7 +46,15 @@ func TestClient(t *testing.T) { t.Logf("ticker: %+v", ticker) }) - t.Run("GetServerTime", func(t *testing.T) { + t.Run("GetDepthRequest", func(t *testing.T) { + req := client.NewGetDepthRequest() + req.Symbol("BTCUSDT_SPBL") + depth, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("depth: %+v", depth) + }) + + t.Run("GetServerTimeRequest", func(t *testing.T) { req := client.NewGetServerTimeRequest() serverTime, err := req.Do(ctx) assert.NoError(t, err) diff --git a/pkg/exchange/bitget/bitgetapi/get_depth_request.go b/pkg/exchange/bitget/bitgetapi/get_depth_request.go new file mode 100644 index 0000000000..7c1f82bc28 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_depth_request.go @@ -0,0 +1,30 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type Depth struct { + Asks [][]fixedpoint.Value `json:"asks"` + Bids [][]fixedpoint.Value `json:"bids"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` +} + +//go:generate GetRequest -url "/api/spot/v1/market/depth" -type GetDepthRequest -responseDataType .Depth +type GetDepthRequest struct { + client requestgen.APIClient + + symbol string `param:"symbol"` + stepType string `param:"type" default:"step0"` + limit *int `param:"limit"` +} + +func (c *RestClient) NewGetDepthRequest() *GetDepthRequest { + return &GetDepthRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_depth_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_depth_request_requestgen.go new file mode 100644 index 0000000000..8f54091c6a --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_depth_request_requestgen.go @@ -0,0 +1,175 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/market/depth -type GetDepthRequest -responseDataType .Depth"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetDepthRequest) Symbol(symbol string) *GetDepthRequest { + g.symbol = symbol + return g +} + +func (g *GetDepthRequest) StepType(stepType string) *GetDepthRequest { + g.stepType = stepType + return g +} + +func (g *GetDepthRequest) Limit(limit int) *GetDepthRequest { + g.limit = &limit + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetDepthRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetDepthRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check stepType field -> json key type + stepType := g.stepType + + // assign parameter of stepType + params["type"] = stepType + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetDepthRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetDepthRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetDepthRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetDepthRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetDepthRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetDepthRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetDepthRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetDepthRequest) Do(ctx context.Context) (*Depth, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/spot/v1/market/depth" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data Depth + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return &data, nil +} From 0c887a6bfb38de1311476e0cfa006b0125fcc46d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 11:23:30 +0800 Subject: [PATCH 0865/1392] bitgetapi: add place order request api --- .../bitget/bitgetapi/place_order_request.go | 29 ++ .../place_order_request_requestgen.go | 247 ++++++++++++++++++ pkg/exchange/bitget/bitgetapi/types.go | 25 +- 3 files changed, 286 insertions(+), 15 deletions(-) create mode 100644 pkg/exchange/bitget/bitgetapi/place_order_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/place_order_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/place_order_request.go b/pkg/exchange/bitget/bitgetapi/place_order_request.go new file mode 100644 index 0000000000..10266ce59b --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/place_order_request.go @@ -0,0 +1,29 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" +) + +type OrderResponse struct { + OrderId string `json:"orderId"` + ClientOrderId string `json:"clientOrderId"` +} + +//go:generate PostRequest -url "/api/spot/v1/trade/orders" -type PlaceOrderRequest -responseDataType .OrderResponse +type PlaceOrderRequest struct { + client requestgen.AuthenticatedAPIClient + symbol string `param:"symbol"` + orderType OrderType `param:"orderType"` + side OrderSide `param:"side"` + force OrderForce `param:"force"` + price string `param:"price"` + quantity string `param:"quantity"` + clientOrderId *string `param:"clientOrderId"` +} + +func (c *RestClient) NewPlaceOrderRequest() *PlaceOrderRequest { + return &PlaceOrderRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/place_order_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/place_order_request_requestgen.go new file mode 100644 index 0000000000..9ce8799b10 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/place_order_request_requestgen.go @@ -0,0 +1,247 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Data -url /api/spot/v1/trade/orders -type PlaceOrderRequest -responseDataType .OrderResponse"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (p *PlaceOrderRequest) Symbol(symbol string) *PlaceOrderRequest { + p.symbol = symbol + return p +} + +func (p *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest { + p.orderType = orderType + return p +} + +func (p *PlaceOrderRequest) Side(side OrderSide) *PlaceOrderRequest { + p.side = side + return p +} + +func (p *PlaceOrderRequest) Force(force OrderForce) *PlaceOrderRequest { + p.force = force + return p +} + +func (p *PlaceOrderRequest) Price(price string) *PlaceOrderRequest { + p.price = price + return p +} + +func (p *PlaceOrderRequest) Quantity(quantity string) *PlaceOrderRequest { + p.quantity = quantity + return p +} + +func (p *PlaceOrderRequest) ClientOrderId(clientOrderId string) *PlaceOrderRequest { + p.clientOrderId = &clientOrderId + return p +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (p *PlaceOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (p *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := p.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check orderType field -> json key orderType + orderType := p.orderType + + // TEMPLATE check-valid-values + switch orderType { + case OrderTypeLimit, OrderTypeMarket: + params["orderType"] = orderType + + default: + return nil, fmt.Errorf("orderType value %v is invalid", orderType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of orderType + params["orderType"] = orderType + // check side field -> json key side + side := p.side + + // TEMPLATE check-valid-values + switch side { + case OrderSideBuy, OrderSideSell: + params["side"] = side + + default: + return nil, fmt.Errorf("side value %v is invalid", side) + + } + // END TEMPLATE check-valid-values + + // assign parameter of side + params["side"] = side + // check force field -> json key force + force := p.force + + // TEMPLATE check-valid-values + switch force { + case OrderForceGTC, OrderForcePostOnly, OrderForceFOK, OrderForceIOC: + params["force"] = force + + default: + return nil, fmt.Errorf("force value %v is invalid", force) + + } + // END TEMPLATE check-valid-values + + // assign parameter of force + params["force"] = force + // check price field -> json key price + price := p.price + + // assign parameter of price + params["price"] = price + // check quantity field -> json key quantity + quantity := p.quantity + + // assign parameter of quantity + params["quantity"] = quantity + // check clientOrderId field -> json key clientOrderId + if p.clientOrderId != nil { + clientOrderId := *p.clientOrderId + + // assign parameter of clientOrderId + params["clientOrderId"] = clientOrderId + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (p *PlaceOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := p.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if p.isVarSlice(_v) { + p.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (p *PlaceOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := p.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (p *PlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (p *PlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (p *PlaceOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (p *PlaceOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (p *PlaceOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := p.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (p *PlaceOrderRequest) Do(ctx context.Context) (*OrderResponse, error) { + + params, err := p.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/spot/v1/trade/orders" + + req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := p.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data OrderResponse + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bitget/bitgetapi/types.go b/pkg/exchange/bitget/bitgetapi/types.go index 59153cb8dd..d7eefdc00e 100644 --- a/pkg/exchange/bitget/bitgetapi/types.go +++ b/pkg/exchange/bitget/bitgetapi/types.go @@ -10,27 +10,22 @@ const ( type OrderType string const ( - OrderTypeMarket OrderType = "market" - OrderTypeLimit OrderType = "limit" - OrderTypePostOnly OrderType = "post_only" - OrderTypeFOK OrderType = "fok" - OrderTypeIOC OrderType = "ioc" + OrderTypeLimit OrderType = "limit" + OrderTypeMarket OrderType = "market" ) -type InstrumentType string +type OrderSide string const ( - InstrumentTypeSpot InstrumentType = "SPOT" - InstrumentTypeSwap InstrumentType = "SWAP" - InstrumentTypeFutures InstrumentType = "FUTURES" - InstrumentTypeOption InstrumentType = "OPTION" + OrderSideBuy OrderSide = "buy" + OrderSideSell OrderSide = "sell" ) -type OrderState string +type OrderForce string const ( - OrderStateCanceled OrderState = "canceled" - OrderStateLive OrderState = "live" - OrderStatePartiallyFilled OrderState = "partially_filled" - OrderStateFilled OrderState = "filled" + OrderForceGTC OrderForce = "normal" + OrderForcePostOnly OrderForce = "post_only" + OrderForceFOK OrderForce = "fok" + OrderForceIOC OrderForce = "ioc" ) From 51e05499b20b1da44a55aa5d3ba52dae451a86f5 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 11:59:49 +0800 Subject: [PATCH 0866/1392] bitgetapi: add CancelOrderBySymbolRequest --- .../cancel_order_by_symbol_request.go | 20 ++ ...ncel_order_by_symbol_request_requestgen.go | 152 +++++++++++++++ .../bitget/bitgetapi/cancel_order_request.go | 25 +++ .../cancel_order_request_requestgen.go | 177 ++++++++++++++++++ 4 files changed, 374 insertions(+) create mode 100644 pkg/exchange/bitget/bitgetapi/cancel_order_by_symbol_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/cancel_order_by_symbol_request_requestgen.go create mode 100644 pkg/exchange/bitget/bitgetapi/cancel_order_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/cancel_order_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/cancel_order_by_symbol_request.go b/pkg/exchange/bitget/bitgetapi/cancel_order_by_symbol_request.go new file mode 100644 index 0000000000..89ff6dba31 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/cancel_order_by_symbol_request.go @@ -0,0 +1,20 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" +) + +type CancelOrderBySymbolResponse string + +//go:generate GetRequest -url "/api/spot/v1/trade/cancel-symbol-order" -type CancelOrderBySymbolRequest -responseDataType .CancelOrderBySymbolResponse +type CancelOrderBySymbolRequest struct { + client requestgen.AuthenticatedAPIClient + symbol string `param:"symbol"` +} + +func (c *RestClient) NewCancelOrderBySymbolRequest() *CancelOrderBySymbolRequest { + return &CancelOrderBySymbolRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/cancel_order_by_symbol_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/cancel_order_by_symbol_request_requestgen.go new file mode 100644 index 0000000000..043f3c35e1 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/cancel_order_by_symbol_request_requestgen.go @@ -0,0 +1,152 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/trade/cancel-symbol-order -type CancelOrderBySymbolRequest -responseDataType .CancelOrderBySymbolResponse"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (c *CancelOrderBySymbolRequest) Symbol(symbol string) *CancelOrderBySymbolRequest { + c.symbol = symbol + return c +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (c *CancelOrderBySymbolRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (c *CancelOrderBySymbolRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := c.symbol + + // assign parameter of symbol + params["symbol"] = symbol + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (c *CancelOrderBySymbolRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := c.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if c.isVarSlice(_v) { + c.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (c *CancelOrderBySymbolRequest) GetParametersJSON() ([]byte, error) { + params, err := c.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (c *CancelOrderBySymbolRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (c *CancelOrderBySymbolRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (c *CancelOrderBySymbolRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (c *CancelOrderBySymbolRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (c *CancelOrderBySymbolRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := c.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (c *CancelOrderBySymbolRequest) Do(ctx context.Context) (*CancelOrderBySymbolResponse, error) { + + // empty params for GET operation + var params interface{} + query, err := c.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/spot/v1/trade/cancel-symbol-order" + + req, err := c.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := c.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data CancelOrderBySymbolResponse + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bitget/bitgetapi/cancel_order_request.go b/pkg/exchange/bitget/bitgetapi/cancel_order_request.go new file mode 100644 index 0000000000..b0a7542b41 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/cancel_order_request.go @@ -0,0 +1,25 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" +) + +type CancelOrderResponse struct { + OrderId string `json:"orderId"` + ClientOrderId string `json:"clientOrderId"` +} + +//go:generate PostRequest -url "/api/spot/v1/trade/cancel-order-v2" -type CancelOrderRequest -responseDataType .CancelOrderResponse +type CancelOrderRequest struct { + client requestgen.AuthenticatedAPIClient + symbol string `param:"symbol"` + orderId *string `param:"orderId"` + clientOrderId *string `param:"clientOid"` +} + +func (c *RestClient) NewCancelOrderRequest() *CancelOrderRequest { + return &CancelOrderRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/cancel_order_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/cancel_order_request_requestgen.go new file mode 100644 index 0000000000..986db6973b --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/cancel_order_request_requestgen.go @@ -0,0 +1,177 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Data -url /api/spot/v1/trade/cancel-order-v2 -type CancelOrderRequest -responseDataType .CancelOrderResponse"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (c *CancelOrderRequest) Symbol(symbol string) *CancelOrderRequest { + c.symbol = symbol + return c +} + +func (c *CancelOrderRequest) OrderId(orderId string) *CancelOrderRequest { + c.orderId = &orderId + return c +} + +func (c *CancelOrderRequest) ClientOrderId(clientOrderId string) *CancelOrderRequest { + c.clientOrderId = &clientOrderId + return c +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (c *CancelOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (c *CancelOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := c.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check orderId field -> json key orderId + if c.orderId != nil { + orderId := *c.orderId + + // assign parameter of orderId + params["orderId"] = orderId + } else { + } + // check clientOrderId field -> json key clientOid + if c.clientOrderId != nil { + clientOrderId := *c.clientOrderId + + // assign parameter of clientOrderId + params["clientOid"] = clientOrderId + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (c *CancelOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := c.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if c.isVarSlice(_v) { + c.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (c *CancelOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := c.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (c *CancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (c *CancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (c *CancelOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (c *CancelOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (c *CancelOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := c.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (c *CancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) { + + params, err := c.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/spot/v1/trade/cancel-order-v2" + + req, err := c.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := c.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data CancelOrderResponse + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return &data, nil +} From 90f704bab0569df7a177cdca3ce3e1a3209fd0ed Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 15:22:50 +0800 Subject: [PATCH 0867/1392] bitgetapi: add get order detail request --- .../bitgetapi/get_order_detail_request.go | 41 ++++ .../get_order_detail_request_requestgen.go | 177 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 pkg/exchange/bitget/bitgetapi/get_order_detail_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_order_detail_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go b/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go new file mode 100644 index 0000000000..717bed4a6a --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go @@ -0,0 +1,41 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type OrderDetail struct { + AccountId string `json:"accountId"` + Symbol string `json:"symbol"` + OrderId string `json:"orderId"` + ClientOrderId string `json:"clientOrderId"` + Price fixedpoint.Value `json:"price"` + Quantity fixedpoint.Value `json:"quantity"` + OrderType OrderType `json:"orderType"` + Side OrderSide `json:"side"` + Status string `json:"status"` + FillPrice fixedpoint.Value `json:"fillPrice"` + FillQuantity fixedpoint.Value `json:"fillQuantity"` + FillTotalAmount fixedpoint.Value `json:"fillTotalAmount"` + EnterPointSource string `json:"enterPointSource"` + CTime types.MillisecondTimestamp `json:"cTime"` +} + +//go:generate PostRequest -url "/api/spot/v1/trade/orderInfo" -type GetOrderDetailRequest -responseDataType .OrderDetail +type GetOrderDetailRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` + orderId *string `param:"orderId"` + clientOrderId *string `param:"clientOid"` +} + +func (c *RestClient) NewGetOrderDetailRequest() *GetOrderDetailRequest { + return &GetOrderDetailRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_order_detail_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_order_detail_request_requestgen.go new file mode 100644 index 0000000000..70055fe7cb --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_order_detail_request_requestgen.go @@ -0,0 +1,177 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Data -url /api/spot/v1/trade/orderInfo -type GetOrderDetailRequest -responseDataType .OrderDetail"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetOrderDetailRequest) Symbol(symbol string) *GetOrderDetailRequest { + g.symbol = symbol + return g +} + +func (g *GetOrderDetailRequest) OrderId(orderId string) *GetOrderDetailRequest { + g.orderId = &orderId + return g +} + +func (g *GetOrderDetailRequest) ClientOrderId(clientOrderId string) *GetOrderDetailRequest { + g.clientOrderId = &clientOrderId + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetOrderDetailRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetOrderDetailRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check orderId field -> json key orderId + if g.orderId != nil { + orderId := *g.orderId + + // assign parameter of orderId + params["orderId"] = orderId + } else { + } + // check clientOrderId field -> json key clientOid + if g.clientOrderId != nil { + clientOrderId := *g.clientOrderId + + // assign parameter of clientOrderId + params["clientOid"] = clientOrderId + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetOrderDetailRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetOrderDetailRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetOrderDetailRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetOrderDetailRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetOrderDetailRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetOrderDetailRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetOrderDetailRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetOrderDetailRequest) Do(ctx context.Context) (*OrderDetail, error) { + + params, err := g.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/spot/v1/trade/orderInfo" + + req, err := g.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data OrderDetail + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return &data, nil +} From ae1c1377ce768bf934264845a3cacac674144e27 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 15:33:29 +0800 Subject: [PATCH 0868/1392] bitget: define OrderStatus --- .../bitget/bitgetapi/get_order_detail_request.go | 2 +- pkg/exchange/bitget/bitgetapi/types.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go b/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go index 717bed4a6a..f6081f0da6 100644 --- a/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go @@ -19,7 +19,7 @@ type OrderDetail struct { Quantity fixedpoint.Value `json:"quantity"` OrderType OrderType `json:"orderType"` Side OrderSide `json:"side"` - Status string `json:"status"` + Status OrderStatus `json:"status"` FillPrice fixedpoint.Value `json:"fillPrice"` FillQuantity fixedpoint.Value `json:"fillQuantity"` FillTotalAmount fixedpoint.Value `json:"fillTotalAmount"` diff --git a/pkg/exchange/bitget/bitgetapi/types.go b/pkg/exchange/bitget/bitgetapi/types.go index d7eefdc00e..1aa6280f77 100644 --- a/pkg/exchange/bitget/bitgetapi/types.go +++ b/pkg/exchange/bitget/bitgetapi/types.go @@ -29,3 +29,13 @@ const ( OrderForceFOK OrderForce = "fok" OrderForceIOC OrderForce = "ioc" ) + +type OrderStatus string + +const ( + OrderStatusInit OrderStatus = "init" + OrderStatusNew OrderStatus = "new" + OrderStatusPartialFill OrderStatus = "partial_fill" + OrderStatusFullFill OrderStatus = "full_fill" + OrderStatusCancelled OrderStatus = "cancelled" +) From 312c8baeb36cb896694a84a9d1dde2cc64915c1c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 15:38:57 +0800 Subject: [PATCH 0869/1392] bitget: add open orders request --- .../bitgetapi/get_open_orders_request.go | 19 +++ .../get_open_orders_request_requestgen.go | 152 ++++++++++++++++++ .../bitgetapi/get_order_detail_request.go | 2 +- 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 pkg/exchange/bitget/bitgetapi/get_open_orders_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_open_orders_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/get_open_orders_request.go b/pkg/exchange/bitget/bitgetapi/get_open_orders_request.go new file mode 100644 index 0000000000..24074f710b --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_open_orders_request.go @@ -0,0 +1,19 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" +) + +//go:generate GetRequest -url "/api/spot/v1/trade/open-orders" -type GetOpenOrdersRequest -responseDataType []OrderDetail +type GetOpenOrdersRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` +} + +func (c *RestClient) NewGetOpenOrdersRequest() *GetOpenOrdersRequest { + return &GetOpenOrdersRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_open_orders_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_open_orders_request_requestgen.go new file mode 100644 index 0000000000..e5617a4f48 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_open_orders_request_requestgen.go @@ -0,0 +1,152 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/trade/open-orders -type GetOpenOrdersRequest -responseDataType []OrderDetail"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetOpenOrdersRequest) Symbol(symbol string) *GetOpenOrdersRequest { + g.symbol = symbol + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetOpenOrdersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetOpenOrdersRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetOpenOrdersRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetOpenOrdersRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetOpenOrdersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetOpenOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetOpenOrdersRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetOpenOrdersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetOpenOrdersRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetOpenOrdersRequest) Do(ctx context.Context) ([]OrderDetail, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/spot/v1/trade/open-orders" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []OrderDetail + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go b/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go index f6081f0da6..4f7e09b32f 100644 --- a/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go @@ -27,7 +27,7 @@ type OrderDetail struct { CTime types.MillisecondTimestamp `json:"cTime"` } -//go:generate PostRequest -url "/api/spot/v1/trade/orderInfo" -type GetOrderDetailRequest -responseDataType .OrderDetail +//go:generate PostRequest -url "/api/spot/v1/trade/orderInfo" -type GetOrderDetailRequest -responseDataType []OrderDetail type GetOrderDetailRequest struct { client requestgen.AuthenticatedAPIClient From fce281b6a84555f1857c47976559799b2e9aed1d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 15:47:58 +0800 Subject: [PATCH 0870/1392] bitgetapi: add GetOrderHistoryRequest --- .../bitgetapi/get_order_history_request.go | 27 +++ .../get_order_history_request_requestgen.go | 182 ++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 pkg/exchange/bitget/bitgetapi/get_order_history_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_order_history_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/get_order_history_request.go b/pkg/exchange/bitget/bitgetapi/get_order_history_request.go new file mode 100644 index 0000000000..aa88605a36 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_order_history_request.go @@ -0,0 +1,27 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" +) + +//go:generate GetRequest -url "/api/spot/v1/trade/history" -type GetOrderHistoryRequest -responseDataType []OrderDetail +type GetOrderHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol string `param:"symbol"` + + // after - order id + after string `param:"after"` + + // before - order id + before string `param:"before"` + + limit string `param:"limit"` +} + +func (c *RestClient) NewGetOrderHistoryRequest() *GetOrderHistoryRequest { + return &GetOrderHistoryRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_order_history_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_order_history_request_requestgen.go new file mode 100644 index 0000000000..8b7d6a6f85 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_order_history_request_requestgen.go @@ -0,0 +1,182 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/trade/history -type GetOrderHistoryRequest -responseDataType []OrderDetail"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetOrderHistoryRequest) Symbol(symbol string) *GetOrderHistoryRequest { + g.symbol = symbol + return g +} + +func (g *GetOrderHistoryRequest) After(after string) *GetOrderHistoryRequest { + g.after = after + return g +} + +func (g *GetOrderHistoryRequest) Before(before string) *GetOrderHistoryRequest { + g.before = before + return g +} + +func (g *GetOrderHistoryRequest) Limit(limit string) *GetOrderHistoryRequest { + g.limit = limit + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetOrderHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetOrderHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + symbol := g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check after field -> json key after + after := g.after + + // assign parameter of after + params["after"] = after + // check before field -> json key before + before := g.before + + // assign parameter of before + params["before"] = before + // check limit field -> json key limit + limit := g.limit + + // assign parameter of limit + params["limit"] = limit + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetOrderHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetOrderHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetOrderHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetOrderHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetOrderHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetOrderHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetOrderHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetOrderHistoryRequest) Do(ctx context.Context) ([]OrderDetail, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/spot/v1/trade/history" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []OrderDetail + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} From b32d890860962b22a42a82f1fa85e085395fefcc Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 15:59:16 +0800 Subject: [PATCH 0871/1392] bitgetapi: add GetFillsRequest --- .../bitget/bitgetapi/get_fills_request.go | 43 +++++ .../bitgetapi/get_fills_request_requestgen.go | 182 ++++++++++++++++++ .../bitgetapi/get_order_history_request.go | 6 +- .../get_order_history_request_requestgen.go | 33 ++-- 4 files changed, 249 insertions(+), 15 deletions(-) create mode 100644 pkg/exchange/bitget/bitgetapi/get_fills_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_fills_request_requestgen.go diff --git a/pkg/exchange/bitget/bitgetapi/get_fills_request.go b/pkg/exchange/bitget/bitgetapi/get_fills_request.go new file mode 100644 index 0000000000..2d80b76754 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_fills_request.go @@ -0,0 +1,43 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type Fill struct { + AccountId string `json:"accountId"` + Symbol string `json:"symbol"` + OrderId string `json:"orderId"` + FillId string `json:"fillId"` + OrderType OrderType `json:"orderType"` + Side OrderSide `json:"side"` + FillPrice fixedpoint.Value `json:"fillPrice"` + FillQuantity fixedpoint.Value `json:"fillQuantity"` + FillTotalAmount fixedpoint.Value `json:"fillTotalAmount"` + CreationTime types.MillisecondTimestamp `json:"cTime"` + FeeCurrency string `json:"feeCcy"` + Fees fixedpoint.Value `json:"fees"` +} + +//go:generate GetRequest -url "/api/spot/v1/trade/fills" -type GetFillsRequest -responseDataType .ServerTime +type GetFillsRequest struct { + client requestgen.AuthenticatedAPIClient + + // after - order id + after *string `param:"after"` + + // before - order id + before *string `param:"before"` + + limit *string `param:"limit"` +} + +func (c *RestClient) NewGetFillsRequest() *GetFillsRequest { + return &GetFillsRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_fills_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_fills_request_requestgen.go new file mode 100644 index 0000000000..7d32f74758 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_fills_request_requestgen.go @@ -0,0 +1,182 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/trade/fills -type GetFillsRequest -responseDataType .ServerTime"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/types" + "net/url" + "reflect" + "regexp" +) + +func (g *GetFillsRequest) After(after string) *GetFillsRequest { + g.after = &after + return g +} + +func (g *GetFillsRequest) Before(before string) *GetFillsRequest { + g.before = &before + return g +} + +func (g *GetFillsRequest) Limit(limit string) *GetFillsRequest { + g.limit = &limit + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetFillsRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetFillsRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check after field -> json key after + if g.after != nil { + after := *g.after + + // assign parameter of after + params["after"] = after + } else { + } + // check before field -> json key before + if g.before != nil { + before := *g.before + + // assign parameter of before + params["before"] = before + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetFillsRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetFillsRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetFillsRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetFillsRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetFillsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetFillsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetFillsRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetFillsRequest) Do(ctx context.Context) (*types.MillisecondTimestamp, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/spot/v1/trade/fills" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data types.MillisecondTimestamp + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bitget/bitgetapi/get_order_history_request.go b/pkg/exchange/bitget/bitgetapi/get_order_history_request.go index aa88605a36..6de083d710 100644 --- a/pkg/exchange/bitget/bitgetapi/get_order_history_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_order_history_request.go @@ -14,12 +14,12 @@ type GetOrderHistoryRequest struct { symbol string `param:"symbol"` // after - order id - after string `param:"after"` + after *string `param:"after"` // before - order id - before string `param:"before"` + before *string `param:"before"` - limit string `param:"limit"` + limit *string `param:"limit"` } func (c *RestClient) NewGetOrderHistoryRequest() *GetOrderHistoryRequest { diff --git a/pkg/exchange/bitget/bitgetapi/get_order_history_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_order_history_request_requestgen.go index 8b7d6a6f85..257aef2105 100644 --- a/pkg/exchange/bitget/bitgetapi/get_order_history_request_requestgen.go +++ b/pkg/exchange/bitget/bitgetapi/get_order_history_request_requestgen.go @@ -17,17 +17,17 @@ func (g *GetOrderHistoryRequest) Symbol(symbol string) *GetOrderHistoryRequest { } func (g *GetOrderHistoryRequest) After(after string) *GetOrderHistoryRequest { - g.after = after + g.after = &after return g } func (g *GetOrderHistoryRequest) Before(before string) *GetOrderHistoryRequest { - g.before = before + g.before = &before return g } func (g *GetOrderHistoryRequest) Limit(limit string) *GetOrderHistoryRequest { - g.limit = limit + g.limit = &limit return g } @@ -52,20 +52,29 @@ func (g *GetOrderHistoryRequest) GetParameters() (map[string]interface{}, error) // assign parameter of symbol params["symbol"] = symbol // check after field -> json key after - after := g.after + if g.after != nil { + after := *g.after - // assign parameter of after - params["after"] = after + // assign parameter of after + params["after"] = after + } else { + } // check before field -> json key before - before := g.before + if g.before != nil { + before := *g.before - // assign parameter of before - params["before"] = before + // assign parameter of before + params["before"] = before + } else { + } // check limit field -> json key limit - limit := g.limit + if g.limit != nil { + limit := *g.limit - // assign parameter of limit - params["limit"] = limit + // assign parameter of limit + params["limit"] = limit + } else { + } return params, nil } From 9c6de12e19d38fe35ff099642dfb4cffbe88cb65 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 17:32:15 +0800 Subject: [PATCH 0872/1392] types: add StrInt64 type for unmarshalling integer in string --- pkg/types/strint.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 pkg/types/strint.go diff --git a/pkg/types/strint.go b/pkg/types/strint.go new file mode 100644 index 0000000000..c69a695072 --- /dev/null +++ b/pkg/types/strint.go @@ -0,0 +1,38 @@ +package types + +import ( + "encoding/json" + "fmt" + "strconv" +) + +type StrInt64 int64 + +func (s *StrInt64) UnmarshalJSON(body []byte) error { + var arg interface{} + if err := json.Unmarshal(body, &arg); err != nil { + return err + } + + switch ta := arg.(type) { + case string: + // parse string + i, err := strconv.ParseInt(ta, 10, 64) + if err != nil { + return err + } + *s = StrInt64(i) + + case int64: + *s = StrInt64(ta) + case int32: + *s = StrInt64(ta) + case int: + *s = StrInt64(ta) + + default: + return fmt.Errorf("StrInt64 error: unsupported value type %T", ta) + } + + return nil +} From 2fe915f73a7ad437ac019fb880d09f430831cf62 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 18:08:40 +0800 Subject: [PATCH 0873/1392] types: add MarshalJSON method on strint64 --- .../bitget/bitgetapi/get_account_request.go | 18 +++++++++++------- .../bitget/bitgetapi/get_fills_request.go | 6 +++--- .../bitgetapi/get_order_detail_request.go | 4 ++-- pkg/types/strint.go | 5 +++++ 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pkg/exchange/bitget/bitgetapi/get_account_request.go b/pkg/exchange/bitget/bitgetapi/get_account_request.go index a3ad8743ab..713eb13d71 100644 --- a/pkg/exchange/bitget/bitgetapi/get_account_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_account_request.go @@ -3,15 +3,19 @@ package bitgetapi //go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data //go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data -import "github.com/c9s/requestgen" +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/types" +) type Account struct { - UserId string `json:"user_id"` - InviterId string `json:"inviter_id"` - Ips string `json:"ips"` - Authorities []string `json:"authorities"` - ParentId string `json:"parentId"` - Trader bool `json:"trader"` + UserId types.StrInt64 `json:"user_id"` + InviterId types.StrInt64 `json:"inviter_id"` + Ips string `json:"ips"` + Authorities []string `json:"authorities"` + ParentId types.StrInt64 `json:"parentId"` + Trader bool `json:"trader"` } //go:generate GetRequest -url "/api/spot/v1/account/getInfo" -type GetAccountRequest -responseDataType .Account diff --git a/pkg/exchange/bitget/bitgetapi/get_fills_request.go b/pkg/exchange/bitget/bitgetapi/get_fills_request.go index 2d80b76754..0172f0cfb3 100644 --- a/pkg/exchange/bitget/bitgetapi/get_fills_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_fills_request.go @@ -11,10 +11,10 @@ import ( ) type Fill struct { - AccountId string `json:"accountId"` + AccountId types.StrInt64 `json:"accountId"` Symbol string `json:"symbol"` - OrderId string `json:"orderId"` - FillId string `json:"fillId"` + OrderId types.StrInt64 `json:"orderId"` + FillId types.StrInt64 `json:"fillId"` OrderType OrderType `json:"orderType"` Side OrderSide `json:"side"` FillPrice fixedpoint.Value `json:"fillPrice"` diff --git a/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go b/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go index 4f7e09b32f..aa5cca0a85 100644 --- a/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go +++ b/pkg/exchange/bitget/bitgetapi/get_order_detail_request.go @@ -11,9 +11,9 @@ import ( ) type OrderDetail struct { - AccountId string `json:"accountId"` + AccountId types.StrInt64 `json:"accountId"` Symbol string `json:"symbol"` - OrderId string `json:"orderId"` + OrderId types.StrInt64 `json:"orderId"` ClientOrderId string `json:"clientOrderId"` Price fixedpoint.Value `json:"price"` Quantity fixedpoint.Value `json:"quantity"` diff --git a/pkg/types/strint.go b/pkg/types/strint.go index c69a695072..c5270d525a 100644 --- a/pkg/types/strint.go +++ b/pkg/types/strint.go @@ -8,6 +8,11 @@ import ( type StrInt64 int64 +func (s *StrInt64) MarshalJSON() ([]byte, error) { + ss := strconv.FormatInt(int64(*s), 10) + return json.Marshal(ss) +} + func (s *StrInt64) UnmarshalJSON(body []byte) error { var arg interface{} if err := json.Unmarshal(body, &arg); err != nil { From 0bb697bc1e47d57e4fcded4c6b920880ef6de4c2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 15 May 2023 16:29:49 +0800 Subject: [PATCH 0874/1392] maxapi: move NewGetMarginLoanHistoryRequest method to the bottom of the file --- .../max/maxapi/v3/get_margin_loan_history_request.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go b/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go index 2904ecc12b..e3d6547e89 100644 --- a/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request.go @@ -13,10 +13,6 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -func (s *Client) NewGetMarginLoanHistoryRequest() *GetMarginLoanHistoryRequest { - return &GetMarginLoanHistoryRequest{client: s.Client} -} - type LoanRecord struct { SN string `json:"sn"` Currency string `json:"currency"` @@ -36,3 +32,7 @@ type GetMarginLoanHistoryRequest struct { endTime *time.Time `param:"endTime,milliseconds"` limit *int `param:"limit"` } + +func (s *Client) NewGetMarginLoanHistoryRequest() *GetMarginLoanHistoryRequest { + return &GetMarginLoanHistoryRequest{client: s.Client} +} From c5e7a78067d578afae0c385263517cb68b36d918 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 15 May 2023 16:29:25 +0800 Subject: [PATCH 0875/1392] types: add RoundDownQuantityByPrecision --- pkg/types/market.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/types/market.go b/pkg/types/market.go index 99b2f9efbd..6b71997984 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -70,6 +70,12 @@ func (m Market) TruncateQuantity(quantity fixedpoint.Value) fixedpoint.Value { return fixedpoint.MustNewFromString(qs) } +// RoundDownQuantityByPrecision uses the volume precision to round down the quantity +// This is different from the TruncateQuantity, which uses StepSize (it uses fewer fractions to truncate) +func (m Market) RoundDownQuantityByPrecision(quantity fixedpoint.Value) fixedpoint.Value { + return quantity.Round(m.VolumePrecision, fixedpoint.Down) +} + func (m Market) TruncatePrice(price fixedpoint.Value) fixedpoint.Value { return fixedpoint.MustNewFromString(m.FormatPrice(price)) } From 86a99b59020a18a7cee97903a785e6ef243837df Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 18 May 2023 18:23:58 +0800 Subject: [PATCH 0876/1392] grid2: truncate max base quantity --- pkg/strategy/grid2/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6192c63ddc..b2ea027cbf 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -785,7 +785,11 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 || maxBaseQuantity.Compare(minBaseQuantity) <= 0 { maxNumberOfSellOrders-- maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) + + // maxBaseQuantity = s.Market.RoundDownQuantityByPrecision(maxBaseQuantity) + maxBaseQuantity = s.Market.TruncateQuantity(maxBaseQuantity) } + s.logger.Infof("grid base investment sell orders: %d", maxNumberOfSellOrders) if maxNumberOfSellOrders > 0 { s.logger.Infof("grid base investment quantity: %f (base investment) / %d (number of sell orders) = %f (base quantity per order)", baseInvestment.Float64(), maxNumberOfSellOrders, maxBaseQuantity.Float64()) From 0c4cd7049f70d6350b96f9bab4660dc6b4adee45 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 19 May 2023 15:04:17 +0800 Subject: [PATCH 0877/1392] grid2: rewrite the base+quote algo --- pkg/strategy/grid2/strategy.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b2ea027cbf..3b5de8a72c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -779,22 +779,22 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders // so that the quantity can be increased. - maxNumberOfSellOrders := numberOfSellOrders + 1 - minBaseQuantity := fixedpoint.Max(s.Market.MinNotional.Div(lastPrice), s.Market.MinQuantity) - maxBaseQuantity := fixedpoint.Zero - for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 || maxBaseQuantity.Compare(minBaseQuantity) <= 0 { - maxNumberOfSellOrders-- - maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) + baseQuantity := s.Market.TruncateQuantity( + baseInvestment.Div( + fixedpoint.NewFromInt( + int64(numberOfSellOrders)))) - // maxBaseQuantity = s.Market.RoundDownQuantityByPrecision(maxBaseQuantity) - maxBaseQuantity = s.Market.TruncateQuantity(maxBaseQuantity) - } + minBaseQuantity := fixedpoint.Max( + s.Market.MinNotional.Div(lastPrice), + s.Market.MinQuantity) - s.logger.Infof("grid base investment sell orders: %d", maxNumberOfSellOrders) - if maxNumberOfSellOrders > 0 { - s.logger.Infof("grid base investment quantity: %f (base investment) / %d (number of sell orders) = %f (base quantity per order)", baseInvestment.Float64(), maxNumberOfSellOrders, maxBaseQuantity.Float64()) + if baseQuantity.Compare(minBaseQuantity) <= 0 { + numberOfSellOrders = int(math.Floor(baseInvestment.Div(minBaseQuantity).Float64())) } + s.logger.Infof("grid base investment sell orders: %d", numberOfSellOrders) + s.logger.Infof("grid base investment quantity: %f (base investment) / %d (number of sell orders) = %f (base quantity per order)", baseInvestment.Float64(), numberOfSellOrders, baseQuantity.Float64()) + // calculate quantity with quote investment totalQuotePrice := fixedpoint.Zero // quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + .... @@ -802,7 +802,7 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv // quoteInvestment = (p1 + p2 + p3) * q // maxBuyQuantity = quoteInvestment / (p1 + p2 + p3) si := -1 - for i := len(pins) - 1 - maxNumberOfSellOrders; i >= 0; i-- { + for i := len(pins) - 1 - numberOfSellOrders; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) @@ -838,8 +838,8 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv } quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) - if maxNumberOfSellOrders > 0 { - return fixedpoint.Min(quoteSideQuantity, maxBaseQuantity), nil + if numberOfSellOrders > 0 { + return fixedpoint.Min(quoteSideQuantity, baseQuantity), nil } return quoteSideQuantity, nil From 3a2dbc934b7ee22e08aa088d1b23e662ab9ce5d6 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 19 May 2023 16:37:44 +0800 Subject: [PATCH 0878/1392] grid2: add TestStrategy_calculateBaseQuoteInvestmentQuantity test case --- pkg/strategy/grid2/strategy.go | 5 +++- pkg/strategy/grid2/strategy_test.go | 36 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3b5de8a72c..6befe6b0da 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -777,6 +777,9 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv numberOfSellOrders++ } + // avoid placing a sell order above the last price + numberOfSellOrders-- + // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders // so that the quantity can be increased. baseQuantity := s.Market.TruncateQuantity( @@ -785,7 +788,7 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv int64(numberOfSellOrders)))) minBaseQuantity := fixedpoint.Max( - s.Market.MinNotional.Div(lastPrice), + s.Market.MinNotional.Div(s.UpperPrice), s.Market.MinQuantity) if baseQuantity.Compare(minBaseQuantity) <= 0 { diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 432245916d..4076b3c64e 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -285,6 +285,41 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { }) } +func TestStrategy_calculateBaseQuoteInvestmentQuantity(t *testing.T) { + t.Run("basic", func(t *testing.T) { + s := newTestStrategy() + s.Market = types.Market{ + BaseCurrency: "ETH", + QuoteCurrency: "USDT", + TickSize: number(0.01), + StepSize: number(0.00001), + PricePrecision: 2, + VolumePrecision: 6, + MinNotional: number(8.000), + MinQuantity: number(0.00030), + } + s.UpperPrice = number(200.0) + s.LowerPrice = number(100.0) + s.GridNum = 7 + s.Compound = true + + lastPrice := number(180.0) + quoteInvestment := number(334.0) // 333.33 + baseInvestment := number(0.5) + quantity, err := s.calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice, []Pin{ + Pin(number(100.00)), + Pin(number(116.67)), + Pin(number(133.33)), + Pin(number(150.00)), + Pin(number(166.67)), + Pin(number(183.33)), + Pin(number(200.00)), + }) + assert.NoError(t, err) + assert.InDelta(t, 0.5, quantity.Float64(), 0.0001) + }) +} + func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { t.Run("quote quantity", func(t *testing.T) { @@ -380,6 +415,7 @@ func newTestMarket() types.Market { BaseCurrency: "BTC", QuoteCurrency: "USDT", TickSize: number(0.01), + StepSize: number(0.00001), PricePrecision: 2, VolumePrecision: 8, MinNotional: number(10.0), From 4c13171cb0933602cfac5647a9c29ea4168aae46 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 19 May 2023 16:42:26 +0800 Subject: [PATCH 0879/1392] grid2: add more test for spec --- pkg/strategy/grid2/strategy_test.go | 71 +++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 4076b3c64e..1ef1d9104e 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -286,18 +286,9 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { } func TestStrategy_calculateBaseQuoteInvestmentQuantity(t *testing.T) { - t.Run("basic", func(t *testing.T) { + t.Run("1 sell", func(t *testing.T) { s := newTestStrategy() - s.Market = types.Market{ - BaseCurrency: "ETH", - QuoteCurrency: "USDT", - TickSize: number(0.01), - StepSize: number(0.00001), - PricePrecision: 2, - VolumePrecision: 6, - MinNotional: number(8.000), - MinQuantity: number(0.00030), - } + s.Market = newTestMarket("ETHUSDT") s.UpperPrice = number(200.0) s.LowerPrice = number(100.0) s.GridNum = 7 @@ -318,10 +309,34 @@ func TestStrategy_calculateBaseQuoteInvestmentQuantity(t *testing.T) { assert.NoError(t, err) assert.InDelta(t, 0.5, quantity.Float64(), 0.0001) }) + + t.Run("6 sell", func(t *testing.T) { + s := newTestStrategy() + s.Market = newTestMarket("ETHUSDT") + s.UpperPrice = number(200.0) + s.LowerPrice = number(100.0) + s.GridNum = 7 + s.Compound = true + + lastPrice := number(95.0) + quoteInvestment := number(334.0) // 333.33 + baseInvestment := number(0.5) + quantity, err := s.calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice, []Pin{ + Pin(number(100.00)), + Pin(number(116.67)), + Pin(number(133.33)), + Pin(number(150.00)), + Pin(number(166.67)), + Pin(number(183.33)), + Pin(number(200.00)), + }) + assert.NoError(t, err) + assert.InDelta(t, 0.08333, quantity.Float64(), 0.0001) + }) + } func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { - t.Run("quote quantity", func(t *testing.T) { // quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000) * q // q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000) @@ -410,7 +425,33 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { }) } -func newTestMarket() types.Market { +func newTestMarket(symbol string) types.Market { + switch symbol { + case "BTCUSDT": + return types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + TickSize: number(0.01), + StepSize: number(0.00001), + PricePrecision: 2, + VolumePrecision: 8, + MinNotional: number(10.0), + MinQuantity: number(0.001), + } + case "ETHUSDT": + return types.Market{ + BaseCurrency: "ETH", + QuoteCurrency: "USDT", + TickSize: number(0.01), + StepSize: number(0.00001), + PricePrecision: 2, + VolumePrecision: 6, + MinNotional: number(8.000), + MinQuantity: number(0.00030), + } + } + + // default return types.Market{ BaseCurrency: "BTC", QuoteCurrency: "USDT", @@ -426,7 +467,7 @@ func newTestMarket() types.Market { var testOrderID = uint64(0) func newTestOrder(price, quantity fixedpoint.Value, side types.SideType) types.Order { - market := newTestMarket() + market := newTestMarket("BTCUSDT") testOrderID++ return types.Order{ SubmitOrder: types.SubmitOrder{ @@ -450,7 +491,7 @@ func newTestOrder(price, quantity fixedpoint.Value, side types.SideType) types.O } func newTestStrategy() *Strategy { - market := newTestMarket() + market := newTestMarket("BTCUSDT") s := &Strategy{ logger: logrus.NewEntry(logrus.New()), From c93a3d14b3099c3c4937cb3597da0d82001903f4 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 19 May 2023 16:46:17 +0800 Subject: [PATCH 0880/1392] grid2: round up minBaseQuantity --- pkg/strategy/grid2/strategy.go | 1 + pkg/types/market.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6befe6b0da..67f8e5fda9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -793,6 +793,7 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv if baseQuantity.Compare(minBaseQuantity) <= 0 { numberOfSellOrders = int(math.Floor(baseInvestment.Div(minBaseQuantity).Float64())) + baseQuantity = s.Market.RoundUpQuantityByPrecision(minBaseQuantity) } s.logger.Infof("grid base investment sell orders: %d", numberOfSellOrders) diff --git a/pkg/types/market.go b/pkg/types/market.go index 6b71997984..d9660c970f 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -76,6 +76,11 @@ func (m Market) RoundDownQuantityByPrecision(quantity fixedpoint.Value) fixedpoi return quantity.Round(m.VolumePrecision, fixedpoint.Down) } +// RoundUpQuantityByPrecision uses the volume precision to round up the quantity +func (m Market) RoundUpQuantityByPrecision(quantity fixedpoint.Value) fixedpoint.Value { + return quantity.Round(m.VolumePrecision, fixedpoint.Up) +} + func (m Market) TruncatePrice(price fixedpoint.Value) fixedpoint.Value { return fixedpoint.MustNewFromString(m.FormatPrice(price)) } From a083ec8395dcb3853a9bc51cf287a367fe402dc2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 22 May 2023 17:20:16 +0800 Subject: [PATCH 0881/1392] grid2: check numberOfSellOrders == 0 --- pkg/strategy/grid2/strategy.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 67f8e5fda9..e0e58fe7a6 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -779,6 +779,9 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv // avoid placing a sell order above the last price numberOfSellOrders-- + if numberOfSellOrders < 0 { + numberOfSellOrders = 0 + } // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders // so that the quantity can be increased. @@ -1331,7 +1334,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin if price.Compare(lastPrice) >= 0 { si = i - // do not place sell order when i == 0 + // do not place sell order when i == 0 (the bottom of grid) if i == 0 { continue } From 6ae5d2f33a7ffd5b30a3c16ebbbeb99963509372 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 22 May 2023 17:25:00 +0800 Subject: [PATCH 0882/1392] grid2: round down before the quantity calculation --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e0e58fe7a6..5413c70233 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -795,8 +795,8 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv s.Market.MinQuantity) if baseQuantity.Compare(minBaseQuantity) <= 0 { - numberOfSellOrders = int(math.Floor(baseInvestment.Div(minBaseQuantity).Float64())) baseQuantity = s.Market.RoundUpQuantityByPrecision(minBaseQuantity) + numberOfSellOrders = int(math.Floor(baseInvestment.Div(baseQuantity).Float64())) } s.logger.Infof("grid base investment sell orders: %d", numberOfSellOrders) From f11d869d021f0dd3ebd4d369e3c430752f58ed62 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 22 May 2023 17:26:06 +0800 Subject: [PATCH 0883/1392] grid2: sub 1 only when num > 0 --- pkg/strategy/grid2/strategy.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5413c70233..7cd54af6dd 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -778,9 +778,8 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv } // avoid placing a sell order above the last price - numberOfSellOrders-- - if numberOfSellOrders < 0 { - numberOfSellOrders = 0 + if numberOfSellOrders > 0 { + numberOfSellOrders-- } // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders From 0c6ef38ea3640ae584788c20eb972a104182aeb2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 22 May 2023 18:07:40 +0800 Subject: [PATCH 0884/1392] grid2: apply baseGridNumber --- pkg/strategy/grid2/strategy.go | 39 ++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7cd54af6dd..0549e19de9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -91,6 +91,9 @@ type Strategy struct { // GridNum is the grid number, how many orders you want to post on the orderbook. GridNum int64 `json:"gridNumber"` + // BaseGridNum is an optional field used for base investment sell orders + BaseGridNum int `json:"baseGridNumber,omitempty"` + AutoRange *types.SimpleDuration `json:"autoRange"` UpperPrice fixedpoint.Value `json:"upperPrice"` @@ -761,25 +764,29 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv // maxBaseQuantity = baseInvestment / numberOfSellOrders // if maxBaseQuantity < minQuantity or maxBaseQuantity * priceLowest < minNotional // then reduce the numberOfSellOrders - numberOfSellOrders := 0 - for i := len(pins) - 1; i >= 0; i-- { - pin := pins[i] - price := fixedpoint.Value(pin) - sellPrice := price - if s.ProfitSpread.Sign() > 0 { - sellPrice = sellPrice.Add(s.ProfitSpread) - } + numberOfSellOrders := s.BaseGridNum + + // if it's not configured + if numberOfSellOrders == 0 { + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + sellPrice := price + if s.ProfitSpread.Sign() > 0 { + sellPrice = sellPrice.Add(s.ProfitSpread) + } - if sellPrice.Compare(lastPrice) < 0 { - break - } + if sellPrice.Compare(lastPrice) < 0 { + break + } - numberOfSellOrders++ - } + numberOfSellOrders++ + } - // avoid placing a sell order above the last price - if numberOfSellOrders > 0 { - numberOfSellOrders-- + // avoid placing a sell order above the last price + if numberOfSellOrders > 0 { + numberOfSellOrders-- + } } // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders From 2046ccc791916ef7395e6a33b206224dc7ad8ef5 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 22 May 2023 18:10:51 +0800 Subject: [PATCH 0885/1392] grid2: pull out sell boolean var --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 0549e19de9..f7c22c6cb5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1336,8 +1336,8 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin quantity = s.QuantityOrAmount.Amount.Div(price) } - // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... - if price.Compare(lastPrice) >= 0 { + placeSell := price.Compare(lastPrice) >= 0 + if placeSell { si = i // do not place sell order when i == 0 (the bottom of grid) From ce2bd7ca7d6935ca18cdc7a472ab68d286b73713 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 22 May 2023 18:13:51 +0800 Subject: [PATCH 0886/1392] grid2: override placeSell if BaseGridNumber is defined --- pkg/strategy/grid2/strategy.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f7c22c6cb5..09071e2472 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1337,6 +1337,12 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin } placeSell := price.Compare(lastPrice) >= 0 + + // override the relative price position for sell order if BaseGridNum is defined + if s.BaseGridNum > 0 && i > len(pins)-1-s.BaseGridNum { + placeSell = true + } + if placeSell { si = i From d5cf53ee948b8eb6fe3dcc3e7b344f1df36d4fba Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 22 May 2023 18:20:34 +0800 Subject: [PATCH 0887/1392] grid2: fix comparison --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 09071e2472..8acc01e766 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1339,7 +1339,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin placeSell := price.Compare(lastPrice) >= 0 // override the relative price position for sell order if BaseGridNum is defined - if s.BaseGridNum > 0 && i > len(pins)-1-s.BaseGridNum { + if s.BaseGridNum > 0 && i >= len(pins)-1-s.BaseGridNum { placeSell = true } From 1cf788c9255818c1a9c645f884918abf5f96d925 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 23 May 2023 17:34:03 +0800 Subject: [PATCH 0888/1392] grid2: fix base + quote order placement and add test case --- pkg/strategy/grid2/strategy.go | 2 +- pkg/strategy/grid2/strategy_test.go | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 8acc01e766..9db78e6d82 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1351,7 +1351,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin continue } - if usedBase.Add(quantity).Compare(totalBase) < 0 { + if usedBase.Add(quantity).Compare(totalBase) <= 0 { submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, Type: types.OrderTypeLimit, diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 1ef1d9104e..3cad2e3838 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -167,14 +167,22 @@ func TestStrategy_generateGridOrders(t *testing.T) { }, orders) }) - t.Run("base + quote", func(t *testing.T) { + t.Run("base and quote", func(t *testing.T) { s := newTestStrategy() s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) s.grid.CalculateArithmeticPins() - s.QuantityOrAmount.Quantity = number(0.01) + quoteInvestment := number(10_000.0) + baseInvestment := number(0.1) lastPrice := number(15300) - orders, err := s.generateGridOrders(number(10000.0), number(0.021), lastPrice) + + quantity, err := s.calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice, s.grid.Pins) + assert.NoError(t, err) + assert.Equal(t, number(0.025), quantity) + + s.QuantityOrAmount.Quantity = quantity + + orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice) assert.NoError(t, err) if !assert.Equal(t, 10, len(orders)) { for _, o := range orders { @@ -185,8 +193,9 @@ func TestStrategy_generateGridOrders(t *testing.T) { assertPriceSide(t, []PriceSideAssert{ {number(20000.0), types.SideTypeSell}, {number(19000.0), types.SideTypeSell}, - {number(17000.0), types.SideTypeBuy}, - {number(16000.0), types.SideTypeBuy}, + {number(18000.0), types.SideTypeSell}, + {number(17000.0), types.SideTypeSell}, + // -- 16_000 should be empty {number(15000.0), types.SideTypeBuy}, {number(14000.0), types.SideTypeBuy}, {number(13000.0), types.SideTypeBuy}, From 26cbd60a661164546c885d177449693cedd2bbf0 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 23 May 2023 17:36:01 +0800 Subject: [PATCH 0889/1392] grid2: add one more test case for base + quote --- pkg/strategy/grid2/strategy_test.go | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 3cad2e3838..4e4ada351c 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -205,6 +205,46 @@ func TestStrategy_generateGridOrders(t *testing.T) { }, orders) }) + t.Run("base and quote with pre-calculated baseGridNumber", func(t *testing.T) { + s := newTestStrategy() + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + + s.BaseGridNum = 4 + + quoteInvestment := number(10_000.0) + baseInvestment := number(0.1) + lastPrice := number(12300) // last price should not affect the sell order calculation + + quantity, err := s.calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice, s.grid.Pins) + assert.NoError(t, err) + assert.Equal(t, number(0.025), quantity) + + s.QuantityOrAmount.Quantity = quantity + + orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice) + assert.NoError(t, err) + if !assert.Equal(t, 10, len(orders)) { + for _, o := range orders { + t.Logf("- %s %s", o.Price.String(), o.Side) + } + } + + assertPriceSide(t, []PriceSideAssert{ + {number(20000.0), types.SideTypeSell}, + {number(19000.0), types.SideTypeSell}, + {number(18000.0), types.SideTypeSell}, + {number(17000.0), types.SideTypeSell}, + // -- 16_000 should be empty + {number(15000.0), types.SideTypeBuy}, + {number(14000.0), types.SideTypeBuy}, + {number(13000.0), types.SideTypeBuy}, + {number(12000.0), types.SideTypeBuy}, + {number(11000.0), types.SideTypeBuy}, + {number(10000.0), types.SideTypeBuy}, + }, orders) + }) + t.Run("enough base + quote", func(t *testing.T) { s := newTestStrategy() s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) From 862848721f8b279b353b96ccc1349003e9fb0ce8 Mon Sep 17 00:00:00 2001 From: Yo-An Lin Date: Wed, 24 May 2023 17:52:14 +0800 Subject: [PATCH 0890/1392] Fix placeSell condition --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 9db78e6d82..fc95d9e9e3 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1339,8 +1339,8 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin placeSell := price.Compare(lastPrice) >= 0 // override the relative price position for sell order if BaseGridNum is defined - if s.BaseGridNum > 0 && i >= len(pins)-1-s.BaseGridNum { - placeSell = true + if s.BaseGridNum > 0 { + placeSell = i >= len(pins)-1-s.BaseGridNum } if placeSell { From 508f42663dd1f449ed93eefb2c42739fb93231b2 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 24 May 2023 11:31:28 +0900 Subject: [PATCH 0891/1392] fix: some types in SeriesExtended are not supported --- pkg/types/indicator.go | 72 +++++++++++++++---------------------- pkg/types/indicator_test.go | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 43 deletions(-) diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index f5263ac2fd..e3dd7690fe 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -383,27 +383,8 @@ type AddSeriesResult struct { // Add two series, result[i] = a[i] + b[i] func Add(a interface{}, b interface{}) SeriesExtend { - var aa Series - var bb Series - - switch tp := a.(type) { - case float64: - aa = NumberSeries(tp) - case Series: - aa = tp - default: - panic("input should be either *Series or float64") - - } - switch tp := b.(type) { - case float64: - bb = NumberSeries(tp) - case Series: - bb = tp - default: - panic("input should be either *Series or float64") - - } + aa := switchIface(a) + bb := switchIface(b) return NewSeries(&AddSeriesResult{aa, bb}) } @@ -473,7 +454,7 @@ func switchIface(b interface{}) Series { return tp default: fmt.Println(reflect.TypeOf(b)) - panic("input should be either *Series or float64") + panic("input should be either *Series or numbers") } } @@ -515,28 +496,9 @@ var _ Series = &DivSeriesResult{} // Multiple two series, result[i] = a[i] * b[i] func Mul(a interface{}, b interface{}) SeriesExtend { - var aa Series - var bb Series - - switch tp := a.(type) { - case float64: - aa = NumberSeries(tp) - case Series: - aa = tp - default: - panic("input should be either Series or float64") - } - switch tp := b.(type) { - case float64: - bb = NumberSeries(tp) - case Series: - bb = tp - default: - panic("input should be either Series or float64") - - } + aa := switchIface(a) + bb := switchIface(b) return NewSeries(&MulSeriesResult{aa, bb}) - } type MulSeriesResult struct { @@ -577,6 +539,18 @@ func Dot(a interface{}, b interface{}, limit ...int) float64 { case float64: aaf = tp isaf = true + case int32: + aaf = float64(tp) + isaf = true + case int64: + aaf = float64(tp) + isaf = true + case float32: + aaf = float64(tp) + isaf = true + case int: + aaf = float64(tp) + isaf = true case Series: aas = tp isaf = false @@ -587,6 +561,18 @@ func Dot(a interface{}, b interface{}, limit ...int) float64 { case float64: bbf = tp isbf = true + case int32: + aaf = float64(tp) + isaf = true + case int64: + aaf = float64(tp) + isaf = true + case float32: + aaf = float64(tp) + isaf = true + case int: + aaf = float64(tp) + isaf = true case Series: bbs = tp isbf = false diff --git a/pkg/types/indicator_test.go b/pkg/types/indicator_test.go index 7da2c17c5b..47d4886341 100644 --- a/pkg/types/indicator_test.go +++ b/pkg/types/indicator_test.go @@ -2,6 +2,7 @@ package types import ( //"os" + "math" "testing" "time" @@ -12,6 +13,14 @@ import ( "github.com/c9s/bbgo/pkg/datatype/floats" ) +func TestQueue(t *testing.T) { + zeroq := NewQueue(0) + assert.Equal(t, zeroq.Last(), 0.) + assert.Equal(t, zeroq.Index(0), 0.) + zeroq.Update(1.) + assert.Equal(t, zeroq.Length(), 0) +} + func TestFloat(t *testing.T) { var a Series = Minus(3., 2.) assert.Equal(t, a.Last(), 1.) @@ -123,6 +132,64 @@ func TestSigmoid(t *testing.T) { } } +func TestHighLowest(t *testing.T) { + a := floats.Slice{3.0, 1.0, 2.1} + assert.Equal(t, 3.0, Highest(&a, 4)) + assert.Equal(t, 1.0, Lowest(&a, 4)) +} + +func TestAdd(t *testing.T) { + var a NumberSeries = 3.0 + var b NumberSeries = 2.0 + out := Add(&a, &b) + assert.Equal(t, out.Last(), 5.0) + assert.Equal(t, out.Index(0), 5.0) + assert.Equal(t, out.Length(), math.MaxInt32) +} + +func TestDiv(t *testing.T) { + a := floats.Slice{3.0, 1.0, 2.0} + b := NumberSeries(2.0) + out := Div(&a, &b) + assert.Equal(t, out.Last(), 1.0) + assert.Equal(t, out.Length(), 3) + assert.Equal(t, out.Index(1), 0.5) +} + +func TestMul(t *testing.T) { + a := floats.Slice{3.0, 1.0, 2.0} + b := NumberSeries(2.0) + out := Mul(&a, &b) + assert.Equal(t, out.Last(), 4.0) + assert.Equal(t, out.Length(), 3) + assert.Equal(t, out.Index(1), 2.0) +} + +func TestArray(t *testing.T) { + a := floats.Slice{3.0, 1.0, 2.0} + out := Array(&a, 1) + assert.Equal(t, len(out), 1) + out = Array(&a, 4) + assert.Equal(t, len(out), 3) +} + +func TestSwitchInterface(t *testing.T) { + var a int = 1 + var af float64 = 1.0 + var b int32 = 2 + var bf float64 = 2.0 + var c int64 = 3 + var cf float64 = 3.0 + var d float32 = 4.0 + var df float64 = 4.0 + var e float64 = 5.0 + assert.Equal(t, switchIface(a).Last(), af) + assert.Equal(t, switchIface(b).Last(), bf) + assert.Equal(t, switchIface(c).Last(), cf) + assert.Equal(t, switchIface(d).Last(), df) + assert.Equal(t, switchIface(e).Last(), e) +} + // from https://en.wikipedia.org/wiki/Logistic_regression func TestLogisticRegression(t *testing.T) { a := []floats.Slice{{0.5, 0.75, 1., 1.25, 1.5, 1.75, 1.75, 2.0, 2.25, 2.5, 2.75, 3., 3.25, 3.5, 4., 4.25, 4.5, 4.75, 5., 5.5}} From 8e426ca4bf0bfe2ac039c1722fb877d4f0cc538a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 25 May 2023 13:31:19 +0800 Subject: [PATCH 0892/1392] grid2: add last price == sell price case --- pkg/strategy/grid2/strategy_test.go | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 4e4ada351c..73ce5814af 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -245,6 +245,45 @@ func TestStrategy_generateGridOrders(t *testing.T) { }, orders) }) + t.Run("base and quote with last price eq sell price", func(t *testing.T) { + s := newTestStrategy() + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + s.BaseGridNum = 4 + + quoteInvestment := number(10_000.0) + baseInvestment := number(0.1) + lastPrice := number(17000) // last price should not affect the sell order calculation + + quantity, err := s.calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice, s.grid.Pins) + assert.NoError(t, err) + assert.Equal(t, number(0.025), quantity) + + s.QuantityOrAmount.Quantity = quantity + + orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice) + assert.NoError(t, err) + if !assert.Equal(t, 10, len(orders)) { + for _, o := range orders { + t.Logf("- %s %s", o.Price.String(), o.Side) + } + } + + assertPriceSide(t, []PriceSideAssert{ + {number(20000.0), types.SideTypeSell}, + {number(19000.0), types.SideTypeSell}, + {number(18000.0), types.SideTypeSell}, + {number(17000.0), types.SideTypeSell}, + // -- 16_000 should be empty + {number(15000.0), types.SideTypeBuy}, + {number(14000.0), types.SideTypeBuy}, + {number(13000.0), types.SideTypeBuy}, + {number(12000.0), types.SideTypeBuy}, + {number(11000.0), types.SideTypeBuy}, + {number(10000.0), types.SideTypeBuy}, + }, orders) + }) + t.Run("enough base + quote", func(t *testing.T) { s := newTestStrategy() s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) From f7a5c847689033d9997dace5c7ea59ea7e6c2895 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 25 May 2023 14:01:22 +0800 Subject: [PATCH 0893/1392] all: reformat code --- pkg/cache/cache.go | 2 +- pkg/service/memory.go | 2 +- pkg/service/persistence.go | 2 +- pkg/service/persistence_redis.go | 2 +- pkg/strategy/grid2/grid_dnum_test.go | 2 +- pkg/strategy/grid2/grid_int64_test.go | 2 +- pkg/strategy/grid2/grid_test.go | 2 +- pkg/strategy/grid2/metrics.go | 2 +- pkg/strategy/grid2/strategy_test.go | 8 ++++---- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index f659e0f3fb..a1bfb9f845 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -175,4 +175,4 @@ func loadMarketsFromFile(ctx context.Context, ex types.Exchange) (markets types. return ex.QueryMarkets(ctx) }) return markets, err -} \ No newline at end of file +} diff --git a/pkg/service/memory.go b/pkg/service/memory.go index 978b3d2caa..efa0716bce 100644 --- a/pkg/service/memory.go +++ b/pkg/service/memory.go @@ -59,4 +59,4 @@ func (store *MemoryStore) Reset() error { delete(store.memory.Slots, store.Key) return nil -} \ No newline at end of file +} diff --git a/pkg/service/persistence.go b/pkg/service/persistence.go index 3e82ffde49..0cd04ac7f5 100644 --- a/pkg/service/persistence.go +++ b/pkg/service/persistence.go @@ -26,4 +26,4 @@ type RedisPersistenceConfig struct { type JsonPersistenceConfig struct { Directory string `yaml:"directory" json:"directory"` -} \ No newline at end of file +} diff --git a/pkg/service/persistence_redis.go b/pkg/service/persistence_redis.go index 0c00351921..8688c42412 100644 --- a/pkg/service/persistence_redis.go +++ b/pkg/service/persistence_redis.go @@ -109,4 +109,4 @@ func (store *RedisStore) Save(val interface{}) error { func (store *RedisStore) Reset() error { _, err := store.redis.Del(context.Background(), store.ID).Result() return err -} \ No newline at end of file +} diff --git a/pkg/strategy/grid2/grid_dnum_test.go b/pkg/strategy/grid2/grid_dnum_test.go index bffb79ea31..5070a65d5f 100644 --- a/pkg/strategy/grid2/grid_dnum_test.go +++ b/pkg/strategy/grid2/grid_dnum_test.go @@ -90,4 +90,4 @@ func TestGrid_HasPrice_Dnum(t *testing.T) { assert.True(t, grid.HasPrice(number("36.666")), "found 36.666 price ok") }) -} \ No newline at end of file +} diff --git a/pkg/strategy/grid2/grid_int64_test.go b/pkg/strategy/grid2/grid_int64_test.go index 1603642c72..73ba412ec3 100644 --- a/pkg/strategy/grid2/grid_int64_test.go +++ b/pkg/strategy/grid2/grid_int64_test.go @@ -90,4 +90,4 @@ func TestGrid_HasPrice(t *testing.T) { assert.True(t, grid.HasPrice(number("36.666")), "found 36.666 price ok") }) -} \ No newline at end of file +} diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index cf242b7e8a..8176ad8d00 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -213,4 +213,4 @@ func Test_calculateArithmeticPins(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/pkg/strategy/grid2/metrics.go b/pkg/strategy/grid2/metrics.go index 56e9638acc..ed4f849cd1 100644 --- a/pkg/strategy/grid2/metrics.go +++ b/pkg/strategy/grid2/metrics.go @@ -210,4 +210,4 @@ func registerMetrics() { metricsGridFilledOrderPrice, ) metricsRegistered = true -} \ No newline at end of file +} diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 73ce5814af..3cdcb79538 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -1240,7 +1240,7 @@ func Test_buildPinOrderMap(t *testing.T) { t.Run("successful case", func(t *testing.T) { openOrders := []types.Order{ - types.Order{ + { SubmitOrder: types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, @@ -1275,7 +1275,7 @@ func Test_buildPinOrderMap(t *testing.T) { t.Run("there is one order with non-pin price in openOrders", func(t *testing.T) { openOrders := []types.Order{ - types.Order{ + { SubmitOrder: types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, @@ -1301,7 +1301,7 @@ func Test_buildPinOrderMap(t *testing.T) { t.Run("there are duplicated open orders at same pin", func(t *testing.T) { openOrders := []types.Order{ - types.Order{ + { SubmitOrder: types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, @@ -1320,7 +1320,7 @@ func Test_buildPinOrderMap(t *testing.T) { ExecutedQuantity: number(0.0), IsWorking: false, }, - types.Order{ + { SubmitOrder: types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, From bcf77141caf044a353e649b8cbcf1bfc1cd80316 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 25 May 2023 22:15:14 +0800 Subject: [PATCH 0894/1392] all: re-design and refactor indicator api --- go.mod | 2 +- pkg/datatype/floats/slice.go | 64 ++++++++--- pkg/datatype/floats/slice_test.go | 25 ++++ pkg/indicator/ewmastream_callbacks.go | 5 + pkg/indicator/float64updater_callbacks.go | 15 +++ pkg/indicator/klinestream_callbacks.go | 17 +++ pkg/indicator/macd.go | 16 +++ pkg/indicator/macd2.go | 132 ++++++++++++++++++++++ pkg/indicator/macd2_test.go | 35 ++++++ pkg/types/indicator.go | 6 +- pkg/types/seriesbase_imp.go | 4 + 11 files changed, 302 insertions(+), 19 deletions(-) create mode 100644 pkg/datatype/floats/slice_test.go create mode 100644 pkg/indicator/ewmastream_callbacks.go create mode 100644 pkg/indicator/float64updater_callbacks.go create mode 100644 pkg/indicator/klinestream_callbacks.go create mode 100644 pkg/indicator/macd2.go create mode 100644 pkg/indicator/macd2_test.go diff --git a/go.mod b/go.mod index 205e413b4e..a902c8d055 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/c9s/bbgo -go 1.17 +go 1.18 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index 4aab25eb87..40b3a427bb 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -16,6 +16,12 @@ func (s *Slice) Push(v float64) { *s = append(*s, v) } +func (s *Slice) Append(vs ...float64) { + *s = append(*s, vs...) +} + +// Update equals to Push() +// which push an element into the slice func (s *Slice) Update(v float64) { *s = append(*s, v) } @@ -34,6 +40,38 @@ func (s Slice) Min() float64 { return floats.Min(s) } +func (s Slice) Sub(b Slice) (c Slice) { + if len(s) != len(b) { + return c + } + + c = make(Slice, len(s)) + for i := 0; i < len(s); i++ { + ai := s[i] + bi := b[i] + ci := ai - bi + c[i] = ci + } + + return c +} + +func (s Slice) Add(b Slice) (c Slice) { + if len(s) != len(b) { + return c + } + + c = make(Slice, len(s)) + for i := 0; i < len(s); i++ { + ai := s[i] + bi := b[i] + ci := ai + bi + c[i] = ci + } + + return c +} + func (s Slice) Sum() (sum float64) { return floats.Sum(s) } @@ -125,27 +163,27 @@ func (s Slice) Normalize() Slice { return s.DivScalar(s.Sum()) } -func (s *Slice) Last() float64 { - length := len(*s) +func (s Slice) Addr() *Slice { + return &s +} + +// Last, Index, Length implements the types.Series interface +func (s Slice) Last() float64 { + length := len(s) if length > 0 { - return (*s)[length-1] + return (s)[length-1] } return 0.0 } -func (s *Slice) Index(i int) float64 { - length := len(*s) +func (s Slice) Index(i int) float64 { + length := len(s) if length-i <= 0 || i < 0 { return 0.0 } - return (*s)[length-i-1] -} - -func (s *Slice) Length() int { - return len(*s) + return (s)[length-i-1] } -func (s Slice) Addr() *Slice { - return &s +func (s Slice) Length() int { + return len(s) } - diff --git a/pkg/datatype/floats/slice_test.go b/pkg/datatype/floats/slice_test.go new file mode 100644 index 0000000000..d5fdc000c6 --- /dev/null +++ b/pkg/datatype/floats/slice_test.go @@ -0,0 +1,25 @@ +package floats + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSub(t *testing.T) { + a := New(1, 2, 3, 4, 5) + b := New(1, 2, 3, 4, 5) + c := a.Sub(b) + assert.Equal(t, Slice{.0, .0, .0, .0, .0}, c) + assert.Equal(t, 5, len(c)) + assert.Equal(t, 5, c.Length()) +} + +func TestAdd(t *testing.T) { + a := New(1, 2, 3, 4, 5) + b := New(1, 2, 3, 4, 5) + c := a.Add(b) + assert.Equal(t, Slice{2.0, 4.0, 6.0, 8.0, 10.0}, c) + assert.Equal(t, 5, len(c)) + assert.Equal(t, 5, c.Length()) +} diff --git a/pkg/indicator/ewmastream_callbacks.go b/pkg/indicator/ewmastream_callbacks.go new file mode 100644 index 0000000000..79da29114f --- /dev/null +++ b/pkg/indicator/ewmastream_callbacks.go @@ -0,0 +1,5 @@ +// Code generated by "callbackgen -type EWMAStream"; DO NOT EDIT. + +package indicator + +import () diff --git a/pkg/indicator/float64updater_callbacks.go b/pkg/indicator/float64updater_callbacks.go new file mode 100644 index 0000000000..ff766f2d53 --- /dev/null +++ b/pkg/indicator/float64updater_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type Float64Updater"; DO NOT EDIT. + +package indicator + +import () + +func (F *Float64Updater) OnUpdate(cb func(v float64)) { + F.updateCallbacks = append(F.updateCallbacks, cb) +} + +func (F *Float64Updater) EmitUpdate(v float64) { + for _, cb := range F.updateCallbacks { + cb(v) + } +} diff --git a/pkg/indicator/klinestream_callbacks.go b/pkg/indicator/klinestream_callbacks.go new file mode 100644 index 0000000000..a3fa68d975 --- /dev/null +++ b/pkg/indicator/klinestream_callbacks.go @@ -0,0 +1,17 @@ +// Code generated by "callbackgen -type KLineStream"; DO NOT EDIT. + +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +func (K *KLineStream) OnUpdate(cb func(k types.KLine)) { + K.updateCallbacks = append(K.updateCallbacks, cb) +} + +func (K *KLineStream) EmitUpdate(k types.KLine) { + for _, cb := range K.updateCallbacks { + cb(k) + } +} diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index 93ed7c0cc8..cbbb8aae4b 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -27,6 +27,22 @@ type MACDConfig struct { LongPeriod int `json:"long"` } +/* +klines := kLines(marketDataStream) +closePrices := closePrices(klines) +macd := MACD(klines, {Fast: 12, Slow: 10}) + +equals to: + +klines := KLines(marketDataStream) +closePrices := ClosePrice(klines) +fastEMA := EMA(closePrices, 7) +slowEMA := EMA(closePrices, 25) +macd := Subtract(fastEMA, slowEMA) +signal := EMA(macd, 16) +histogram := Subtract(macd, signal) +*/ + //go:generate callbackgen -type MACD type MACD struct { MACDConfig diff --git a/pkg/indicator/macd2.go b/pkg/indicator/macd2.go new file mode 100644 index 0000000000..29de3e3c3e --- /dev/null +++ b/pkg/indicator/macd2.go @@ -0,0 +1,132 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type KLineStream +type KLineStream struct { + updateCallbacks []func(k types.KLine) +} + +func KLines(source types.Stream) *KLineStream { + stream := &KLineStream{} + source.OnKLineClosed(stream.EmitUpdate) + return stream +} + +type KLineSource interface { + OnUpdate(f func(k types.KLine)) +} + +type PriceStream struct { + types.SeriesBase + Float64Updater + + slice floats.Slice + mapper KLineValueMapper +} + +func Price(source KLineSource, mapper KLineValueMapper) *PriceStream { + s := &PriceStream{ + mapper: mapper, + } + s.SeriesBase.Series = s.slice + source.OnUpdate(func(k types.KLine) { + v := s.mapper(k) + s.slice.Push(v) + s.EmitUpdate(v) + }) + return s +} + +func ClosePrices(source KLineSource) *PriceStream { + return Price(source, KLineClosePriceMapper) +} + +func OpenPrices(source KLineSource) *PriceStream { + return Price(source, KLineOpenPriceMapper) +} + +type Float64Source interface { + types.Series + OnUpdate(f func(v float64)) +} + +//go:generate callbackgen -type EWMAStream +type EWMAStream struct { + Float64Updater + types.SeriesBase + + slice floats.Slice + + window int + multiplier float64 +} + +func EWMA2(source Float64Source, window int) *EWMAStream { + s := &EWMAStream{ + window: window, + multiplier: 2.0 / float64(1+window), + } + + s.SeriesBase.Series = s.slice + source.OnUpdate(func(v float64) { + v2 := s.calculate(v) + s.slice.Push(v2) + s.EmitUpdate(v2) + }) + return s +} + +func (s *EWMAStream) calculate(v float64) float64 { + last := s.slice.Last() + m := s.multiplier + return (1.0-m)*last + m*v +} + +//go:generate callbackgen -type Float64Updater +type Float64Updater struct { + updateCallbacks []func(v float64) +} + +type SubtractStream struct { + Float64Updater + types.SeriesBase + + a, b, c floats.Slice + i int +} + +func Subtract(a, b Float64Source) *SubtractStream { + s := &SubtractStream{} + s.SeriesBase.Series = s.c + + a.OnUpdate(func(v float64) { + s.a.Push(v) + s.calculate() + }) + b.OnUpdate(func(v float64) { + s.b.Push(v) + s.calculate() + }) + return s +} + +func (s *SubtractStream) calculate() { + if s.a.Length() != s.b.Length() { + return + } + + if s.a.Length() > s.c.Length() { + var numNewElems = s.a.Length() - s.c.Length() + var tailA = s.a.Tail(numNewElems) + var tailB = s.b.Tail(numNewElems) + var tailC = tailA.Sub(tailB) + for _, f := range tailC { + s.c.Push(f) + s.EmitUpdate(f) + } + } +} diff --git a/pkg/indicator/macd2_test.go b/pkg/indicator/macd2_test.go new file mode 100644 index 0000000000..b689cc1eb7 --- /dev/null +++ b/pkg/indicator/macd2_test.go @@ -0,0 +1,35 @@ +package indicator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func TestSubtract(t *testing.T) { + stream := &types.StandardStream{} + kLines := KLines(stream) + closePrices := ClosePrices(kLines) + fastEMA := EWMA2(closePrices, 10) + slowEMA := EWMA2(closePrices, 25) + subtract := Subtract(fastEMA, slowEMA) + _ = subtract + + for i := .0; i < 50.0; i++ { + stream.EmitKLineClosed(types.KLine{Close: fixedpoint.NewFromFloat(19_000.0 + i)}) + } + + t.Logf("fastEMA: %+v", fastEMA.slice) + t.Logf("slowEMA: %+v", slowEMA.slice) + + assert.Equal(t, len(subtract.a), len(subtract.b)) + assert.Equal(t, len(subtract.a), len(subtract.c)) + assert.Equal(t, subtract.c[0], subtract.a[0]-subtract.b[0]) + + t.Logf("subtract.a: %+v", subtract.a) + t.Logf("subtract.b: %+v", subtract.b) + t.Logf("subtract.c: %+v", subtract.c) +} diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index e3dd7690fe..0e1b30861a 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -115,10 +115,6 @@ type SeriesExtend interface { Filter(b func(i int, value float64) bool, length int) SeriesExtend } -type SeriesBase struct { - Series -} - func NewSeries(a Series) SeriesExtend { return &SeriesBase{ Series: a, @@ -618,7 +614,7 @@ func Dot(a interface{}, b interface{}, limit ...int) float64 { } } -// Extract elements from the Series to a float64 array, following the order of Index(0..limit) +// Array extracts elements from the Series to a float64 array, following the order of Index(0..limit) // if limit is given, will only take the first limit numbers (a.Index[0..limit]) // otherwise will operate on all elements func Array(a Series, limit ...int) (result []float64) { diff --git a/pkg/types/seriesbase_imp.go b/pkg/types/seriesbase_imp.go index 98329ad296..96525b69dd 100644 --- a/pkg/types/seriesbase_imp.go +++ b/pkg/types/seriesbase_imp.go @@ -2,6 +2,10 @@ package types import "github.com/c9s/bbgo/pkg/datatype/floats" +type SeriesBase struct { + Series +} + func (s *SeriesBase) Index(i int) float64 { if s.Series == nil { return 0 From a994235300ff46c96dbaed3ef4defc90b66aa0c2 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 25 May 2023 22:18:54 +0800 Subject: [PATCH 0895/1392] indicator: move doc --- pkg/indicator/macd.go | 16 ---------------- pkg/indicator/macd2.go | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index cbbb8aae4b..93ed7c0cc8 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -27,22 +27,6 @@ type MACDConfig struct { LongPeriod int `json:"long"` } -/* -klines := kLines(marketDataStream) -closePrices := closePrices(klines) -macd := MACD(klines, {Fast: 12, Slow: 10}) - -equals to: - -klines := KLines(marketDataStream) -closePrices := ClosePrice(klines) -fastEMA := EMA(closePrices, 7) -slowEMA := EMA(closePrices, 25) -macd := Subtract(fastEMA, slowEMA) -signal := EMA(macd, 16) -histogram := Subtract(macd, signal) -*/ - //go:generate callbackgen -type MACD type MACD struct { MACDConfig diff --git a/pkg/indicator/macd2.go b/pkg/indicator/macd2.go index 29de3e3c3e..1997d90ac6 100644 --- a/pkg/indicator/macd2.go +++ b/pkg/indicator/macd2.go @@ -5,6 +5,24 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +/* +NEW INDICATOR DESIGN: + +klines := kLines(marketDataStream) +closePrices := closePrices(klines) +macd := MACD(klines, {Fast: 12, Slow: 10}) + +equals to: + +klines := KLines(marketDataStream) +closePrices := ClosePrice(klines) +fastEMA := EMA(closePrices, 7) +slowEMA := EMA(closePrices, 25) +macd := Subtract(fastEMA, slowEMA) +signal := EMA(macd, 16) +histogram := Subtract(macd, signal) +*/ + //go:generate callbackgen -type KLineStream type KLineStream struct { updateCallbacks []func(k types.KLine) From 171e0678b68d86d244a31fe2e4d1a24f38e0b714 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 25 May 2023 22:19:14 +0800 Subject: [PATCH 0896/1392] indicator: remove underscore var --- pkg/indicator/macd2_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/indicator/macd2_test.go b/pkg/indicator/macd2_test.go index b689cc1eb7..dc4791dba5 100644 --- a/pkg/indicator/macd2_test.go +++ b/pkg/indicator/macd2_test.go @@ -16,7 +16,6 @@ func TestSubtract(t *testing.T) { fastEMA := EWMA2(closePrices, 10) slowEMA := EWMA2(closePrices, 25) subtract := Subtract(fastEMA, slowEMA) - _ = subtract for i := .0; i < 50.0; i++ { stream.EmitKLineClosed(types.KLine{Close: fixedpoint.NewFromFloat(19_000.0 + i)}) From 8c09c9668ae8e3071bb908ac1e3f166b4b8c7d30 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 26 May 2023 14:49:56 +0800 Subject: [PATCH 0897/1392] grid2: improve base quote investment check --- pkg/strategy/grid2/strategy.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index fc95d9e9e3..b1f7e9d7fe 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -850,12 +850,16 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv } } - quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) - if numberOfSellOrders > 0 { - return fixedpoint.Min(quoteSideQuantity, baseQuantity), nil + if totalQuotePrice.Sign() > 0 && quoteInvestment.Sign() > 0 { + quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) + if numberOfSellOrders > 0 { + return fixedpoint.Min(quoteSideQuantity, baseQuantity), nil + } + + return quoteSideQuantity, nil } - return quoteSideQuantity, nil + return baseQuantity, nil } func (s *Strategy) newTriggerPriceHandler(ctx context.Context, session *bbgo.ExchangeSession) types.KLineCallback { @@ -1079,7 +1083,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) } } else { // calculate the quantity from the investment configuration - if !s.QuoteInvestment.IsZero() && !s.BaseInvestment.IsZero() { + if !s.BaseInvestment.IsZero() { quantity, err2 := s.calculateBaseQuoteInvestmentQuantity(s.QuoteInvestment, s.BaseInvestment, lastPrice, s.grid.Pins) if err2 != nil { s.EmitGridError(err2) From 273659a87041de9808e4e3e0c989e3feabb724cf Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 26 May 2023 14:51:06 +0800 Subject: [PATCH 0898/1392] grid2: update comment --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b1f7e9d7fe..cb60ca6cb0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -766,7 +766,7 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInv // then reduce the numberOfSellOrders numberOfSellOrders := s.BaseGridNum - // if it's not configured + // if it's not configured, calculate the number of sell orders if numberOfSellOrders == 0 { for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] From 648e99f52a8d823db43946bbbcb50598cdd1550a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 26 May 2023 14:31:06 +0800 Subject: [PATCH 0899/1392] all: refactor and rename indicator.MACD to indicator.MACDLegacy --- pkg/bbgo/standard_indicator_set.go | 8 +-- pkg/indicator/float64updater.go | 6 +++ pkg/indicator/klinestream.go | 37 ++++++++++++++ pkg/indicator/klinestream_callbacks.go | 8 +-- pkg/indicator/macd2.go | 49 ------------------- pkg/indicator/macd2_test.go | 6 +-- pkg/indicator/macd_callbacks.go | 15 ------ pkg/indicator/{macd.go => macdlegacy.go} | 23 +++++---- pkg/indicator/macdlegacy_callbacks.go | 15 ++++++ .../{macd_test.go => macdlegacy_test.go} | 2 +- pkg/indicator/price.go | 47 ++++++++++++++++++ pkg/strategy/pivotshort/failedbreakhigh.go | 2 +- 12 files changed, 127 insertions(+), 91 deletions(-) create mode 100644 pkg/indicator/float64updater.go create mode 100644 pkg/indicator/klinestream.go delete mode 100644 pkg/indicator/macd_callbacks.go rename pkg/indicator/{macd.go => macdlegacy.go} (87%) create mode 100644 pkg/indicator/macdlegacy_callbacks.go rename pkg/indicator/{macd_test.go => macdlegacy_test.go} (92%) create mode 100644 pkg/indicator/price.go diff --git a/pkg/bbgo/standard_indicator_set.go b/pkg/bbgo/standard_indicator_set.go index 3f74591d0e..71befe8908 100644 --- a/pkg/bbgo/standard_indicator_set.go +++ b/pkg/bbgo/standard_indicator_set.go @@ -29,7 +29,7 @@ type StandardIndicatorSet struct { // interval -> window iwbIndicators map[types.IntervalWindowBandWidth]*indicator.BOLL iwIndicators map[indicatorKey]indicator.KLinePusher - macdIndicators map[indicator.MACDConfig]*indicator.MACD + macdIndicators map[indicator.MACDConfig]*indicator.MACDLegacy stream types.Stream store *MarketDataStore @@ -47,7 +47,7 @@ func NewStandardIndicatorSet(symbol string, stream types.Stream, store *MarketDa stream: stream, iwIndicators: make(map[indicatorKey]indicator.KLinePusher), iwbIndicators: make(map[types.IntervalWindowBandWidth]*indicator.BOLL), - macdIndicators: make(map[indicator.MACDConfig]*indicator.MACD), + macdIndicators: make(map[indicator.MACDConfig]*indicator.MACDLegacy), } } @@ -154,14 +154,14 @@ func (s *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) return inc } -func (s *StandardIndicatorSet) MACD(iw types.IntervalWindow, shortPeriod, longPeriod int) *indicator.MACD { +func (s *StandardIndicatorSet) MACD(iw types.IntervalWindow, shortPeriod, longPeriod int) *indicator.MACDLegacy { config := indicator.MACDConfig{IntervalWindow: iw, ShortPeriod: shortPeriod, LongPeriod: longPeriod} inc, ok := s.macdIndicators[config] if ok { return inc } - inc = &indicator.MACD{MACDConfig: config} + inc = &indicator.MACDLegacy{MACDConfig: config} s.macdIndicators[config] = inc s.initAndBind(inc, config.IntervalWindow.Interval) return inc diff --git a/pkg/indicator/float64updater.go b/pkg/indicator/float64updater.go new file mode 100644 index 0000000000..a9743538eb --- /dev/null +++ b/pkg/indicator/float64updater.go @@ -0,0 +1,6 @@ +package indicator + +//go:generate callbackgen -type Float64Updater +type Float64Updater struct { + updateCallbacks []func(v float64) +} diff --git a/pkg/indicator/klinestream.go b/pkg/indicator/klinestream.go new file mode 100644 index 0000000000..4fafafc080 --- /dev/null +++ b/pkg/indicator/klinestream.go @@ -0,0 +1,37 @@ +package indicator + +import "github.com/c9s/bbgo/pkg/types" + +const MaxNumOfKLines = 4_000 + +//go:generate callbackgen -type KLineStream +type KLineStream struct { + updateCallbacks []func(k types.KLine) + + kLines []types.KLine +} + +// AddSubscriber adds the subscriber function and push histrical data to the subscriber +func (s *KLineStream) AddSubscriber(f func(k types.KLine)) { + if len(s.kLines) > 0 { + // push historical klines to the subscriber + } + + s.OnUpdate(f) +} + +// KLines creates a KLine stream that pushes the klines to the subscribers +func KLines(source types.Stream) *KLineStream { + s := &KLineStream{} + + source.OnKLineClosed(func(k types.KLine) { + s.kLines = append(s.kLines, k) + + if len(s.kLines) > MaxNumOfKLines { + s.kLines = s.kLines[len(s.kLines)-1-MaxNumOfKLines:] + } + s.EmitUpdate(k) + }) + + return s +} diff --git a/pkg/indicator/klinestream_callbacks.go b/pkg/indicator/klinestream_callbacks.go index a3fa68d975..eb6e59dbbb 100644 --- a/pkg/indicator/klinestream_callbacks.go +++ b/pkg/indicator/klinestream_callbacks.go @@ -6,12 +6,12 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -func (K *KLineStream) OnUpdate(cb func(k types.KLine)) { - K.updateCallbacks = append(K.updateCallbacks, cb) +func (s *KLineStream) OnUpdate(cb func(k types.KLine)) { + s.updateCallbacks = append(s.updateCallbacks, cb) } -func (K *KLineStream) EmitUpdate(k types.KLine) { - for _, cb := range K.updateCallbacks { +func (s *KLineStream) EmitUpdate(k types.KLine) { + for _, cb := range s.updateCallbacks { cb(k) } } diff --git a/pkg/indicator/macd2.go b/pkg/indicator/macd2.go index 1997d90ac6..407c7d5e1f 100644 --- a/pkg/indicator/macd2.go +++ b/pkg/indicator/macd2.go @@ -23,50 +23,6 @@ signal := EMA(macd, 16) histogram := Subtract(macd, signal) */ -//go:generate callbackgen -type KLineStream -type KLineStream struct { - updateCallbacks []func(k types.KLine) -} - -func KLines(source types.Stream) *KLineStream { - stream := &KLineStream{} - source.OnKLineClosed(stream.EmitUpdate) - return stream -} - -type KLineSource interface { - OnUpdate(f func(k types.KLine)) -} - -type PriceStream struct { - types.SeriesBase - Float64Updater - - slice floats.Slice - mapper KLineValueMapper -} - -func Price(source KLineSource, mapper KLineValueMapper) *PriceStream { - s := &PriceStream{ - mapper: mapper, - } - s.SeriesBase.Series = s.slice - source.OnUpdate(func(k types.KLine) { - v := s.mapper(k) - s.slice.Push(v) - s.EmitUpdate(v) - }) - return s -} - -func ClosePrices(source KLineSource) *PriceStream { - return Price(source, KLineClosePriceMapper) -} - -func OpenPrices(source KLineSource) *PriceStream { - return Price(source, KLineOpenPriceMapper) -} - type Float64Source interface { types.Series OnUpdate(f func(v float64)) @@ -104,11 +60,6 @@ func (s *EWMAStream) calculate(v float64) float64 { return (1.0-m)*last + m*v } -//go:generate callbackgen -type Float64Updater -type Float64Updater struct { - updateCallbacks []func(v float64) -} - type SubtractStream struct { Float64Updater types.SeriesBase diff --git a/pkg/indicator/macd2_test.go b/pkg/indicator/macd2_test.go index dc4791dba5..5fe59773d6 100644 --- a/pkg/indicator/macd2_test.go +++ b/pkg/indicator/macd2_test.go @@ -26,9 +26,5 @@ func TestSubtract(t *testing.T) { assert.Equal(t, len(subtract.a), len(subtract.b)) assert.Equal(t, len(subtract.a), len(subtract.c)) - assert.Equal(t, subtract.c[0], subtract.a[0]-subtract.b[0]) - - t.Logf("subtract.a: %+v", subtract.a) - t.Logf("subtract.b: %+v", subtract.b) - t.Logf("subtract.c: %+v", subtract.c) + assert.InDelta(t, subtract.c[0], subtract.a[0]-subtract.b[0], 0.0001) } diff --git a/pkg/indicator/macd_callbacks.go b/pkg/indicator/macd_callbacks.go deleted file mode 100644 index 93a1bc8c99..0000000000 --- a/pkg/indicator/macd_callbacks.go +++ /dev/null @@ -1,15 +0,0 @@ -// Code generated by "callbackgen -type MACD"; DO NOT EDIT. - -package indicator - -import () - -func (inc *MACD) OnUpdate(cb func(macd float64, signal float64, histogram float64)) { - inc.updateCallbacks = append(inc.updateCallbacks, cb) -} - -func (inc *MACD) EmitUpdate(macd float64, signal float64, histogram float64) { - for _, cb := range inc.updateCallbacks { - cb(macd, signal, histogram) - } -} diff --git a/pkg/indicator/macd.go b/pkg/indicator/macdlegacy.go similarity index 87% rename from pkg/indicator/macd.go rename to pkg/indicator/macdlegacy.go index 93ed7c0cc8..19818c70e8 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macdlegacy.go @@ -27,20 +27,19 @@ type MACDConfig struct { LongPeriod int `json:"long"` } -//go:generate callbackgen -type MACD -type MACD struct { +//go:generate callbackgen -type MACDLegacy +type MACDLegacy struct { MACDConfig Values floats.Slice `json:"-"` fastEWMA, slowEWMA, signalLine *EWMA Histogram floats.Slice `json:"-"` - EndTime time.Time - updateCallbacks []func(macd, signal, histogram float64) + EndTime time.Time } -func (inc *MACD) Update(x float64) { +func (inc *MACDLegacy) Update(x float64) { if len(inc.Values) == 0 { // apply default values inc.fastEWMA = &EWMA{IntervalWindow: types.IntervalWindow{Window: inc.ShortPeriod}} @@ -76,7 +75,7 @@ func (inc *MACD) Update(x float64) { inc.EmitUpdate(macd, signal, histogram) } -func (inc *MACD) Last() float64 { +func (inc *MACDLegacy) Last() float64 { if len(inc.Values) == 0 { return 0.0 } @@ -84,27 +83,27 @@ func (inc *MACD) Last() float64 { return inc.Values[len(inc.Values)-1] } -func (inc *MACD) Length() int { +func (inc *MACDLegacy) Length() int { return len(inc.Values) } -func (inc *MACD) PushK(k types.KLine) { +func (inc *MACDLegacy) PushK(k types.KLine) { inc.Update(k.Close.Float64()) } -func (inc *MACD) MACD() types.SeriesExtend { - out := &MACDValues{MACD: inc} +func (inc *MACDLegacy) MACD() types.SeriesExtend { + out := &MACDValues{MACDLegacy: inc} out.SeriesBase.Series = out return out } -func (inc *MACD) Singals() types.SeriesExtend { +func (inc *MACDLegacy) Singals() types.SeriesExtend { return inc.signalLine } type MACDValues struct { types.SeriesBase - *MACD + *MACDLegacy } func (inc *MACDValues) Last() float64 { diff --git a/pkg/indicator/macdlegacy_callbacks.go b/pkg/indicator/macdlegacy_callbacks.go new file mode 100644 index 0000000000..ed4d108163 --- /dev/null +++ b/pkg/indicator/macdlegacy_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type MACDLegacy"; DO NOT EDIT. + +package indicator + +import () + +func (inc *MACDLegacy) OnUpdate(cb func(macd float64, signal float64, histogram float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *MACDLegacy) EmitUpdate(macd float64, signal float64, histogram float64) { + for _, cb := range inc.updateCallbacks { + cb(macd, signal, histogram) + } +} diff --git a/pkg/indicator/macd_test.go b/pkg/indicator/macdlegacy_test.go similarity index 92% rename from pkg/indicator/macd_test.go rename to pkg/indicator/macdlegacy_test.go index e3f5075bb7..c9bede48ad 100644 --- a/pkg/indicator/macd_test.go +++ b/pkg/indicator/macdlegacy_test.go @@ -40,7 +40,7 @@ func Test_calculateMACD(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { iw := types.IntervalWindow{Window: 9} - macd := MACD{MACDConfig: MACDConfig{IntervalWindow: iw, ShortPeriod: 12, LongPeriod: 26}} + macd := MACDLegacy{MACDConfig: MACDConfig{IntervalWindow: iw, ShortPeriod: 12, LongPeriod: 26}} for _, k := range tt.kLines { macd.PushK(k) } diff --git a/pkg/indicator/price.go b/pkg/indicator/price.go new file mode 100644 index 0000000000..dbd5853fea --- /dev/null +++ b/pkg/indicator/price.go @@ -0,0 +1,47 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +type KLineSource interface { + OnUpdate(f func(k types.KLine)) +} + +type PriceStream struct { + types.SeriesBase + Float64Updater + + slice floats.Slice + mapper KLineValueMapper +} + +func Price(source KLineSource, mapper KLineValueMapper) *PriceStream { + s := &PriceStream{ + mapper: mapper, + } + s.SeriesBase.Series = s.slice + source.OnUpdate(func(k types.KLine) { + v := s.mapper(k) + s.slice.Push(v) + s.EmitUpdate(v) + }) + return s +} + +func ClosePrices(source KLineSource) *PriceStream { + return Price(source, KLineClosePriceMapper) +} + +func LowPrices(source KLineSource) *PriceStream { + return Price(source, KLineLowPriceMapper) +} + +func HighPrices(source KLineSource) *PriceStream { + return Price(source, KLineHighPriceMapper) +} + +func OpenPrices(source KLineSource) *PriceStream { + return Price(source, KLineOpenPriceMapper) +} diff --git a/pkg/strategy/pivotshort/failedbreakhigh.go b/pkg/strategy/pivotshort/failedbreakhigh.go index eb193e994f..eb30dc91bf 100644 --- a/pkg/strategy/pivotshort/failedbreakhigh.go +++ b/pkg/strategy/pivotshort/failedbreakhigh.go @@ -47,7 +47,7 @@ type FailedBreakHigh struct { MACDDivergence *MACDDivergence `json:"macdDivergence"` - macd *indicator.MACD + macd *indicator.MACDLegacy macdTopDivergence bool From 9fac61351de435d0d25f37cf888b93af88ff1c5c Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 26 May 2023 15:04:58 +0800 Subject: [PATCH 0900/1392] all: rename Minus() to Sub() --- pkg/strategy/drift/draw.go | 5 +++-- pkg/types/indicator.go | 4 ++-- pkg/types/indicator_test.go | 12 ++++++------ pkg/types/seriesbase_imp.go | 5 ++++- pkg/types/trade_stats.go | 6 +++--- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/pkg/strategy/drift/draw.go b/pkg/strategy/drift/draw.go index 3ad7c377cb..e7ed1868dd 100644 --- a/pkg/strategy/drift/draw.go +++ b/pkg/strategy/drift/draw.go @@ -5,10 +5,11 @@ import ( "fmt" "os" + "github.com/wcharczuk/go-chart/v2" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/interact" "github.com/c9s/bbgo/pkg/types" - "github.com/wcharczuk/go-chart/v2" ) func (s *Strategy) InitDrawCommands(profit, cumProfit types.Series) { @@ -76,7 +77,7 @@ func (s *Strategy) DrawIndicators(time types.Time) *types.Canvas { // canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, length) canvas.Plot("ma", s.ma, time, length) - // canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, length) + // canvas.Plot("downband", s.ma.Sub(s.stdevLow), time, length) fmt.Printf("%f %f\n", highestPrice, hi) canvas.Plot("trend", s.trendLine, time, length) diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index 0e1b30861a..92c751a2ae 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -408,8 +408,8 @@ type MinusSeriesResult struct { b Series } -// Minus two series, result[i] = a[i] - b[i] -func Minus(a interface{}, b interface{}) SeriesExtend { +// Sub two series, result[i] = a[i] - b[i] +func Sub(a interface{}, b interface{}) SeriesExtend { aa := switchIface(a) bb := switchIface(b) return NewSeries(&MinusSeriesResult{aa, bb}) diff --git a/pkg/types/indicator_test.go b/pkg/types/indicator_test.go index 47d4886341..458d9dcf25 100644 --- a/pkg/types/indicator_test.go +++ b/pkg/types/indicator_test.go @@ -1,7 +1,7 @@ package types import ( - //"os" + // "os" "math" "testing" "time" @@ -22,7 +22,7 @@ func TestQueue(t *testing.T) { } func TestFloat(t *testing.T) { - var a Series = Minus(3., 2.) + var a Series = Sub(3., 2.) assert.Equal(t, a.Last(), 1.) assert.Equal(t, a.Index(100), 1.) } @@ -44,7 +44,7 @@ func TestNextCross(t *testing.T) { func TestFloat64Slice(t *testing.T) { var a = floats.Slice{1.0, 2.0, 3.0} var b = floats.Slice{1.0, 2.0, 3.0} - var c Series = Minus(&a, &b) + var c Series = Sub(&a, &b) a = append(a, 4.0) b = append(b, 3.0) assert.Equal(t, c.Last(), 1.) @@ -233,9 +233,9 @@ func TestPlot(t *testing.T) { ct.Plot("test", &a, Time(time.Now()), 4) assert.Equal(t, ct.Interval, Interval5m) assert.Equal(t, ct.Series[0].(chart.TimeSeries).Len(), 4) - //f, _ := os.Create("output.png") - //defer f.Close() - //ct.Render(chart.PNG, f) + // f, _ := os.Create("output.png") + // defer f.Close() + // ct.Render(chart.PNG, f) } func TestFilter(t *testing.T) { diff --git a/pkg/types/seriesbase_imp.go b/pkg/types/seriesbase_imp.go index 96525b69dd..feb95645af 100644 --- a/pkg/types/seriesbase_imp.go +++ b/pkg/types/seriesbase_imp.go @@ -2,6 +2,9 @@ package types import "github.com/c9s/bbgo/pkg/datatype/floats" +// SeriesBase is a wrapper of the Series interface +// You can assign a data container that implements the Series interface +// And this SeriesBase struct provides the implemented methods for manipulating your data type SeriesBase struct { Series } @@ -68,7 +71,7 @@ func (s *SeriesBase) Add(b interface{}) SeriesExtend { } func (s *SeriesBase) Minus(b interface{}) SeriesExtend { - return Minus(s, b) + return Sub(s, b) } func (s *SeriesBase) Div(b interface{}) SeriesExtend { diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go index 4bbb688fac..b20d3791c0 100644 --- a/pkg/types/trade_stats.go +++ b/pkg/types/trade_stats.go @@ -126,7 +126,7 @@ func (s *IntervalProfitCollector) GetSharpe() float64 { if s.Profits == nil { panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") } - return Sharpe(Minus(s.Profits, 1.), s.Profits.Length(), true, false) + return Sharpe(Sub(s.Profits, 1.), s.Profits.Length(), true, false) } // Get sortino value with the interval of profit collected. @@ -138,11 +138,11 @@ func (s *IntervalProfitCollector) GetSortino() float64 { if s.Profits == nil { panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") } - return Sortino(Minus(s.Profits, 1.), 0., s.Profits.Length(), true, false) + return Sortino(Sub(s.Profits, 1.), 0., s.Profits.Length(), true, false) } func (s *IntervalProfitCollector) GetOmega() float64 { - return Omega(Minus(s.Profits, 1.)) + return Omega(Sub(s.Profits, 1.)) } func (s IntervalProfitCollector) MarshalYAML() (interface{}, error) { From 5bf204b8900cc7d49f2e39dc664a21ef132e5635 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 26 May 2023 15:18:43 +0800 Subject: [PATCH 0901/1392] indicator: support histrical price push --- pkg/indicator/klinestream.go | 6 ++++-- pkg/indicator/macd2.go | 23 ++++++++++++++++++----- pkg/indicator/price.go | 18 ++++++++++-------- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/pkg/indicator/klinestream.go b/pkg/indicator/klinestream.go index 4fafafc080..13c43737e9 100644 --- a/pkg/indicator/klinestream.go +++ b/pkg/indicator/klinestream.go @@ -11,12 +11,14 @@ type KLineStream struct { kLines []types.KLine } -// AddSubscriber adds the subscriber function and push histrical data to the subscriber +// AddSubscriber adds the subscriber function and push historical data to the subscriber func (s *KLineStream) AddSubscriber(f func(k types.KLine)) { if len(s.kLines) > 0 { // push historical klines to the subscriber + for _, k := range s.kLines { + f(k) + } } - s.OnUpdate(f) } diff --git a/pkg/indicator/macd2.go b/pkg/indicator/macd2.go index 407c7d5e1f..c9d763aa22 100644 --- a/pkg/indicator/macd2.go +++ b/pkg/indicator/macd2.go @@ -28,6 +28,11 @@ type Float64Source interface { OnUpdate(f func(v float64)) } +type Float64Subscription interface { + types.Series + AddSubscriber(f func(v float64)) +} + //go:generate callbackgen -type EWMAStream type EWMAStream struct { Float64Updater @@ -46,14 +51,22 @@ func EWMA2(source Float64Source, window int) *EWMAStream { } s.SeriesBase.Series = s.slice - source.OnUpdate(func(v float64) { - v2 := s.calculate(v) - s.slice.Push(v2) - s.EmitUpdate(v2) - }) + + if sub, ok := source.(Float64Subscription); ok { + sub.AddSubscriber(s.calculateAndPush) + } else { + source.OnUpdate(s.calculateAndPush) + } + return s } +func (s *EWMAStream) calculateAndPush(v float64) { + v2 := s.calculate(v) + s.slice.Push(v2) + s.EmitUpdate(v2) +} + func (s *EWMAStream) calculate(v float64) float64 { last := s.slice.Last() m := s.multiplier diff --git a/pkg/indicator/price.go b/pkg/indicator/price.go index dbd5853fea..fd944121c4 100644 --- a/pkg/indicator/price.go +++ b/pkg/indicator/price.go @@ -5,8 +5,8 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -type KLineSource interface { - OnUpdate(f func(k types.KLine)) +type KLineSubscription interface { + AddSubscriber(f func(k types.KLine)) } type PriceStream struct { @@ -17,12 +17,14 @@ type PriceStream struct { mapper KLineValueMapper } -func Price(source KLineSource, mapper KLineValueMapper) *PriceStream { +func Price(source KLineSubscription, mapper KLineValueMapper) *PriceStream { s := &PriceStream{ mapper: mapper, } + s.SeriesBase.Series = s.slice - source.OnUpdate(func(k types.KLine) { + + source.AddSubscriber(func(k types.KLine) { v := s.mapper(k) s.slice.Push(v) s.EmitUpdate(v) @@ -30,18 +32,18 @@ func Price(source KLineSource, mapper KLineValueMapper) *PriceStream { return s } -func ClosePrices(source KLineSource) *PriceStream { +func ClosePrices(source KLineSubscription) *PriceStream { return Price(source, KLineClosePriceMapper) } -func LowPrices(source KLineSource) *PriceStream { +func LowPrices(source KLineSubscription) *PriceStream { return Price(source, KLineLowPriceMapper) } -func HighPrices(source KLineSource) *PriceStream { +func HighPrices(source KLineSubscription) *PriceStream { return Price(source, KLineHighPriceMapper) } -func OpenPrices(source KLineSource) *PriceStream { +func OpenPrices(source KLineSubscription) *PriceStream { return Price(source, KLineOpenPriceMapper) } From 5ef7da84226941bfea8b3be145c9405bcdea6473 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 26 May 2023 16:09:07 +0800 Subject: [PATCH 0902/1392] grid2: fix precheck --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index cb60ca6cb0..b8a614e17c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -236,7 +236,7 @@ func (s *Strategy) Validate() error { } } - if !s.QuantityOrAmount.IsSet() && s.QuoteInvestment.IsZero() { + if !s.QuantityOrAmount.IsSet() && s.QuoteInvestment.IsZero() && s.BaseInvestment.IsZero() { return fmt.Errorf("either quantity, amount or quoteInvestment must be set") } From 89aa63dd643e0b2abbbfe4d22d7cba8f8169cf4d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 29 May 2023 17:12:34 +0800 Subject: [PATCH 0903/1392] floats: add floats LSM --- pkg/datatype/floats/funcs_test.go | 6 ++++++ pkg/datatype/floats/pivot.go | 4 ++-- pkg/datatype/floats/slice.go | 27 +++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pkg/datatype/floats/funcs_test.go b/pkg/datatype/floats/funcs_test.go index 25c37f908a..52401fbd46 100644 --- a/pkg/datatype/floats/funcs_test.go +++ b/pkg/datatype/floats/funcs_test.go @@ -15,3 +15,9 @@ func TestHigher(t *testing.T) { out := Higher([]float64{10.0, 11.0, 12.0, 13.0, 15.0}, 12.0) assert.Equal(t, []float64{13.0, 15.0}, out) } + +func TestLSM(t *testing.T) { + slice := Slice{1., 2., 3., 4.} + slope := LSM(slice) + assert.Equal(t, 1.0, slope) +} diff --git a/pkg/datatype/floats/pivot.go b/pkg/datatype/floats/pivot.go index f794030477..b7536cbe3e 100644 --- a/pkg/datatype/floats/pivot.go +++ b/pkg/datatype/floats/pivot.go @@ -1,10 +1,10 @@ package floats func (s Slice) Pivot(left, right int, f func(a, pivot float64) bool) (float64, bool) { - return CalculatePivot(s, left, right, f) + return FindPivot(s, left, right, f) } -func CalculatePivot(values Slice, left, right int, f func(a, pivot float64) bool) (float64, bool) { +func FindPivot(values Slice, left, right int, f func(a, pivot float64) bool) (float64, bool) { length := len(values) if right == 0 { diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index 40b3a427bb..f9efa2d6c9 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -187,3 +187,30 @@ func (s Slice) Index(i int) float64 { func (s Slice) Length() int { return len(s) } + +func (s Slice) LSM() float64 { + return LSM(s) +} + +func LSM(values Slice) float64 { + var sumX, sumY, sumXSqr, sumXY = .0, .0, .0, .0 + + end := len(values) - 1 + for i := end; i >= 0; i-- { + val := values[i] + per := float64(end - i + 1) + sumX += per + sumY += val + sumXSqr += per * per + sumXY += val * per + } + + length := float64(len(values)) + slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX) + + average := sumY / length + tail := average - slope*sumX/length + slope + head := tail + slope*(length-1) + slope2 := (tail - head) / (length - 1) + return slope2 +} From e91142f4e96db0d0c622166c882fdcc7a3cc27bd Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 13:14:36 +0800 Subject: [PATCH 0904/1392] indicator: rename func to floats.FindPivot --- pkg/indicator/pivotlow.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/indicator/pivotlow.go b/pkg/indicator/pivotlow.go index 135b61868a..c195c488c8 100644 --- a/pkg/indicator/pivotlow.go +++ b/pkg/indicator/pivotlow.go @@ -64,13 +64,13 @@ func (inc *PivotLow) PushK(k types.KLine) { } func calculatePivotHigh(highs floats.Slice, left, right int) (float64, bool) { - return floats.CalculatePivot(highs, left, right, func(a, pivot float64) bool { + return floats.FindPivot(highs, left, right, func(a, pivot float64) bool { return a < pivot }) } func calculatePivotLow(lows floats.Slice, left, right int) (float64, bool) { - return floats.CalculatePivot(lows, left, right, func(a, pivot float64) bool { + return floats.FindPivot(lows, left, right, func(a, pivot float64) bool { return a > pivot }) } From f067c9273395fe685cd068885e4040f7d7f82e8c Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 29 May 2023 17:55:57 +0800 Subject: [PATCH 0905/1392] floats: document LSM --- pkg/datatype/floats/slice.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index f9efa2d6c9..727cfe9bb6 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -192,6 +192,7 @@ func (s Slice) LSM() float64 { return LSM(s) } +// LSM is the least squares method for linear regression func LSM(values Slice) float64 { var sumX, sumY, sumXSqr, sumXY = .0, .0, .0, .0 From 8c7962f07fa613f83db90542ed6342e6374fca38 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 29 May 2023 21:37:31 +0800 Subject: [PATCH 0906/1392] indicator: move out subtract stream --- pkg/indicator/macd2.go | 40 -------------------------------- pkg/indicator/subtract.go | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 40 deletions(-) create mode 100644 pkg/indicator/subtract.go diff --git a/pkg/indicator/macd2.go b/pkg/indicator/macd2.go index c9d763aa22..8b4c515332 100644 --- a/pkg/indicator/macd2.go +++ b/pkg/indicator/macd2.go @@ -72,43 +72,3 @@ func (s *EWMAStream) calculate(v float64) float64 { m := s.multiplier return (1.0-m)*last + m*v } - -type SubtractStream struct { - Float64Updater - types.SeriesBase - - a, b, c floats.Slice - i int -} - -func Subtract(a, b Float64Source) *SubtractStream { - s := &SubtractStream{} - s.SeriesBase.Series = s.c - - a.OnUpdate(func(v float64) { - s.a.Push(v) - s.calculate() - }) - b.OnUpdate(func(v float64) { - s.b.Push(v) - s.calculate() - }) - return s -} - -func (s *SubtractStream) calculate() { - if s.a.Length() != s.b.Length() { - return - } - - if s.a.Length() > s.c.Length() { - var numNewElems = s.a.Length() - s.c.Length() - var tailA = s.a.Tail(numNewElems) - var tailB = s.b.Tail(numNewElems) - var tailC = tailA.Sub(tailB) - for _, f := range tailC { - s.c.Push(f) - s.EmitUpdate(f) - } - } -} diff --git a/pkg/indicator/subtract.go b/pkg/indicator/subtract.go new file mode 100644 index 0000000000..fa03d85ebc --- /dev/null +++ b/pkg/indicator/subtract.go @@ -0,0 +1,49 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// SubtractStream subscribes 2 upstream data, and then subtract these 2 values +type SubtractStream struct { + Float64Updater + types.SeriesBase + + a, b, c floats.Slice + i int +} + +// Subtract creates the SubtractStream object +// subtract := Subtract(longEWMA, shortEWMA) +func Subtract(a, b Float64Source) *SubtractStream { + s := &SubtractStream{} + s.SeriesBase.Series = s.c + + a.OnUpdate(func(v float64) { + s.a.Push(v) + s.calculate() + }) + b.OnUpdate(func(v float64) { + s.b.Push(v) + s.calculate() + }) + return s +} + +func (s *SubtractStream) calculate() { + if s.a.Length() != s.b.Length() { + return + } + + if s.a.Length() > s.c.Length() { + var numNewElems = s.a.Length() - s.c.Length() + var tailA = s.a.Tail(numNewElems) + var tailB = s.b.Tail(numNewElems) + var tailC = tailA.Sub(tailB) + for _, f := range tailC { + s.c.Push(f) + s.EmitUpdate(f) + } + } +} From 68570e1eeb80694402ef53f5044f869ad55de42a Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 29 May 2023 21:42:22 +0800 Subject: [PATCH 0907/1392] indicator: move EWMA2 to ewma2.go --- pkg/indicator/ewma2.go | 46 ++++++++++++++++++++++++++++++++++++++++++ pkg/indicator/macd2.go | 41 ------------------------------------- 2 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 pkg/indicator/ewma2.go diff --git a/pkg/indicator/ewma2.go b/pkg/indicator/ewma2.go new file mode 100644 index 0000000000..ba5bea29b6 --- /dev/null +++ b/pkg/indicator/ewma2.go @@ -0,0 +1,46 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type EWMAStream +type EWMAStream struct { + Float64Updater + types.SeriesBase + + slice floats.Slice + + window int + multiplier float64 +} + +func EWMA2(source Float64Source, window int) *EWMAStream { + s := &EWMAStream{ + window: window, + multiplier: 2.0 / float64(1+window), + } + + s.SeriesBase.Series = s.slice + + if sub, ok := source.(Float64Subscription); ok { + sub.AddSubscriber(s.calculateAndPush) + } else { + source.OnUpdate(s.calculateAndPush) + } + + return s +} + +func (s *EWMAStream) calculateAndPush(v float64) { + v2 := s.calculate(v) + s.slice.Push(v2) + s.EmitUpdate(v2) +} + +func (s *EWMAStream) calculate(v float64) float64 { + last := s.slice.Last() + m := s.multiplier + return (1.0-m)*last + m*v +} diff --git a/pkg/indicator/macd2.go b/pkg/indicator/macd2.go index 8b4c515332..b1ef53bd90 100644 --- a/pkg/indicator/macd2.go +++ b/pkg/indicator/macd2.go @@ -1,7 +1,6 @@ package indicator import ( - "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -32,43 +31,3 @@ type Float64Subscription interface { types.Series AddSubscriber(f func(v float64)) } - -//go:generate callbackgen -type EWMAStream -type EWMAStream struct { - Float64Updater - types.SeriesBase - - slice floats.Slice - - window int - multiplier float64 -} - -func EWMA2(source Float64Source, window int) *EWMAStream { - s := &EWMAStream{ - window: window, - multiplier: 2.0 / float64(1+window), - } - - s.SeriesBase.Series = s.slice - - if sub, ok := source.(Float64Subscription); ok { - sub.AddSubscriber(s.calculateAndPush) - } else { - source.OnUpdate(s.calculateAndPush) - } - - return s -} - -func (s *EWMAStream) calculateAndPush(v float64) { - v2 := s.calculate(v) - s.slice.Push(v2) - s.EmitUpdate(v2) -} - -func (s *EWMAStream) calculate(v float64) float64 { - last := s.slice.Last() - m := s.multiplier - return (1.0-m)*last + m*v -} From e094f422fcdff13fd331b67d7ee8d5e05a7ec6eb Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 29 May 2023 21:44:23 +0800 Subject: [PATCH 0908/1392] indicator: rename v2 indicator file --- pkg/indicator/{macd2.go => v2.go} | 0 pkg/indicator/{macd2_test.go => v2_test.go} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename pkg/indicator/{macd2.go => v2.go} (100%) rename pkg/indicator/{macd2_test.go => v2_test.go} (95%) diff --git a/pkg/indicator/macd2.go b/pkg/indicator/v2.go similarity index 100% rename from pkg/indicator/macd2.go rename to pkg/indicator/v2.go diff --git a/pkg/indicator/macd2_test.go b/pkg/indicator/v2_test.go similarity index 95% rename from pkg/indicator/macd2_test.go rename to pkg/indicator/v2_test.go index 5fe59773d6..bbaeb24d3f 100644 --- a/pkg/indicator/macd2_test.go +++ b/pkg/indicator/v2_test.go @@ -9,7 +9,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -func TestSubtract(t *testing.T) { +func Test_v2_Subtract(t *testing.T) { stream := &types.StandardStream{} kLines := KLines(stream) closePrices := ClosePrices(kLines) From 1450d193a47a6264a47afc5e653c8e5cf8e1db10 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 11:35:24 +0800 Subject: [PATCH 0909/1392] indicator: refactor/add float64 series --- pkg/indicator/atr2.go | 37 ++++++++++++ pkg/indicator/ewma2.go | 11 +--- pkg/indicator/ewmastream_callbacks.go | 5 -- pkg/indicator/float64updater.go | 28 ++++++++++ pkg/indicator/float64updater_callbacks.go | 8 +-- pkg/indicator/price.go | 5 +- pkg/indicator/rma2.go | 68 +++++++++++++++++++++++ pkg/indicator/subtract.go | 12 ++-- pkg/indicator/v2_test.go | 4 +- 9 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 pkg/indicator/atr2.go delete mode 100644 pkg/indicator/ewmastream_callbacks.go create mode 100644 pkg/indicator/rma2.go diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go new file mode 100644 index 0000000000..ac0f4ba3dd --- /dev/null +++ b/pkg/indicator/atr2.go @@ -0,0 +1,37 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +type ATRStream struct { + Float64Updater + + types.SeriesBase + + window int + multiplier float64 +} + +func ATR2(source KLineSubscription, window int) *ATRStream { + s := &ATRStream{ + window: window, + multiplier: 2.0 / float64(1+window), + } + + s.SeriesBase.Series = s.slice + + source.AddSubscriber(func(k types.KLine) { + // v := s.mapper(k) + // s.slice.Push(v) + // s.EmitUpdate(v) + }) + + return s +} + +func (s *ATRStream) calculateAndPush(k types.KLine) { + // v2 := s.calculate(v) + // s.slice.Push(v2) + // s.EmitUpdate(v2) +} diff --git a/pkg/indicator/ewma2.go b/pkg/indicator/ewma2.go index ba5bea29b6..9041238c95 100644 --- a/pkg/indicator/ewma2.go +++ b/pkg/indicator/ewma2.go @@ -1,16 +1,7 @@ package indicator -import ( - "github.com/c9s/bbgo/pkg/datatype/floats" - "github.com/c9s/bbgo/pkg/types" -) - -//go:generate callbackgen -type EWMAStream type EWMAStream struct { - Float64Updater - types.SeriesBase - - slice floats.Slice + Float64Series window int multiplier float64 diff --git a/pkg/indicator/ewmastream_callbacks.go b/pkg/indicator/ewmastream_callbacks.go deleted file mode 100644 index 79da29114f..0000000000 --- a/pkg/indicator/ewmastream_callbacks.go +++ /dev/null @@ -1,5 +0,0 @@ -// Code generated by "callbackgen -type EWMAStream"; DO NOT EDIT. - -package indicator - -import () diff --git a/pkg/indicator/float64updater.go b/pkg/indicator/float64updater.go index a9743538eb..f55fa9c2b0 100644 --- a/pkg/indicator/float64updater.go +++ b/pkg/indicator/float64updater.go @@ -1,6 +1,34 @@ package indicator +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + //go:generate callbackgen -type Float64Updater type Float64Updater struct { updateCallbacks []func(v float64) + + slice floats.Slice +} + +type Float64Series struct { + types.SeriesBase + Float64Updater +} + +func (f *Float64Series) Last() float64 { + return f.slice.Last() +} + +func (f *Float64Series) Index(i int) float64 { + length := len(f.slice) + if length == 0 || length-i-1 < 0 { + return 0 + } + return f.slice[length-i-1] +} + +func (f *Float64Series) Length() int { + return len(f.slice) } diff --git a/pkg/indicator/float64updater_callbacks.go b/pkg/indicator/float64updater_callbacks.go index ff766f2d53..3226608630 100644 --- a/pkg/indicator/float64updater_callbacks.go +++ b/pkg/indicator/float64updater_callbacks.go @@ -4,12 +4,12 @@ package indicator import () -func (F *Float64Updater) OnUpdate(cb func(v float64)) { - F.updateCallbacks = append(F.updateCallbacks, cb) +func (f *Float64Updater) OnUpdate(cb func(v float64)) { + f.updateCallbacks = append(f.updateCallbacks, cb) } -func (F *Float64Updater) EmitUpdate(v float64) { - for _, cb := range F.updateCallbacks { +func (f *Float64Updater) EmitUpdate(v float64) { + for _, cb := range f.updateCallbacks { cb(v) } } diff --git a/pkg/indicator/price.go b/pkg/indicator/price.go index fd944121c4..a3c7c45a14 100644 --- a/pkg/indicator/price.go +++ b/pkg/indicator/price.go @@ -1,7 +1,6 @@ package indicator import ( - "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -10,10 +9,8 @@ type KLineSubscription interface { } type PriceStream struct { - types.SeriesBase - Float64Updater + Float64Series - slice floats.Slice mapper KLineValueMapper } diff --git a/pkg/indicator/rma2.go b/pkg/indicator/rma2.go new file mode 100644 index 0000000000..b751b87a77 --- /dev/null +++ b/pkg/indicator/rma2.go @@ -0,0 +1,68 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +type RMAStream struct { + // embedded structs + Float64Updater + types.SeriesBase + + // config fields + types.IntervalWindow + Adjust bool + + counter int + sum, tmp float64 +} + +func RMA2(source Float64Source, iw types.IntervalWindow) *RMAStream { + s := &RMAStream{ + IntervalWindow: iw, + } + + s.SeriesBase.Series = s.slice + + if sub, ok := source.(Float64Subscription); ok { + sub.AddSubscriber(s.calculateAndPush) + } else { + source.OnUpdate(s.calculateAndPush) + } + + return s +} + +func (s *RMAStream) calculateAndPush(v float64) { + v2 := s.calculate(v) + s.slice.Push(v2) + s.EmitUpdate(v2) +} + +func (s *RMAStream) calculate(x float64) float64 { + lambda := 1 / float64(s.Window) + if s.counter == 0 { + s.sum = 1 + s.tmp = x + } else { + if s.Adjust { + s.sum = s.sum*(1-lambda) + 1 + s.tmp = s.tmp + (x-s.tmp)/s.sum + } else { + s.tmp = s.tmp*(1-lambda) + x*lambda + } + } + s.counter++ + + if s.counter < s.Window { + s.slice.Push(0) + } + + s.slice.Push(s.tmp) + + if len(s.slice) > MaxNumOfRMA { + s.slice = s.slice[MaxNumOfRMATruncateSize-1:] + } + + return s.tmp +} diff --git a/pkg/indicator/subtract.go b/pkg/indicator/subtract.go index fa03d85ebc..287e34ca8a 100644 --- a/pkg/indicator/subtract.go +++ b/pkg/indicator/subtract.go @@ -10,15 +10,15 @@ type SubtractStream struct { Float64Updater types.SeriesBase - a, b, c floats.Slice - i int + a, b floats.Slice + i int } // Subtract creates the SubtractStream object // subtract := Subtract(longEWMA, shortEWMA) func Subtract(a, b Float64Source) *SubtractStream { s := &SubtractStream{} - s.SeriesBase.Series = s.c + s.SeriesBase.Series = s.slice a.OnUpdate(func(v float64) { s.a.Push(v) @@ -36,13 +36,13 @@ func (s *SubtractStream) calculate() { return } - if s.a.Length() > s.c.Length() { - var numNewElems = s.a.Length() - s.c.Length() + if s.a.Length() > s.slice.Length() { + var numNewElems = s.a.Length() - s.slice.Length() var tailA = s.a.Tail(numNewElems) var tailB = s.b.Tail(numNewElems) var tailC = tailA.Sub(tailB) for _, f := range tailC { - s.c.Push(f) + s.slice.Push(f) s.EmitUpdate(f) } } diff --git a/pkg/indicator/v2_test.go b/pkg/indicator/v2_test.go index bbaeb24d3f..a96812c1fc 100644 --- a/pkg/indicator/v2_test.go +++ b/pkg/indicator/v2_test.go @@ -25,6 +25,6 @@ func Test_v2_Subtract(t *testing.T) { t.Logf("slowEMA: %+v", slowEMA.slice) assert.Equal(t, len(subtract.a), len(subtract.b)) - assert.Equal(t, len(subtract.a), len(subtract.c)) - assert.InDelta(t, subtract.c[0], subtract.a[0]-subtract.b[0], 0.0001) + assert.Equal(t, len(subtract.a), len(subtract.slice)) + assert.InDelta(t, subtract.slice[0], subtract.a[0]-subtract.b[0], 0.0001) } From 1bf44720e2b5045209bf1b50d9d331158b662fb5 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 11:35:59 +0800 Subject: [PATCH 0910/1392] indicator: update and clean up rma2 --- pkg/indicator/rma2.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/indicator/rma2.go b/pkg/indicator/rma2.go index b751b87a77..66a92e99db 100644 --- a/pkg/indicator/rma2.go +++ b/pkg/indicator/rma2.go @@ -6,8 +6,7 @@ import ( type RMAStream struct { // embedded structs - Float64Updater - types.SeriesBase + Float64Series // config fields types.IntervalWindow From da15f47f17596430e9e8b9b6a355f300d8d249bc Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 12:13:55 +0800 Subject: [PATCH 0911/1392] indicator: refactor Float64Series and improve RMA2 --- pkg/indicator/atr2.go | 9 ++++----- pkg/indicator/ewma2.go | 7 +++---- pkg/indicator/float64updater.go | 10 ++++++++-- pkg/indicator/rma2.go | 25 +++++++++++++++---------- pkg/indicator/subtract.go | 9 ++++----- 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index ac0f4ba3dd..a881562d22 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -5,7 +5,7 @@ import ( ) type ATRStream struct { - Float64Updater + Float64Series types.SeriesBase @@ -15,12 +15,11 @@ type ATRStream struct { func ATR2(source KLineSubscription, window int) *ATRStream { s := &ATRStream{ - window: window, - multiplier: 2.0 / float64(1+window), + Float64Series: NewFloat64Series(), + window: window, + multiplier: 2.0 / float64(1+window), } - s.SeriesBase.Series = s.slice - source.AddSubscriber(func(k types.KLine) { // v := s.mapper(k) // s.slice.Push(v) diff --git a/pkg/indicator/ewma2.go b/pkg/indicator/ewma2.go index 9041238c95..e452aaedae 100644 --- a/pkg/indicator/ewma2.go +++ b/pkg/indicator/ewma2.go @@ -9,12 +9,11 @@ type EWMAStream struct { func EWMA2(source Float64Source, window int) *EWMAStream { s := &EWMAStream{ - window: window, - multiplier: 2.0 / float64(1+window), + Float64Series: NewFloat64Series(), + window: window, + multiplier: 2.0 / float64(1+window), } - s.SeriesBase.Series = s.slice - if sub, ok := source.(Float64Subscription); ok { sub.AddSubscriber(s.calculateAndPush) } else { diff --git a/pkg/indicator/float64updater.go b/pkg/indicator/float64updater.go index f55fa9c2b0..3c3746ffef 100644 --- a/pkg/indicator/float64updater.go +++ b/pkg/indicator/float64updater.go @@ -8,13 +8,19 @@ import ( //go:generate callbackgen -type Float64Updater type Float64Updater struct { updateCallbacks []func(v float64) - - slice floats.Slice } type Float64Series struct { types.SeriesBase Float64Updater + slice floats.Slice +} + +func NewFloat64Series(v ...float64) Float64Series { + s := Float64Series{} + s.slice = v + s.SeriesBase.Series = s.slice + return s } func (f *Float64Series) Last() float64 { diff --git a/pkg/indicator/rma2.go b/pkg/indicator/rma2.go index 66a92e99db..4e48c4bda0 100644 --- a/pkg/indicator/rma2.go +++ b/pkg/indicator/rma2.go @@ -12,17 +12,16 @@ type RMAStream struct { types.IntervalWindow Adjust bool - counter int - sum, tmp float64 + counter int + sum, previous float64 } func RMA2(source Float64Source, iw types.IntervalWindow) *RMAStream { s := &RMAStream{ + Float64Series: NewFloat64Series(), IntervalWindow: iw, } - s.SeriesBase.Series = s.slice - if sub, ok := source.(Float64Subscription); ok { sub.AddSubscriber(s.calculateAndPush) } else { @@ -36,32 +35,38 @@ func (s *RMAStream) calculateAndPush(v float64) { v2 := s.calculate(v) s.slice.Push(v2) s.EmitUpdate(v2) + s.truncate() } func (s *RMAStream) calculate(x float64) float64 { lambda := 1 / float64(s.Window) + tmp := 0.0 if s.counter == 0 { s.sum = 1 - s.tmp = x + tmp = x } else { if s.Adjust { s.sum = s.sum*(1-lambda) + 1 - s.tmp = s.tmp + (x-s.tmp)/s.sum + tmp = s.previous + (x-s.previous)/s.sum } else { - s.tmp = s.tmp*(1-lambda) + x*lambda + tmp = s.previous*(1-lambda) + x*lambda } } s.counter++ if s.counter < s.Window { + // we can use x, but we need to use 0. to make the same behavior as the result from python pandas_ta s.slice.Push(0) } - s.slice.Push(s.tmp) + s.slice.Push(tmp) + s.previous = tmp + + return tmp +} +func (s *RMAStream) truncate() { if len(s.slice) > MaxNumOfRMA { s.slice = s.slice[MaxNumOfRMATruncateSize-1:] } - - return s.tmp } diff --git a/pkg/indicator/subtract.go b/pkg/indicator/subtract.go index 287e34ca8a..7ccde2bf67 100644 --- a/pkg/indicator/subtract.go +++ b/pkg/indicator/subtract.go @@ -2,13 +2,11 @@ package indicator import ( "github.com/c9s/bbgo/pkg/datatype/floats" - "github.com/c9s/bbgo/pkg/types" ) // SubtractStream subscribes 2 upstream data, and then subtract these 2 values type SubtractStream struct { - Float64Updater - types.SeriesBase + Float64Series a, b floats.Slice i int @@ -17,8 +15,9 @@ type SubtractStream struct { // Subtract creates the SubtractStream object // subtract := Subtract(longEWMA, shortEWMA) func Subtract(a, b Float64Source) *SubtractStream { - s := &SubtractStream{} - s.SeriesBase.Series = s.slice + s := &SubtractStream{ + Float64Series: NewFloat64Series(), + } a.OnUpdate(func(v float64) { s.a.Push(v) From f65d6267fca0b4b19ef52bb070e158f377e65af0 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 12:29:50 +0800 Subject: [PATCH 0912/1392] indicator: refactor ATRStream --- pkg/indicator/atr2.go | 43 +++++++++++++++++++++++++++++++------------ pkg/indicator/rma2.go | 17 +++++++---------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index a881562d22..af8fd9959b 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -1,36 +1,55 @@ package indicator import ( + "math" + "github.com/c9s/bbgo/pkg/types" ) type ATRStream struct { + // embedded struct Float64Series - types.SeriesBase + // parameters + types.IntervalWindow + + // private states + rma *RMAStream - window int - multiplier float64 + window int + previousClose float64 } func ATR2(source KLineSubscription, window int) *ATRStream { s := &ATRStream{ Float64Series: NewFloat64Series(), window: window, - multiplier: 2.0 / float64(1+window), } + s.rma = RMA2(s, window, true) source.AddSubscriber(func(k types.KLine) { - // v := s.mapper(k) - // s.slice.Push(v) - // s.EmitUpdate(v) + s.calculateAndPush(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) }) - return s } -func (s *ATRStream) calculateAndPush(k types.KLine) { - // v2 := s.calculate(v) - // s.slice.Push(v2) - // s.EmitUpdate(v2) +func (s *ATRStream) calculateAndPush(high, low, cls float64) { + if s.previousClose == .0 { + s.previousClose = cls + return + } + + trueRange := high - low + hc := math.Abs(high - s.previousClose) + lc := math.Abs(low - s.previousClose) + if trueRange < hc { + trueRange = hc + } + if trueRange < lc { + trueRange = lc + } + + s.previousClose = cls + s.slice.Push(trueRange) + s.rma.EmitUpdate(trueRange) } diff --git a/pkg/indicator/rma2.go b/pkg/indicator/rma2.go index 4e48c4bda0..f268aec527 100644 --- a/pkg/indicator/rma2.go +++ b/pkg/indicator/rma2.go @@ -1,25 +1,22 @@ package indicator -import ( - "github.com/c9s/bbgo/pkg/types" -) - type RMAStream struct { // embedded structs Float64Series // config fields - types.IntervalWindow Adjust bool + window int counter int sum, previous float64 } -func RMA2(source Float64Source, iw types.IntervalWindow) *RMAStream { +func RMA2(source Float64Source, window int, adjust bool) *RMAStream { s := &RMAStream{ - Float64Series: NewFloat64Series(), - IntervalWindow: iw, + Float64Series: NewFloat64Series(), + window: window, + Adjust: adjust, } if sub, ok := source.(Float64Subscription); ok { @@ -39,7 +36,7 @@ func (s *RMAStream) calculateAndPush(v float64) { } func (s *RMAStream) calculate(x float64) float64 { - lambda := 1 / float64(s.Window) + lambda := 1 / float64(s.window) tmp := 0.0 if s.counter == 0 { s.sum = 1 @@ -54,7 +51,7 @@ func (s *RMAStream) calculate(x float64) float64 { } s.counter++ - if s.counter < s.Window { + if s.counter < s.window { // we can use x, but we need to use 0. to make the same behavior as the result from python pandas_ta s.slice.Push(0) } From 811e624302e46b33fc279f32fbfcdbcce7b38420 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 12:51:44 +0800 Subject: [PATCH 0913/1392] indicator: simplify and refactor atr2 --- pkg/indicator/atr2.go | 15 ++----- pkg/indicator/atr2_test.go | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 pkg/indicator/atr2_test.go diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index af8fd9959b..65a214bc20 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -6,26 +6,20 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +// ATRStream is a RMAStream +// This ATRStream calcualtes the ATR first, and then push it to the RMAStream type ATRStream struct { // embedded struct Float64Series - // parameters - types.IntervalWindow - // private states - rma *RMAStream - - window int previousClose float64 } -func ATR2(source KLineSubscription, window int) *ATRStream { +func ATR2(source KLineSubscription) *ATRStream { s := &ATRStream{ Float64Series: NewFloat64Series(), - window: window, } - s.rma = RMA2(s, window, true) source.AddSubscriber(func(k types.KLine) { s.calculateAndPush(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) @@ -50,6 +44,5 @@ func (s *ATRStream) calculateAndPush(high, low, cls float64) { } s.previousClose = cls - s.slice.Push(trueRange) - s.rma.EmitUpdate(trueRange) + s.EmitUpdate(trueRange) } diff --git a/pkg/indicator/atr2_test.go b/pkg/indicator/atr2_test.go new file mode 100644 index 0000000000..6355b476df --- /dev/null +++ b/pkg/indicator/atr2_test.go @@ -0,0 +1,82 @@ +package indicator + +import ( + "encoding/json" + "math" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +/* +python + +import pandas as pd +import pandas_ta as ta + + data = { + "high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0], + "low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84], + "close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, + +39693.79, 39827.96, 40074.94, 40059.84] +} + +high = pd.Series(data['high']) +low = pd.Series(data['low']) +close = pd.Series(data['close']) +result = ta.atr(high, low, close, length=14) +print(result) +*/ +func Test_ATR2(t *testing.T) { + var bytes = []byte(`{ + "high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0], + "low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84], + "close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, 39693.79, 39827.96, 40074.94, 40059.84] + }`) + + var buildKLines = func(bytes []byte) (kLines []types.KLine) { + var prices map[string][]fixedpoint.Value + _ = json.Unmarshal(bytes, &prices) + for i, h := range prices["high"] { + kLine := types.KLine{High: h, Low: prices["low"][i], Close: prices["close"][i]} + kLines = append(kLines, kLine) + } + return kLines + } + + tests := []struct { + name string + kLines []types.KLine + window int + want float64 + }{ + { + name: "test_binance_btcusdt_1h", + kLines: buildKLines(bytes), + window: 14, + want: 367.913903, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stream := &types.StandardStream{} + + kLines := KLines(stream) + atr := ATR2(kLines) + rma := RMA2(atr, tt.window, true) + + for _, k := range tt.kLines { + stream.EmitKLineClosed(k) + } + + got := rma.Last() + diff := math.Trunc((got-tt.want)*100) / 100 + if diff != 0 { + t.Errorf("calculateATR2() = %v, want %v", got, tt.want) + } + }) + } +} From a887eaf54229d442c4bcbc4b6897f1d11e744187 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 13:29:14 +0800 Subject: [PATCH 0914/1392] indicator: fix the comment --- pkg/indicator/atr2.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index 65a214bc20..4df00502b5 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -6,8 +6,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// ATRStream is a RMAStream -// This ATRStream calcualtes the ATR first, and then push it to the RMAStream +// This ATRStream calculates the ATR first type ATRStream struct { // embedded struct Float64Series From ebf9c43cd589b923db1b7fa4f14411649229846e Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 13:46:51 +0800 Subject: [PATCH 0915/1392] indicator: separate TR + RMA and ATR = TR + RMA --- pkg/indicator/atr2.go | 44 +++----------------- pkg/indicator/atr2_test.go | 7 ++-- pkg/indicator/tr2.go | 47 ++++++++++++++++++++++ pkg/indicator/tr2_test.go | 82 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 43 deletions(-) create mode 100644 pkg/indicator/tr2.go create mode 100644 pkg/indicator/tr2_test.go diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index 4df00502b5..f0cdc2c128 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -1,47 +1,13 @@ package indicator -import ( - "math" - - "github.com/c9s/bbgo/pkg/types" -) - -// This ATRStream calculates the ATR first type ATRStream struct { // embedded struct - Float64Series - - // private states - previousClose float64 + *RMAStream } -func ATR2(source KLineSubscription) *ATRStream { - s := &ATRStream{ - Float64Series: NewFloat64Series(), - } - - source.AddSubscriber(func(k types.KLine) { - s.calculateAndPush(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) - }) +func ATR2(source KLineSubscription, window int) *ATRStream { + s := &ATRStream{} + tr := TR2(source) + s.RMAStream = RMA2(tr, window, true) return s } - -func (s *ATRStream) calculateAndPush(high, low, cls float64) { - if s.previousClose == .0 { - s.previousClose = cls - return - } - - trueRange := high - low - hc := math.Abs(high - s.previousClose) - lc := math.Abs(low - s.previousClose) - if trueRange < hc { - trueRange = hc - } - if trueRange < lc { - trueRange = lc - } - - s.previousClose = cls - s.EmitUpdate(trueRange) -} diff --git a/pkg/indicator/atr2_test.go b/pkg/indicator/atr2_test.go index 6355b476df..1dd49501a2 100644 --- a/pkg/indicator/atr2_test.go +++ b/pkg/indicator/atr2_test.go @@ -65,17 +65,16 @@ func Test_ATR2(t *testing.T) { stream := &types.StandardStream{} kLines := KLines(stream) - atr := ATR2(kLines) - rma := RMA2(atr, tt.window, true) + atr := ATR2(kLines, tt.window) for _, k := range tt.kLines { stream.EmitKLineClosed(k) } - got := rma.Last() + got := atr.Last() diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { - t.Errorf("calculateATR2() = %v, want %v", got, tt.want) + t.Errorf("ATR2() = %v, want %v", got, tt.want) } }) } diff --git a/pkg/indicator/tr2.go b/pkg/indicator/tr2.go new file mode 100644 index 0000000000..05c6350e20 --- /dev/null +++ b/pkg/indicator/tr2.go @@ -0,0 +1,47 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +// This TRStream calculates the ATR first +type TRStream struct { + // embedded struct + Float64Series + + // private states + previousClose float64 +} + +func TR2(source KLineSubscription) *TRStream { + s := &TRStream{ + Float64Series: NewFloat64Series(), + } + + source.AddSubscriber(func(k types.KLine) { + s.calculateAndPush(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) + }) + return s +} + +func (s *TRStream) calculateAndPush(high, low, cls float64) { + if s.previousClose == .0 { + s.previousClose = cls + return + } + + trueRange := high - low + hc := math.Abs(high - s.previousClose) + lc := math.Abs(low - s.previousClose) + if trueRange < hc { + trueRange = hc + } + if trueRange < lc { + trueRange = lc + } + + s.previousClose = cls + s.EmitUpdate(trueRange) +} diff --git a/pkg/indicator/tr2_test.go b/pkg/indicator/tr2_test.go new file mode 100644 index 0000000000..38195f55d7 --- /dev/null +++ b/pkg/indicator/tr2_test.go @@ -0,0 +1,82 @@ +package indicator + +import ( + "encoding/json" + "math" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +/* +python + +import pandas as pd +import pandas_ta as ta + + data = { + "high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0], + "low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84], + "close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, + +39693.79, 39827.96, 40074.94, 40059.84] +} + +high = pd.Series(data['high']) +low = pd.Series(data['low']) +close = pd.Series(data['close']) +result = ta.atr(high, low, close, length=14) +print(result) +*/ +func Test_TR_and_RMA(t *testing.T) { + var bytes = []byte(`{ + "high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0], + "low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84], + "close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, 39693.79, 39827.96, 40074.94, 40059.84] + }`) + + var buildKLines = func(bytes []byte) (kLines []types.KLine) { + var prices map[string][]fixedpoint.Value + _ = json.Unmarshal(bytes, &prices) + for i, h := range prices["high"] { + kLine := types.KLine{High: h, Low: prices["low"][i], Close: prices["close"][i]} + kLines = append(kLines, kLine) + } + return kLines + } + + tests := []struct { + name string + kLines []types.KLine + window int + want float64 + }{ + { + name: "test_binance_btcusdt_1h", + kLines: buildKLines(bytes), + window: 14, + want: 367.913903, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stream := &types.StandardStream{} + + kLines := KLines(stream) + atr := TR2(kLines) + rma := RMA2(atr, tt.window, true) + + for _, k := range tt.kLines { + stream.EmitKLineClosed(k) + } + + got := rma.Last() + diff := math.Trunc((got-tt.want)*100) / 100 + if diff != 0 { + t.Errorf("RMA(TR()) = %v, want %v", got, tt.want) + } + }) + } +} From 266016a278704a97483b879745fd4d8d84965fca Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 30 May 2023 13:53:59 +0800 Subject: [PATCH 0916/1392] indicator: simplify ATR2 --- pkg/indicator/atr2.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/indicator/atr2.go b/pkg/indicator/atr2.go index f0cdc2c128..fac3e8e060 100644 --- a/pkg/indicator/atr2.go +++ b/pkg/indicator/atr2.go @@ -6,8 +6,7 @@ type ATRStream struct { } func ATR2(source KLineSubscription, window int) *ATRStream { - s := &ATRStream{} tr := TR2(source) - s.RMAStream = RMA2(tr, window, true) - return s + rma := RMA2(tr, window, true) + return &ATRStream{RMAStream: rma} } From ba0102e992dcef68b352c21eb64491f377ca0f8b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 31 May 2023 13:08:21 +0800 Subject: [PATCH 0917/1392] pivotshort: fix find pivot func call --- pkg/strategy/pivotshort/failedbreakhigh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/pivotshort/failedbreakhigh.go b/pkg/strategy/pivotshort/failedbreakhigh.go index eb30dc91bf..30ad0a32ef 100644 --- a/pkg/strategy/pivotshort/failedbreakhigh.go +++ b/pkg/strategy/pivotshort/failedbreakhigh.go @@ -341,7 +341,7 @@ func (s *FailedBreakHigh) detectMacdDivergence() { var histogramPivots floats.Slice for i := pivotWindow; i > 0 && i < len(histogramValues); i++ { // find positive histogram and the top - pivot, ok := floats.CalculatePivot(histogramValues[0:i], pivotWindow, pivotWindow, func(a, pivot float64) bool { + pivot, ok := floats.FindPivot(histogramValues[0:i], pivotWindow, pivotWindow, func(a, pivot float64) bool { return pivot > 0 && pivot > a }) if ok { From e58db43067a342cddaa2c080f718caec0fa20c8b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 31 May 2023 13:08:40 +0800 Subject: [PATCH 0918/1392] indicator: rename v2 indicators --- pkg/indicator/{macdlegacy.go => macd.go} | 0 .../{macdlegacy_test.go => macd_test.go} | 0 pkg/indicator/{atr2.go => v2_atr.go} | 0 .../{atr2_test.go => v2_atr_test.go} | 0 pkg/indicator/{ewma2.go => v2_ewma.go} | 0 pkg/indicator/{rma2.go => v2_rma.go} | 0 pkg/indicator/v2_rsi.go | 30 +++++++++++++++++++ pkg/indicator/{tr2.go => v2_tr.go} | 0 pkg/indicator/{tr2_test.go => v2_tr_test.go} | 0 9 files changed, 30 insertions(+) rename pkg/indicator/{macdlegacy.go => macd.go} (100%) rename pkg/indicator/{macdlegacy_test.go => macd_test.go} (100%) rename pkg/indicator/{atr2.go => v2_atr.go} (100%) rename pkg/indicator/{atr2_test.go => v2_atr_test.go} (100%) rename pkg/indicator/{ewma2.go => v2_ewma.go} (100%) rename pkg/indicator/{rma2.go => v2_rma.go} (100%) create mode 100644 pkg/indicator/v2_rsi.go rename pkg/indicator/{tr2.go => v2_tr.go} (100%) rename pkg/indicator/{tr2_test.go => v2_tr_test.go} (100%) diff --git a/pkg/indicator/macdlegacy.go b/pkg/indicator/macd.go similarity index 100% rename from pkg/indicator/macdlegacy.go rename to pkg/indicator/macd.go diff --git a/pkg/indicator/macdlegacy_test.go b/pkg/indicator/macd_test.go similarity index 100% rename from pkg/indicator/macdlegacy_test.go rename to pkg/indicator/macd_test.go diff --git a/pkg/indicator/atr2.go b/pkg/indicator/v2_atr.go similarity index 100% rename from pkg/indicator/atr2.go rename to pkg/indicator/v2_atr.go diff --git a/pkg/indicator/atr2_test.go b/pkg/indicator/v2_atr_test.go similarity index 100% rename from pkg/indicator/atr2_test.go rename to pkg/indicator/v2_atr_test.go diff --git a/pkg/indicator/ewma2.go b/pkg/indicator/v2_ewma.go similarity index 100% rename from pkg/indicator/ewma2.go rename to pkg/indicator/v2_ewma.go diff --git a/pkg/indicator/rma2.go b/pkg/indicator/v2_rma.go similarity index 100% rename from pkg/indicator/rma2.go rename to pkg/indicator/v2_rma.go diff --git a/pkg/indicator/v2_rsi.go b/pkg/indicator/v2_rsi.go new file mode 100644 index 0000000000..0e3f0a2f68 --- /dev/null +++ b/pkg/indicator/v2_rsi.go @@ -0,0 +1,30 @@ +package indicator + +type RSIStream struct { + // embedded structs + Float64Series + + // config fields + window int + + // private states +} + +func RSI2(source Float64Source, window int) *RSIStream { + s := &RSIStream{ + Float64Series: NewFloat64Series(), + window: window, + } + + if sub, ok := source.(Float64Subscription); ok { + sub.AddSubscriber(s.calculateAndPush) + } else { + source.OnUpdate(s.calculateAndPush) + } + + return s +} + +func (s *RSIStream) calculateAndPush(x float64) { + +} diff --git a/pkg/indicator/tr2.go b/pkg/indicator/v2_tr.go similarity index 100% rename from pkg/indicator/tr2.go rename to pkg/indicator/v2_tr.go diff --git a/pkg/indicator/tr2_test.go b/pkg/indicator/v2_tr_test.go similarity index 100% rename from pkg/indicator/tr2_test.go rename to pkg/indicator/v2_tr_test.go From 114e292d8f3f72b3c7558f9c26c4c4eecd315751 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 31 May 2023 16:30:04 +0800 Subject: [PATCH 0919/1392] indicator: rewrite RSI indicator --- pkg/indicator/price.go | 8 +++- pkg/indicator/types.go | 17 +++++++ pkg/indicator/util.go | 14 ++++++ pkg/indicator/v2.go | 14 ------ pkg/indicator/v2_rsi.go | 28 +++++++++++- pkg/indicator/v2_rsi_test.go | 87 ++++++++++++++++++++++++++++++++++++ 6 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 pkg/indicator/types.go create mode 100644 pkg/indicator/v2_rsi_test.go diff --git a/pkg/indicator/price.go b/pkg/indicator/price.go index a3c7c45a14..e80736715e 100644 --- a/pkg/indicator/price.go +++ b/pkg/indicator/price.go @@ -23,12 +23,16 @@ func Price(source KLineSubscription, mapper KLineValueMapper) *PriceStream { source.AddSubscriber(func(k types.KLine) { v := s.mapper(k) - s.slice.Push(v) - s.EmitUpdate(v) + s.PushAndEmit(v) }) return s } +func (s *PriceStream) PushAndEmit(v float64) { + s.slice.Push(v) + s.EmitUpdate(v) +} + func ClosePrices(source KLineSubscription) *PriceStream { return Price(source, KLineClosePriceMapper) } diff --git a/pkg/indicator/types.go b/pkg/indicator/types.go new file mode 100644 index 0000000000..78dcaabd3e --- /dev/null +++ b/pkg/indicator/types.go @@ -0,0 +1,17 @@ +package indicator + +import "github.com/c9s/bbgo/pkg/types" + +type Float64Calculator interface { + Calculate(x float64) float64 +} + +type Float64Source interface { + types.Series + OnUpdate(f func(v float64)) +} + +type Float64Subscription interface { + types.Series + AddSubscriber(f func(v float64)) +} diff --git a/pkg/indicator/util.go b/pkg/indicator/util.go index 722d45a367..f0a76d2b04 100644 --- a/pkg/indicator/util.go +++ b/pkg/indicator/util.go @@ -1 +1,15 @@ package indicator + +func max(x, y int) int { + if x > y { + return x + } + return y +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} diff --git a/pkg/indicator/v2.go b/pkg/indicator/v2.go index b1ef53bd90..80fd680957 100644 --- a/pkg/indicator/v2.go +++ b/pkg/indicator/v2.go @@ -1,9 +1,5 @@ package indicator -import ( - "github.com/c9s/bbgo/pkg/types" -) - /* NEW INDICATOR DESIGN: @@ -21,13 +17,3 @@ macd := Subtract(fastEMA, slowEMA) signal := EMA(macd, 16) histogram := Subtract(macd, signal) */ - -type Float64Source interface { - types.Series - OnUpdate(f func(v float64)) -} - -type Float64Subscription interface { - types.Series - AddSubscriber(f func(v float64)) -} diff --git a/pkg/indicator/v2_rsi.go b/pkg/indicator/v2_rsi.go index 0e3f0a2f68..16732f2bdc 100644 --- a/pkg/indicator/v2_rsi.go +++ b/pkg/indicator/v2_rsi.go @@ -8,10 +8,12 @@ type RSIStream struct { window int // private states + source Float64Source } func RSI2(source Float64Source, window int) *RSIStream { s := &RSIStream{ + source: source, Float64Series: NewFloat64Series(), window: window, } @@ -25,6 +27,30 @@ func RSI2(source Float64Source, window int) *RSIStream { return s } -func (s *RSIStream) calculateAndPush(x float64) { +func (s *RSIStream) calculate(_ float64) float64 { + var gainSum, lossSum float64 + var sourceLen = s.source.Length() + var limit = min(s.window, sourceLen) + for i := 0; i < limit; i++ { + value := s.source.Index(i) + prev := s.source.Index(i + 1) + change := value - prev + if change >= 0 { + gainSum += change + } else { + lossSum += -change + } + } + avgGain := gainSum / float64(limit) + avgLoss := lossSum / float64(limit) + rs := avgGain / avgLoss + rsi := 100.0 - (100.0 / (1.0 + rs)) + return rsi +} + +func (s *RSIStream) calculateAndPush(x float64) { + rsi := s.calculate(x) + s.slice.Push(rsi) + s.EmitUpdate(rsi) } diff --git a/pkg/indicator/v2_rsi_test.go b/pkg/indicator/v2_rsi_test.go new file mode 100644 index 0000000000..533a89e1a4 --- /dev/null +++ b/pkg/indicator/v2_rsi_test.go @@ -0,0 +1,87 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/datatype/floats" +) + +func Test_RSI2(t *testing.T) { + // test case from https://school.stockcharts.com/doku.php?id=technical_indicators:relative_strength_index_rsi + var data = []byte(`[44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00, 46.03, 46.41, 46.22, 45.64, 46.21, 46.25, 45.71, 46.45, 45.78, 45.35, 44.03, 44.18, 44.22, 44.57, 43.42, 42.66, 43.13]`) + var values []float64 + err := json.Unmarshal(data, &values) + assert.NoError(t, err) + + tests := []struct { + name string + values []float64 + window int + want floats.Slice + }{ + { + name: "RSI", + values: values, + window: 14, + want: floats.Slice{ + 100.000000, + 99.439336, + 99.440090, + 98.251826, + 98.279242, + 98.297781, + 98.307626, + 98.319149, + 98.334036, + 98.342426, + 97.951933, + 97.957908, + 97.108036, + 97.147514, + 70.464135, + 70.020964, + 69.831224, + 80.567686, + 73.333333, + 59.806295, + 62.528217, + 60.000000, + 48.477752, + 53.878407, + 48.952381, + 43.862816, + 37.732919, + 32.263514, + 32.718121, + 38.142620, + 31.748252, + 25.099602, + 30.217670, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // RSI2() + prices := &PriceStream{} + rsi := RSI2(prices, tt.window) + + t.Logf("data length: %d", len(tt.values)) + for _, price := range tt.values { + prices.PushAndEmit(price) + } + + assert.Equal(t, floats.Slice(tt.values), prices.slice) + + if assert.Equal(t, len(tt.want), len(rsi.slice)) { + for i, v := range tt.want { + assert.InDelta(t, v, rsi.slice[i], 0.000001, "Expected rsi.slice[%d] to be %v, but got %v", i, v, rsi.slice[i]) + } + } + }) + } +} From 2a074ba11bae6b619f85d6439e696f8e13b8d55d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 31 May 2023 16:30:19 +0800 Subject: [PATCH 0920/1392] floats: add Average method on floats.Slice --- pkg/datatype/floats/slice.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index 727cfe9bb6..84e1ff2220 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -73,7 +73,10 @@ func (s Slice) Add(b Slice) (c Slice) { } func (s Slice) Sum() (sum float64) { - return floats.Sum(s) + for _, v := range s { + sum += v + } + return sum } func (s Slice) Mean() (mean float64) { @@ -97,6 +100,18 @@ func (s Slice) Tail(size int) Slice { return win } +func (s Slice) Average() float64 { + if len(s) == 0 { + return 0.0 + } + + total := 0.0 + for _, value := range s { + total += value + } + return total / float64(len(s)) +} + func (s Slice) Diff() (values Slice) { for i, v := range s { if i == 0 { @@ -171,17 +186,19 @@ func (s Slice) Addr() *Slice { func (s Slice) Last() float64 { length := len(s) if length > 0 { - return (s)[length-1] + return s[length-1] } return 0.0 } +// Index fetches the element from the end of the slice +// WARNING: it does not start from 0!!! func (s Slice) Index(i int) float64 { length := len(s) - if length-i <= 0 || i < 0 { + if i < 0 || length-1-i < 0 { return 0.0 } - return (s)[length-i-1] + return s[length-1-i] } func (s Slice) Length() int { From 5515f588e3e56399196d1570dc13f98f3c1aaa41 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 31 May 2023 19:35:44 +0800 Subject: [PATCH 0921/1392] all: add parameter index to the Last method --- pkg/bbgo/exit_lower_shadow_take_profit.go | 2 +- pkg/bbgo/stop_ema.go | 2 +- pkg/bbgo/trend_ema.go | 4 +- pkg/datatype/floats/slice.go | 8 +- pkg/indicator/ad.go | 13 +- pkg/indicator/alma.go | 11 +- pkg/indicator/alma_test.go | 2 +- pkg/indicator/atr.go | 8 +- pkg/indicator/atr_test.go | 2 +- pkg/indicator/atrp.go | 10 +- pkg/indicator/boll.go | 8 +- pkg/indicator/boll_test.go | 4 +- pkg/indicator/cci.go | 20 +- pkg/indicator/cci_test.go | 2 +- pkg/indicator/cma.go | 23 +- pkg/indicator/dema.go | 17 +- pkg/indicator/dema_test.go | 2 +- pkg/indicator/dmi.go | 10 +- pkg/indicator/dmi_test.go | 6 +- pkg/indicator/drift.go | 20 +- pkg/indicator/emv.go | 5 +- pkg/indicator/emv_test.go | 4 +- pkg/indicator/ewma.go | 14 +- pkg/indicator/fisher.go | 10 +- pkg/indicator/float64updater.go | 10 +- pkg/indicator/ghfilter.go | 14 +- pkg/indicator/ghfilter_test.go | 10 +- pkg/indicator/gma.go | 9 +- pkg/indicator/gma_test.go | 4 +- pkg/indicator/hull.go | 9 +- pkg/indicator/hull_test.go | 2 +- pkg/indicator/interface.go | 2 +- pkg/indicator/kalmanfilter.go | 22 +- pkg/indicator/kalmanfilter_test.go | 10 +- pkg/indicator/klingeroscillator.go | 11 +- pkg/indicator/line.go | 6 +- pkg/indicator/linreg.go | 25 +- pkg/indicator/low.go | 2 +- pkg/indicator/macd.go | 23 +- pkg/indicator/macd_test.go | 2 +- pkg/indicator/obv.go | 18 +- pkg/indicator/pivot.go | 5 +- pkg/indicator/pivothigh.go | 6 +- pkg/indicator/pivotlow.go | 6 +- pkg/indicator/psar.go | 10 +- pkg/indicator/psar_test.go | 2 +- pkg/indicator/rma.go | 12 +- pkg/indicator/rsi.go | 15 +- pkg/indicator/sma.go | 15 +- pkg/indicator/sma_test.go | 4 +- pkg/indicator/ssf.go | 30 +- pkg/indicator/ssf_test.go | 2 +- pkg/indicator/stddev.go | 15 +- pkg/indicator/supertrend.go | 26 +- pkg/indicator/supertrendPivot.go | 42 ++- pkg/indicator/tema.go | 22 +- pkg/indicator/tema_test.go | 2 +- pkg/indicator/till.go | 30 +- pkg/indicator/till_test.go | 2 +- pkg/indicator/tma.go | 19 +- pkg/indicator/tsi.go | 13 +- pkg/indicator/tsi_test.go | 2 +- pkg/indicator/utBotAlert.go | 24 +- pkg/indicator/v2_atr_test.go | 2 +- pkg/indicator/v2_ewma.go | 2 +- pkg/indicator/v2_rsi.go | 4 +- pkg/indicator/v2_tr_test.go | 2 +- pkg/indicator/vidya.go | 12 +- pkg/indicator/vidya_test.go | 6 +- pkg/indicator/volatility.go | 12 +- pkg/indicator/vwap.go | 18 +- pkg/indicator/vwma.go | 21 +- pkg/indicator/wdrift.go | 21 +- pkg/indicator/wwma.go | 18 +- pkg/indicator/zlema.go | 13 +- pkg/indicator/zlema_test.go | 2 +- pkg/risk/dynamicrisk/dynamic_exposure.go | 6 +- pkg/risk/dynamicrisk/dynamic_spread.go | 14 +- pkg/strategy/audacitymaker/orderflow.go | 8 +- pkg/strategy/bollgrid/strategy.go | 4 +- pkg/strategy/bollmaker/dynamic_spread.go | 14 +- pkg/strategy/bollmaker/strategy.go | 10 +- pkg/strategy/bollmaker/trend.go | 2 +- pkg/strategy/drift/driftma.go | 10 +- pkg/strategy/drift/stoploss.go | 2 +- pkg/strategy/drift/strategy.go | 20 +- pkg/strategy/elliottwave/ewo.go | 4 +- pkg/strategy/elliottwave/strategy.go | 8 +- pkg/strategy/emastop/strategy.go | 2 +- pkg/strategy/ewoDgtrd/heikinashi.go | 12 +- pkg/strategy/ewoDgtrd/strategy.go | 84 +++--- pkg/strategy/factorzoo/factors/momentum.go | 16 +- .../factorzoo/factors/price_mean_reversion.go | 10 +- .../factors/price_volume_divergence.go | 8 +- pkg/strategy/factorzoo/factors/return_rate.go | 12 +- .../factorzoo/factors/volume_momentum.go | 16 +- pkg/strategy/factorzoo/linear_regression.go | 10 +- pkg/strategy/fixedmaker/strategy.go | 2 +- pkg/strategy/flashcrash/strategy.go | 2 +- pkg/strategy/fmaker/A18.go | 6 +- pkg/strategy/fmaker/A2.go | 6 +- pkg/strategy/fmaker/A3.go | 2 +- pkg/strategy/fmaker/A34.go | 4 +- pkg/strategy/fmaker/R.go | 2 +- pkg/strategy/fmaker/S0.go | 4 +- pkg/strategy/fmaker/S1.go | 2 +- pkg/strategy/fmaker/S2.go | 2 +- pkg/strategy/fmaker/S3.go | 2 +- pkg/strategy/fmaker/S4.go | 2 +- pkg/strategy/fmaker/S5.go | 4 +- pkg/strategy/fmaker/S6.go | 10 +- pkg/strategy/fmaker/S7.go | 6 +- pkg/strategy/fmaker/strategy.go | 6 +- pkg/strategy/grid2/strategy.go | 4 +- pkg/strategy/harmonic/shark.go | 6 +- pkg/strategy/harmonic/strategy.go | 6 +- pkg/strategy/irr/neg_return_rate.go | 14 +- pkg/strategy/irr/strategy.go | 2 +- pkg/strategy/linregmaker/strategy.go | 20 +- pkg/strategy/pivotshort/breaklow.go | 8 +- pkg/strategy/pivotshort/failedbreakhigh.go | 10 +- pkg/strategy/pivotshort/resistance.go | 2 +- pkg/strategy/pricedrop/strategy.go | 4 +- pkg/strategy/rsmaker/strategy.go | 6 +- pkg/strategy/schedule/strategy.go | 4 +- pkg/strategy/skeleton/strategy.go | 2 +- pkg/strategy/supertrend/double_dema.go | 4 +- pkg/strategy/supertrend/linreg.go | 10 +- pkg/strategy/supertrend/strategy.go | 6 +- pkg/strategy/support/strategy.go | 12 +- pkg/strategy/swing/strategy.go | 4 +- pkg/strategy/techsignal/strategy.go | 2 +- pkg/strategy/trendtrader/trend.go | 12 +- pkg/strategy/xmaker/strategy.go | 4 +- pkg/types/filter.go | 10 +- pkg/types/indicator.go | 283 +++--------------- pkg/types/indicator_test.go | 34 +-- pkg/types/kline.go | 2 +- pkg/types/queue.go | 51 ++++ pkg/types/series.go | 123 ++++++++ pkg/types/seriesbase_imp.go | 10 +- pkg/types/sigmoid.go | 27 ++ 142 files changed, 843 insertions(+), 996 deletions(-) create mode 100644 pkg/types/queue.go create mode 100644 pkg/types/series.go create mode 100644 pkg/types/sigmoid.go diff --git a/pkg/bbgo/exit_lower_shadow_take_profit.go b/pkg/bbgo/exit_lower_shadow_take_profit.go index 1ffae6c368..32ac9928e4 100644 --- a/pkg/bbgo/exit_lower_shadow_take_profit.go +++ b/pkg/bbgo/exit_lower_shadow_take_profit.go @@ -48,7 +48,7 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge } // skip close price higher than the ewma - if closePrice.Float64() > ewma.Last() { + if closePrice.Float64() > ewma.Last(0) { return } diff --git a/pkg/bbgo/stop_ema.go b/pkg/bbgo/stop_ema.go index 11c0472b64..f2ec45676c 100644 --- a/pkg/bbgo/stop_ema.go +++ b/pkg/bbgo/stop_ema.go @@ -21,7 +21,7 @@ func (s *StopEMA) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExec } func (s *StopEMA) Allowed(closePrice fixedpoint.Value) bool { - ema := fixedpoint.NewFromFloat(s.stopEWMA.Last()) + ema := fixedpoint.NewFromFloat(s.stopEWMA.Last(0)) if ema.IsZero() { logrus.Infof("stopEMA protection: value is zero, skip") return false diff --git a/pkg/bbgo/trend_ema.go b/pkg/bbgo/trend_ema.go index c37e63aedb..36242fead6 100644 --- a/pkg/bbgo/trend_ema.go +++ b/pkg/bbgo/trend_ema.go @@ -29,12 +29,12 @@ func (s *TrendEMA) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExe } s.last = s.ewma.Values[s.ewma.Length()-2] - s.current = s.ewma.Last() + s.current = s.ewma.Last(0) }) session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { s.last = s.current - s.current = s.ewma.Last() + s.current = s.ewma.Last(0) })) } diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index 84e1ff2220..fe9314666b 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -183,12 +183,12 @@ func (s Slice) Addr() *Slice { } // Last, Index, Length implements the types.Series interface -func (s Slice) Last() float64 { +func (s Slice) Last(i int) float64 { length := len(s) - if length > 0 { - return s[length-1] + if i < 0 || length-1-i < 0 { + return 0.0 } - return 0.0 + return s[length-1-i] } // Index fetches the element from the end of the slice diff --git a/pkg/indicator/ad.go b/pkg/indicator/ad.go index e94f8d2463..463e304ebb 100644 --- a/pkg/indicator/ad.go +++ b/pkg/indicator/ad.go @@ -35,15 +35,16 @@ func (inc *AD) Update(high, low, cloze, volume float64) { moneyFlowVolume = ((2*cloze - high - low) / (high - low)) * volume } - ad := inc.Last() + moneyFlowVolume + ad := inc.Last(0) + moneyFlowVolume inc.Values.Push(ad) } -func (inc *AD) Last() float64 { - if len(inc.Values) == 0 { - return 0.0 +func (inc *AD) Last(i int) float64 { + length := len(inc.Values) + if length == 0 || length-i-1 < 0 { + return 0 } - return inc.Values[len(inc.Values)-1] + return inc.Values[length-i-1] } func (inc *AD) Index(i int) float64 { @@ -68,7 +69,7 @@ func (inc *AD) CalculateAndUpdate(kLines []types.KLine) { inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64(), k.Volume.Float64()) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } diff --git a/pkg/indicator/alma.go b/pkg/indicator/alma.go index f27de2b704..6837db7669 100644 --- a/pkg/indicator/alma.go +++ b/pkg/indicator/alma.go @@ -20,6 +20,7 @@ import ( // // @param offset: Gaussian applied to the combo line. 1->ema, 0->sma // @param sigma: the standard deviation applied to the combo line. This makes the combo line sharper +// //go:generate callbackgen -type ALMA type ALMA struct { types.SeriesBase @@ -64,11 +65,11 @@ func (inc *ALMA) Update(value float64) { } } -func (inc *ALMA) Last() float64 { - if len(inc.Values) == 0 { +func (inc *ALMA) Last(i int) float64 { + if i >= len(inc.Values) { return 0 } - return inc.Values[len(inc.Values)-1] + return inc.Values[len(inc.Values)-i-1] } func (inc *ALMA) Index(i int) float64 { @@ -88,12 +89,12 @@ func (inc *ALMA) CalculateAndUpdate(allKLines []types.KLine) { if inc.input == nil { for _, k := range allKLines { inc.Update(k.Close.Float64()) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } return } inc.Update(allKLines[len(allKLines)-1].Close.Float64()) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func (inc *ALMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { diff --git a/pkg/indicator/alma_test.go b/pkg/indicator/alma_test.go index a00d7113d8..a21342f8a9 100644 --- a/pkg/indicator/alma_test.go +++ b/pkg/indicator/alma_test.go @@ -53,7 +53,7 @@ func Test_ALMA(t *testing.T) { Sigma: 6, } alma.CalculateAndUpdate(tt.kLines) - assert.InDelta(t, tt.want, alma.Last(), Delta) + assert.InDelta(t, tt.want, alma.Last(0), Delta) assert.InDelta(t, tt.next, alma.Index(1), Delta) assert.Equal(t, tt.all, alma.Length()) }) diff --git a/pkg/indicator/atr.go b/pkg/indicator/atr.go index 0d5b264cbf..335c36e2a5 100644 --- a/pkg/indicator/atr.go +++ b/pkg/indicator/atr.go @@ -74,18 +74,18 @@ func (inc *ATR) Update(high, low, cloze float64) { // apply rolling moving average inc.RMA.Update(trueRange) - atr := inc.RMA.Last() + atr := inc.RMA.Last(0) inc.PercentageVolatility.Push(atr / cloze) if len(inc.PercentageVolatility) > MaxNumOfATR { inc.PercentageVolatility = inc.PercentageVolatility[MaxNumOfATRTruncateSize-1:] } } -func (inc *ATR) Last() float64 { +func (inc *ATR) Last(i int) float64 { if inc.RMA == nil { return 0 } - return inc.RMA.Last() + return inc.RMA.Last(i) } func (inc *ATR) Index(i int) float64 { @@ -110,5 +110,5 @@ func (inc *ATR) PushK(k types.KLine) { inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } diff --git a/pkg/indicator/atr_test.go b/pkg/indicator/atr_test.go index b5cb138e98..732dbae22d 100644 --- a/pkg/indicator/atr_test.go +++ b/pkg/indicator/atr_test.go @@ -65,7 +65,7 @@ func Test_calculateATR(t *testing.T) { atr.PushK(k) } - got := atr.Last() + got := atr.Last(0) diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { t.Errorf("calculateATR() = %v, want %v", got, tt.want) diff --git a/pkg/indicator/atrp.go b/pkg/indicator/atrp.go index 8d473942ca..4e85589047 100644 --- a/pkg/indicator/atrp.go +++ b/pkg/indicator/atrp.go @@ -21,7 +21,7 @@ import ( // // Calculation: // -// ATRP = (Average True Range / Close) * 100 +// ATRP = (Average True Range / Close) * 100 // //go:generate callbackgen -type ATRP type ATRP struct { @@ -69,15 +69,15 @@ func (inc *ATRP) Update(high, low, cloze float64) { // apply rolling moving average inc.RMA.Update(trueRange) - atr := inc.RMA.Last() + atr := inc.RMA.Last(0) inc.PercentageVolatility.Push(atr / cloze) } -func (inc *ATRP) Last() float64 { +func (inc *ATRP) Last(i int) float64 { if inc.RMA == nil { return 0 } - return inc.RMA.Last() + return inc.RMA.Last(i) } func (inc *ATRP) Index(i int) float64 { @@ -109,7 +109,7 @@ func (inc *ATRP) CalculateAndUpdate(kLines []types.KLine) { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } diff --git a/pkg/indicator/boll.go b/pkg/indicator/boll.go index 712481ffcb..1a670608c1 100644 --- a/pkg/indicator/boll.go +++ b/pkg/indicator/boll.go @@ -84,8 +84,8 @@ func (inc *BOLL) Update(value float64) { inc.SMA.Update(value) inc.StdDev.Update(value) - var sma = inc.SMA.Last() - var stdDev = inc.StdDev.Last() + var sma = inc.SMA.Last(0) + var stdDev = inc.StdDev.Last(0) var band = inc.K * stdDev var upBand = sma + band @@ -105,7 +105,7 @@ func (inc *BOLL) PushK(k types.KLine) { } inc.Update(k.Close.Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.SMA.Last(), inc.UpBand.Last(), inc.DownBand.Last()) + inc.EmitUpdate(inc.SMA.Last(0), inc.UpBand.Last(0), inc.DownBand.Last(0)) } func (inc *BOLL) LoadK(allKLines []types.KLine) { @@ -113,7 +113,7 @@ func (inc *BOLL) LoadK(allKLines []types.KLine) { inc.PushK(k) } - inc.EmitUpdate(inc.SMA.Last(), inc.UpBand.Last(), inc.DownBand.Last()) + inc.EmitUpdate(inc.SMA.Last(0), inc.UpBand.Last(0), inc.DownBand.Last(0)) } func (inc *BOLL) CalculateAndUpdate(allKLines []types.KLine) { diff --git a/pkg/indicator/boll_test.go b/pkg/indicator/boll_test.go index 7bc8c574c2..579ce2337a 100644 --- a/pkg/indicator/boll_test.go +++ b/pkg/indicator/boll_test.go @@ -61,8 +61,8 @@ func TestBOLL(t *testing.T) { t.Run(tt.name, func(t *testing.T) { boll := BOLL{IntervalWindow: types.IntervalWindow{Window: tt.window}, K: tt.k} boll.CalculateAndUpdate(tt.kLines) - assert.InDelta(t, tt.up, boll.UpBand.Last(), Delta) - assert.InDelta(t, tt.down, boll.DownBand.Last(), Delta) + assert.InDelta(t, tt.up, boll.UpBand.Last(0), Delta) + assert.InDelta(t, tt.down, boll.DownBand.Last(0), Delta) }) } diff --git a/pkg/indicator/cci.go b/pkg/indicator/cci.go index 64d4427b25..dd24b46c3b 100644 --- a/pkg/indicator/cci.go +++ b/pkg/indicator/cci.go @@ -43,7 +43,7 @@ func (inc *CCI) Update(value float64) { } inc.Input.Push(value) - tp := inc.TypicalPrice.Last() - inc.Input.Index(inc.Window) + value + tp := inc.TypicalPrice.Last(0) - inc.Input.Index(inc.Window) + value inc.TypicalPrice.Push(tp) if len(inc.Input) < inc.Window { return @@ -55,7 +55,7 @@ func (inc *CCI) Update(value float64) { } md := 0. for i := 0; i < inc.Window; i++ { - diff := inc.Input.Index(i) - ma + diff := inc.Input.Last(i) - ma md += diff * diff } md = math.Sqrt(md / float64(inc.Window)) @@ -68,18 +68,12 @@ func (inc *CCI) Update(value float64) { } } -func (inc *CCI) Last() float64 { - if len(inc.Values) == 0 { - return 0 - } - return inc.Values[len(inc.Values)-1] +func (inc *CCI) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *CCI) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - return inc.Values[len(inc.Values)-1-i] + return inc.Last(i) } func (inc *CCI) Length() int { @@ -96,12 +90,12 @@ func (inc *CCI) CalculateAndUpdate(allKLines []types.KLine) { if inc.TypicalPrice.Length() == 0 { for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } diff --git a/pkg/indicator/cci_test.go b/pkg/indicator/cci_test.go index 4aeca6fcab..8d93947ef5 100644 --- a/pkg/indicator/cci_test.go +++ b/pkg/indicator/cci_test.go @@ -29,7 +29,7 @@ func Test_CCI(t *testing.T) { cci.Update(value) } - last := cci.Last() + last := cci.Last(0) assert.InDelta(t, 93.250481, last, Delta) assert.InDelta(t, 81.813449, cci.Index(1), Delta) assert.Equal(t, 50-16+1, cci.Length()) diff --git a/pkg/indicator/cma.go b/pkg/indicator/cma.go index 37dad3f8c7..51043f8054 100644 --- a/pkg/indicator/cma.go +++ b/pkg/indicator/cma.go @@ -7,12 +7,13 @@ import ( // Refer: Cumulative Moving Average, Cumulative Average // Refer: https://en.wikipedia.org/wiki/Moving_average +// //go:generate callbackgen -type CA type CA struct { types.SeriesBase - Interval types.Interval - Values floats.Slice - length float64 + Interval types.Interval + Values floats.Slice + length float64 UpdateCallbacks []func(value float64) } @@ -20,7 +21,7 @@ func (inc *CA) Update(x float64) { if len(inc.Values) == 0 { inc.SeriesBase.Series = inc } - newVal := (inc.Values.Last()*inc.length + x) / (inc.length + 1.) + newVal := (inc.Values.Last(0)*inc.length + x) / (inc.length + 1.) inc.length += 1 inc.Values.Push(newVal) if len(inc.Values) > MaxNumOfEWMA { @@ -29,18 +30,12 @@ func (inc *CA) Update(x float64) { } } -func (inc *CA) Last() float64 { - if len(inc.Values) == 0 { - return 0 - } - return inc.Values[len(inc.Values)-1] +func (inc *CA) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *CA) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - return inc.Values[len(inc.Values)-1-i] + return inc.Last(i) } func (inc *CA) Length() int { @@ -56,7 +51,7 @@ func (inc *CA) PushK(k types.KLine) { func (inc *CA) CalculateAndUpdate(allKLines []types.KLine) { for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } diff --git a/pkg/indicator/dema.go b/pkg/indicator/dema.go index 107923de52..6dbc97b595 100644 --- a/pkg/indicator/dema.go +++ b/pkg/indicator/dema.go @@ -51,22 +51,19 @@ func (inc *DEMA) Update(value float64) { } inc.a1.Update(value) - inc.a2.Update(inc.a1.Last()) - inc.Values.Push(2*inc.a1.Last() - inc.a2.Last()) + inc.a2.Update(inc.a1.Last(0)) + inc.Values.Push(2*inc.a1.Last(0) - inc.a2.Last(0)) if len(inc.Values) > MaxNumOfEWMA { inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] } } -func (inc *DEMA) Last() float64 { - return inc.Values.Last() +func (inc *DEMA) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *DEMA) Index(i int) float64 { - if len(inc.Values)-i-1 >= 0 { - return inc.Values[len(inc.Values)-1-i] - } - return 0 + return inc.Last(i) } func (inc *DEMA) Length() int { @@ -83,13 +80,13 @@ func (inc *DEMA) CalculateAndUpdate(allKLines []types.KLine) { if inc.a1 == nil { for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } else { // last k k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } diff --git a/pkg/indicator/dema_test.go b/pkg/indicator/dema_test.go index 99da987b98..845399bbf8 100644 --- a/pkg/indicator/dema_test.go +++ b/pkg/indicator/dema_test.go @@ -46,7 +46,7 @@ func Test_DEMA(t *testing.T) { t.Run(tt.name, func(t *testing.T) { dema := DEMA{IntervalWindow: types.IntervalWindow{Window: 16}} dema.CalculateAndUpdate(tt.kLines) - last := dema.Last() + last := dema.Last(0) assert.InDelta(t, tt.want, last, Delta) assert.InDelta(t, tt.next, dema.Index(1), Delta) assert.Equal(t, tt.all, dema.Length()) diff --git a/pkg/indicator/dmi.go b/pkg/indicator/dmi.go index 853b9deb36..0bf270477a 100644 --- a/pkg/indicator/dmi.go +++ b/pkg/indicator/dmi.go @@ -72,9 +72,9 @@ func (inc *DMI) Update(high, low, cloze float64) { if inc.atr.Length() < inc.Window { return } - k := 100. / inc.atr.Last() - dmp := inc.DMP.Last() - dmn := inc.DMN.Last() + k := 100. / inc.atr.Last(0) + dmp := inc.DMP.Last(0) + dmn := inc.DMN.Last(0) inc.DIPlus.Update(k * dmp) inc.DIMinus.Update(k * dmn) dx := 100. * math.Abs(dmp-dmn) / (dmp + dmn) @@ -108,11 +108,11 @@ func (inc *DMI) CalculateAndUpdate(allKLines []types.KLine) { if inc.ADX == nil { for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.DIPlus.Last(), inc.DIMinus.Last(), inc.ADX.Last()) + inc.EmitUpdate(inc.DIPlus.Last(0), inc.DIMinus.Last(0), inc.ADX.Last(0)) } } else { inc.PushK(last) - inc.EmitUpdate(inc.DIPlus.Last(), inc.DIMinus.Last(), inc.ADX.Last()) + inc.EmitUpdate(inc.DIPlus.Last(0), inc.DIMinus.Last(0), inc.ADX.Last(0)) } } diff --git a/pkg/indicator/dmi_test.go b/pkg/indicator/dmi_test.go index d93e164409..dae952d52b 100644 --- a/pkg/indicator/dmi_test.go +++ b/pkg/indicator/dmi_test.go @@ -78,9 +78,9 @@ func Test_DMI(t *testing.T) { ADXSmoothing: 14, } dmi.CalculateAndUpdate(tt.klines) - assert.InDelta(t, dmi.GetDIPlus().Last(), tt.want.dip, Delta) - assert.InDelta(t, dmi.GetDIMinus().Last(), tt.want.dim, Delta) - assert.InDelta(t, dmi.GetADX().Last(), tt.want.adx, Delta) + assert.InDelta(t, dmi.GetDIPlus().Last(0), tt.want.dip, Delta) + assert.InDelta(t, dmi.GetDIMinus().Last(0), tt.want.dim, Delta) + assert.InDelta(t, dmi.GetADX().Last(0), tt.want.adx, Delta) }) } diff --git a/pkg/indicator/drift.go b/pkg/indicator/drift.go index ac3df837aa..eb1969720f 100644 --- a/pkg/indicator/drift.go +++ b/pkg/indicator/drift.go @@ -51,7 +51,7 @@ func (inc *Drift) Update(value float64) { inc.chng.Update(chng) if inc.chng.Length() >= inc.Window { stdev := types.Stdev(inc.chng, inc.Window) - drift := inc.MA.Last() - stdev*stdev*0.5 + drift := inc.MA.Last(0) - stdev*stdev*0.5 inc.Values.Push(drift) } } @@ -74,7 +74,7 @@ func (inc *Drift) ZeroPoint() float64 { } else { return N2 }*/ - return inc.LastValue * math.Exp(window*(0.5*stdev*stdev)+chng-inc.MA.Last()*window) + return inc.LastValue * math.Exp(window*(0.5*stdev*stdev)+chng-inc.MA.Last(0)*window) } func (inc *Drift) Clone() (out *Drift) { @@ -96,17 +96,11 @@ func (inc *Drift) TestUpdate(value float64) *Drift { } func (inc *Drift) Index(i int) float64 { - if inc.Values == nil { - return 0 - } - return inc.Values.Index(i) + return inc.Last(i) } -func (inc *Drift) Last() float64 { - if inc.Values.Length() == 0 { - return 0 - } - return inc.Values.Last() +func (inc *Drift) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *Drift) Length() int { @@ -126,12 +120,12 @@ func (inc *Drift) CalculateAndUpdate(allKLines []types.KLine) { if inc.chng == nil { for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } diff --git a/pkg/indicator/emv.go b/pkg/indicator/emv.go index a3a761a5a1..a391a08e70 100644 --- a/pkg/indicator/emv.go +++ b/pkg/indicator/emv.go @@ -55,11 +55,12 @@ func (inc *EMV) Index(i int) float64 { return inc.Values.Index(i) } -func (inc *EMV) Last() float64 { +func (inc *EMV) Last(i int) float64 { if inc.Values == nil { return 0 } - return inc.Values.Last() + + return inc.Values.Last(i) } func (inc *EMV) Length() int { diff --git a/pkg/indicator/emv_test.go b/pkg/indicator/emv_test.go index 03b57a7604..0be0b517dc 100644 --- a/pkg/indicator/emv_test.go +++ b/pkg/indicator/emv_test.go @@ -16,7 +16,7 @@ func Test_EMV(t *testing.T) { } emv.Update(63.74, 62.63, 32178836) emv.Update(64.51, 63.85, 36461672) - assert.InDelta(t, 1.8, emv.Values.rawValues.Last(), Delta) + assert.InDelta(t, 1.8, emv.Values.rawValues.Last(0), Delta) emv.Update(64.57, 63.81, 51372680) emv.Update(64.31, 62.62, 42476356) emv.Update(63.43, 62.73, 29504176) @@ -30,5 +30,5 @@ func Test_EMV(t *testing.T) { emv.Update(65.25, 64.48, 37015388) emv.Update(64.69, 63.65, 40672116) emv.Update(64.26, 63.68, 35627200) - assert.InDelta(t, -0.03, emv.Last(), Delta) + assert.InDelta(t, -0.03, emv.Last(0), Delta) } diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index 533c27bd28..e05f30a3cb 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -50,24 +50,20 @@ func (inc *EWMA) Update(value float64) { inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] } - ema := (1-multiplier)*inc.Last() + multiplier*value + ema := (1-multiplier)*inc.Last(0) + multiplier*value inc.Values.Push(ema) } -func (inc *EWMA) Last() float64 { +func (inc *EWMA) Last(i int) float64 { if len(inc.Values) == 0 { return 0 } - return inc.Values[len(inc.Values)-1] + return inc.Values.Last(i) } func (inc *EWMA) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - - return inc.Values[len(inc.Values)-1-i] + return inc.Last(i) } func (inc *EWMA) Length() int { @@ -81,7 +77,7 @@ func (inc *EWMA) PushK(k types.KLine) { inc.Update(k.Close.Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func CalculateKLinesEMA(allKLines []types.KLine, priceF KLineValueMapper, window int) float64 { diff --git a/pkg/indicator/fisher.go b/pkg/indicator/fisher.go index fcd492c955..98198fc045 100644 --- a/pkg/indicator/fisher.go +++ b/pkg/indicator/fisher.go @@ -16,6 +16,7 @@ import ( // The Fisher Transform is calculated by taking the natural logarithm of the ratio of the security's current price to its moving average, // and then double-smoothing the result. This resulting line is called the Fisher Transform line, and can be plotted on the price chart // along with the security's price. +// //go:generate callbackgen -type FisherTransform type FisherTransform struct { types.SeriesBase @@ -60,18 +61,15 @@ func (inc *FisherTransform) Update(value float64) { } } -func (inc *FisherTransform) Last() float64 { +func (inc *FisherTransform) Last(i int) float64 { if inc.Values == nil { return 0.0 } - return inc.Values.Last() + return inc.Values.Last(i) } func (inc *FisherTransform) Index(i int) float64 { - if inc.Values == nil { - return 0.0 - } - return inc.Values.Index(i) + return inc.Last(i) } func (inc *FisherTransform) Length() int { diff --git a/pkg/indicator/float64updater.go b/pkg/indicator/float64updater.go index 3c3746ffef..6f4e69ddb9 100644 --- a/pkg/indicator/float64updater.go +++ b/pkg/indicator/float64updater.go @@ -23,16 +23,12 @@ func NewFloat64Series(v ...float64) Float64Series { return s } -func (f *Float64Series) Last() float64 { - return f.slice.Last() +func (f *Float64Series) Last(i int) float64 { + return f.slice.Last(i) } func (f *Float64Series) Index(i int) float64 { - length := len(f.slice) - if length == 0 || length-i-1 < 0 { - return 0 - } - return f.slice[length-i-1] + return f.slice.Last(i) } func (f *Float64Series) Length() int { diff --git a/pkg/indicator/ghfilter.go b/pkg/indicator/ghfilter.go index f5794d4d59..e5dea876f2 100644 --- a/pkg/indicator/ghfilter.go +++ b/pkg/indicator/ghfilter.go @@ -1,9 +1,10 @@ package indicator import ( + "math" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" - "math" ) // Refer: https://jamesgoulding.com/Research_II/Ehlers/Ehlers%20(Optimal%20Tracking%20Filters).doc @@ -39,16 +40,13 @@ func (inc *GHFilter) update(value, uncertainty float64) { lambda := inc.a / inc.b lambda2 := lambda * lambda alpha := (-lambda2 + math.Sqrt(lambda2*lambda2+16*lambda2)) / 8 - filtered := alpha*value + (1-alpha)*inc.Values.Last() + filtered := alpha*value + (1-alpha)*inc.Values.Last(0) inc.Values.Push(filtered) inc.lastMeasurement = value } func (inc *GHFilter) Index(i int) float64 { - if inc.Values == nil { - return 0.0 - } - return inc.Values.Index(i) + return inc.Last(i) } func (inc *GHFilter) Length() int { @@ -58,11 +56,11 @@ func (inc *GHFilter) Length() int { return inc.Values.Length() } -func (inc *GHFilter) Last() float64 { +func (inc *GHFilter) Last(i int) float64 { if inc.Values == nil { return 0.0 } - return inc.Values.Last() + return inc.Values.Last(i) } // interfaces implementation check diff --git a/pkg/indicator/ghfilter_test.go b/pkg/indicator/ghfilter_test.go index ac74399335..8494388590 100644 --- a/pkg/indicator/ghfilter_test.go +++ b/pkg/indicator/ghfilter_test.go @@ -6058,7 +6058,7 @@ func Test_GHFilter(t *testing.T) { for _, k := range klines { filter.PushK(k) } - got := filter.Last() + got := filter.Last(0) got = math.Trunc(got*100.0) / 100.0 if got != tt.want { t.Errorf("GHFilter.Last() = %v, want %v", got, tt.want) @@ -6125,10 +6125,10 @@ func Test_GHFilterEstimationAccurate(t *testing.T) { for i, k := range klines { // square error between last estimated state and current actual state if i > 0 { - filterDiff2Sum += klineSquareError(filter.Last(), k) - ewmaDiff2Sum += klineSquareError(ewma.Last(), k) - filterCloseDiff2Sum += closeSquareError(filter.Last(), k) - ewmaCloseDiff2Sum += closeSquareError(ewma.Last(), k) + filterDiff2Sum += klineSquareError(filter.Last(0), k) + ewmaDiff2Sum += klineSquareError(ewma.Last(0), k) + filterCloseDiff2Sum += closeSquareError(filter.Last(0), k) + ewmaCloseDiff2Sum += closeSquareError(ewma.Last(0), k) } // update estimations diff --git a/pkg/indicator/gma.go b/pkg/indicator/gma.go index 8b86733002..1fe0abb52b 100644 --- a/pkg/indicator/gma.go +++ b/pkg/indicator/gma.go @@ -22,18 +22,15 @@ type GMA struct { UpdateCallbacks []func(value float64) } -func (inc *GMA) Last() float64 { +func (inc *GMA) Last(i int) float64 { if inc.SMA == nil { return 0.0 } - return math.Exp(inc.SMA.Last()) + return math.Exp(inc.SMA.Last(i)) } func (inc *GMA) Index(i int) float64 { - if inc.SMA == nil { - return 0.0 - } - return math.Exp(inc.SMA.Index(i)) + return inc.Last(i) } func (inc *GMA) Length() int { diff --git a/pkg/indicator/gma_test.go b/pkg/indicator/gma_test.go index ab9740e094..0ad27f9776 100644 --- a/pkg/indicator/gma_test.go +++ b/pkg/indicator/gma_test.go @@ -51,10 +51,10 @@ func Test_GMA(t *testing.T) { for _, k := range tt.kLines { gma.PushK(k) } - assert.InDelta(t, tt.want, gma.Last(), Delta) + assert.InDelta(t, tt.want, gma.Last(0), Delta) assert.InDelta(t, tt.next, gma.Index(1), Delta) gma.Update(tt.update) - assert.InDelta(t, tt.updateResult, gma.Last(), Delta) + assert.InDelta(t, tt.updateResult, gma.Last(0), Delta) assert.Equal(t, tt.all, gma.Length()) }) } diff --git a/pkg/indicator/hull.go b/pkg/indicator/hull.go index 214d2620d3..7a432d39af 100644 --- a/pkg/indicator/hull.go +++ b/pkg/indicator/hull.go @@ -14,6 +14,7 @@ import ( // the weighted moving average of the input data using a weighting factor of W, where W is the square root of the length of the moving average. // The result is then double-smoothed by taking the weighted moving average of this result using a weighting factor of W/2. This final average // forms the HMA line, which can be used to make predictions about future price movements. +// //go:generate callbackgen -type HULL type HULL struct { types.SeriesBase @@ -36,14 +37,14 @@ func (inc *HULL) Update(value float64) { } inc.ma1.Update(value) inc.ma2.Update(value) - inc.result.Update(2*inc.ma1.Last() - inc.ma2.Last()) + inc.result.Update(2*inc.ma1.Last(0) - inc.ma2.Last(0)) } -func (inc *HULL) Last() float64 { +func (inc *HULL) Last(i int) float64 { if inc.result == nil { return 0 } - return inc.result.Last() + return inc.result.Index(i) } func (inc *HULL) Index(i int) float64 { @@ -66,5 +67,5 @@ func (inc *HULL) PushK(k types.KLine) { } inc.Update(k.Close.Float64()) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } diff --git a/pkg/indicator/hull_test.go b/pkg/indicator/hull_test.go index 857c8d30da..e1c077a739 100644 --- a/pkg/indicator/hull_test.go +++ b/pkg/indicator/hull_test.go @@ -51,7 +51,7 @@ func Test_HULL(t *testing.T) { hull.PushK(k) } - last := hull.Last() + last := hull.Last(0) assert.InDelta(t, tt.want, last, Delta) assert.InDelta(t, tt.next, hull.Index(1), Delta) assert.Equal(t, tt.all, hull.Length()) diff --git a/pkg/indicator/interface.go b/pkg/indicator/interface.go index d784f53087..51307440b3 100644 --- a/pkg/indicator/interface.go +++ b/pkg/indicator/interface.go @@ -25,7 +25,7 @@ type KLinePusher interface { // Simple is the simple indicator that only returns one float64 value type Simple interface { KLinePusher - Last() float64 + Last(int) float64 OnUpdate(f func(value float64)) } diff --git a/pkg/indicator/kalmanfilter.go b/pkg/indicator/kalmanfilter.go index 5d83457290..0b9ce2b0a0 100644 --- a/pkg/indicator/kalmanfilter.go +++ b/pkg/indicator/kalmanfilter.go @@ -1,9 +1,10 @@ package indicator import ( + "math" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" - "math" ) // Refer: https://www.kalmanfilter.net/kalman1d.html @@ -25,7 +26,7 @@ type KalmanFilter struct { func (inc *KalmanFilter) Update(value float64) { var measureMove = value if inc.measurements != nil { - measureMove = value - inc.measurements.Last() + measureMove = value - inc.measurements.Last(0) } inc.update(value, math.Abs(measureMove)) } @@ -46,7 +47,7 @@ func (inc *KalmanFilter) update(value, amp float64) { q := math.Sqrt(types.Mean(inc.amp2)) * float64(1+inc.AdditionalSmoothWindow) // update - lastPredict := inc.Values.Last() + lastPredict := inc.Values.Last(0) curState := value + (value - lastPredict) estimated := lastPredict + inc.k*(curState-lastPredict) @@ -57,24 +58,15 @@ func (inc *KalmanFilter) update(value, amp float64) { } func (inc *KalmanFilter) Index(i int) float64 { - if inc.Values == nil { - return 0.0 - } - return inc.Values.Index(i) + return inc.Last(i) } func (inc *KalmanFilter) Length() int { - if inc.Values == nil { - return 0 - } return inc.Values.Length() } -func (inc *KalmanFilter) Last() float64 { - if inc.Values == nil { - return 0.0 - } - return inc.Values.Last() +func (inc *KalmanFilter) Last(i int) float64 { + return inc.Values.Last(i) } // interfaces implementation check diff --git a/pkg/indicator/kalmanfilter_test.go b/pkg/indicator/kalmanfilter_test.go index 250656047e..c357748e4f 100644 --- a/pkg/indicator/kalmanfilter_test.go +++ b/pkg/indicator/kalmanfilter_test.go @@ -6065,7 +6065,7 @@ func Test_KalmanFilter(t *testing.T) { for _, k := range klines { filter.PushK(k) } - got := filter.Last() + got := filter.Last(0) got = math.Trunc(got*100.0) / 100.0 if got != tt.want { t.Errorf("KalmanFilter.Last() = %v, want %v", got, tt.want) @@ -6160,10 +6160,10 @@ func Test_KalmanFilterEstimationAccurate(t *testing.T) { for _, k := range klines { // square error between last estimated state and current actual state if ewma.Length() > 0 { - filterDiff2Sum += klineSquareError(filter.Last(), k) - ewmaDiff2Sum += klineSquareError(ewma.Last(), k) - filterCloseDiff2Sum += closeSquareError(filter.Last(), k) - ewmaCloseDiff2Sum += closeSquareError(ewma.Last(), k) + filterDiff2Sum += klineSquareError(filter.Last(0), k) + ewmaDiff2Sum += klineSquareError(ewma.Last(0), k) + filterCloseDiff2Sum += closeSquareError(filter.Last(0), k) + ewmaCloseDiff2Sum += closeSquareError(ewma.Last(0), k) numEstimations++ } diff --git a/pkg/indicator/klingeroscillator.go b/pkg/indicator/klingeroscillator.go index 198a5d29d7..0b5f703108 100644 --- a/pkg/indicator/klingeroscillator.go +++ b/pkg/indicator/klingeroscillator.go @@ -12,6 +12,7 @@ import ( // The Klinger Oscillator is calculated by taking the difference between a 34-period and 55-period moving average. // Usually the indicator is using together with a 9-period or 13-period of moving average as the signal line. // This indicator is often used to identify potential turning points in the market, as well as to confirm the strength of a trend. +// //go:generate callbackgen -type KlingerOscillator type KlingerOscillator struct { types.SeriesBase @@ -30,17 +31,11 @@ func (inc *KlingerOscillator) Length() int { return inc.Fast.Length() } -func (inc *KlingerOscillator) Last() float64 { +func (inc *KlingerOscillator) Last(i int) float64 { if inc.Fast == nil || inc.Slow == nil { return 0 } - return inc.Fast.Last() - inc.Slow.Last() -} -func (inc *KlingerOscillator) Index(i int) float64 { - if inc.Fast == nil || inc.Slow == nil { - return 0 - } - return inc.Fast.Index(i) - inc.Slow.Index(i) + return inc.Fast.Last(i) - inc.Slow.Last(i) } func (inc *KlingerOscillator) Update(high, low, cloze, volume float64) { diff --git a/pkg/indicator/line.go b/pkg/indicator/line.go index edb8276f16..e6227b30f8 100644 --- a/pkg/indicator/line.go +++ b/pkg/indicator/line.go @@ -38,12 +38,12 @@ func (l *Line) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(l.handleKLineWindowUpdate) } -func (l *Line) Last() float64 { - return (l.end-l.start)/float64(l.startIndex-l.endIndex)*float64(l.endIndex) + l.end +func (l *Line) Last(i int) float64 { + return (l.end-l.start)/float64(l.startIndex-l.endIndex)*float64(l.endIndex-i) + l.end } func (l *Line) Index(i int) float64 { - return (l.end-l.start)/float64(l.startIndex-l.endIndex)*float64(l.endIndex-i) + l.end + return l.Last(i) } func (l *Line) Length() int { diff --git a/pkg/indicator/linreg.go b/pkg/indicator/linreg.go index 3bb4606edc..f27e32c2a5 100644 --- a/pkg/indicator/linreg.go +++ b/pkg/indicator/linreg.go @@ -1,9 +1,10 @@ package indicator import ( - "github.com/sirupsen/logrus" "time" + "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -11,6 +12,7 @@ import ( var logLinReg = logrus.WithField("indicator", "LinReg") // LinReg is Linear Regression baseline +// //go:generate callbackgen -type LinReg type LinReg struct { types.SeriesBase @@ -28,11 +30,8 @@ type LinReg struct { } // Last slope of linear regression baseline -func (lr *LinReg) Last() float64 { - if lr.Values.Length() == 0 { - return 0.0 - } - return lr.Values.Last() +func (lr *LinReg) Last(i int) float64 { + return lr.Values.Last(i) } // LastRatio of slope to price @@ -40,16 +39,12 @@ func (lr *LinReg) LastRatio() float64 { if lr.ValueRatios.Length() == 0 { return 0.0 } - return lr.ValueRatios.Last() + return lr.ValueRatios.Last(0) } // Index returns the slope of specified index func (lr *LinReg) Index(i int) float64 { - if i >= lr.Values.Length() { - return 0.0 - } - - return lr.Values.Index(i) + return lr.Values.Last(i) } // IndexRatio returns the slope ratio @@ -58,7 +53,7 @@ func (lr *LinReg) IndexRatio(i int) float64 { return 0.0 } - return lr.ValueRatios.Index(i) + return lr.ValueRatios.Last(i) } // Length of the slope values @@ -99,9 +94,9 @@ func (lr *LinReg) Update(kline types.KLine) { endPrice := average - slope*sumX/length + slope startPrice := endPrice + slope*(length-1) lr.Values.Push((endPrice - startPrice) / (length - 1)) - lr.ValueRatios.Push(lr.Values.Last() / kline.GetClose().Float64()) + lr.ValueRatios.Push(lr.Values.Last(0) / kline.GetClose().Float64()) - logLinReg.Debugf("linear regression baseline slope: %f", lr.Last()) + logLinReg.Debugf("linear regression baseline slope: %f", lr.Last(0)) } func (lr *LinReg) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { diff --git a/pkg/indicator/low.go b/pkg/indicator/low.go index 77d0457ead..6f6d9468b4 100644 --- a/pkg/indicator/low.go +++ b/pkg/indicator/low.go @@ -33,5 +33,5 @@ func (inc *Low) PushK(k types.KLine) { inc.Update(k.Low.Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index 19818c70e8..2535e96814 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -59,14 +59,14 @@ func (inc *MACDLegacy) Update(x float64) { inc.slowEWMA.Update(x) // update MACD value, it's also the signal line - fast := inc.fastEWMA.Last() - slow := inc.slowEWMA.Last() + fast := inc.fastEWMA.Last(0) + slow := inc.slowEWMA.Last(0) macd := fast - slow inc.Values.Push(macd) // update signal line inc.signalLine.Update(macd) - signal := inc.signalLine.Last() + signal := inc.signalLine.Last(0) // update histogram histogram := macd - signal @@ -75,7 +75,7 @@ func (inc *MACDLegacy) Update(x float64) { inc.EmitUpdate(macd, signal, histogram) } -func (inc *MACDLegacy) Last() float64 { +func (inc *MACDLegacy) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } @@ -106,21 +106,12 @@ type MACDValues struct { *MACDLegacy } -func (inc *MACDValues) Last() float64 { - if len(inc.Values) == 0 { - return 0.0 - } - - return inc.Values[len(inc.Values)-1] +func (inc *MACDValues) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *MACDValues) Index(i int) float64 { - length := len(inc.Values) - if length == 0 || length-1-i < 0 { - return 0.0 - } - - return inc.Values[length-1+i] + return inc.Values.Last(i) } func (inc *MACDValues) Length() int { diff --git a/pkg/indicator/macd_test.go b/pkg/indicator/macd_test.go index c9bede48ad..eac9d54d9c 100644 --- a/pkg/indicator/macd_test.go +++ b/pkg/indicator/macd_test.go @@ -45,7 +45,7 @@ func Test_calculateMACD(t *testing.T) { macd.PushK(k) } - got := macd.Last() + got := macd.Last(0) diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { t.Errorf("calculateMACD() = %v, want %v", got, tt.want) diff --git a/pkg/indicator/obv.go b/pkg/indicator/obv.go index 4ca032445d..f7b768ce3b 100644 --- a/pkg/indicator/obv.go +++ b/pkg/indicator/obv.go @@ -40,24 +40,18 @@ func (inc *OBV) Update(price, volume float64) { } if volume < inc.PrePrice { - inc.Values.Push(inc.Last() - volume) + inc.Values.Push(inc.Last(0) - volume) } else { - inc.Values.Push(inc.Last() + volume) + inc.Values.Push(inc.Last(0) + volume) } } -func (inc *OBV) Last() float64 { - if len(inc.Values) == 0 { - return 0.0 - } - return inc.Values[len(inc.Values)-1] +func (inc *OBV) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *OBV) Index(i int) float64 { - if len(inc.Values)-i <= 0 { - return 0.0 - } - return inc.Values[len(inc.Values)-i-1] + return inc.Last(i) } var _ types.SeriesExtend = &OBV{} @@ -75,7 +69,7 @@ func (inc *OBV) CalculateAndUpdate(kLines []types.KLine) { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } diff --git a/pkg/indicator/pivot.go b/pkg/indicator/pivot.go index 8e027e4109..c32db25cd6 100644 --- a/pkg/indicator/pivot.go +++ b/pkg/indicator/pivot.go @@ -10,7 +10,6 @@ import ( "github.com/c9s/bbgo/pkg/types" ) - //go:generate callbackgen -type Pivot type Pivot struct { types.IntervalWindow @@ -105,12 +104,12 @@ func calculatePivot(klines []types.KLine, window int, valLow KLineValueMapper, v } pl := 0. - if lows.Min() == lows.Index(int(window/2.)-1) { + if lows.Min() == lows.Last(int(window/2.)-1) { pl = lows.Min() } ph := 0. - if highs.Max() == highs.Index(int(window/2.)-1) { + if highs.Max() == highs.Last(int(window/2.)-1) { ph = highs.Max() } diff --git a/pkg/indicator/pivothigh.go b/pkg/indicator/pivothigh.go index 8414c826cd..52fe7be0b4 100644 --- a/pkg/indicator/pivothigh.go +++ b/pkg/indicator/pivothigh.go @@ -24,12 +24,12 @@ func (inc *PivotHigh) Length() int { return inc.Values.Length() } -func (inc *PivotHigh) Last() float64 { +func (inc *PivotHigh) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } - return inc.Values.Last() + return inc.Values.Last(0) } func (inc *PivotHigh) Update(value float64) { @@ -60,5 +60,5 @@ func (inc *PivotHigh) PushK(k types.KLine) { inc.Update(k.High.Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } diff --git a/pkg/indicator/pivotlow.go b/pkg/indicator/pivotlow.go index c195c488c8..7bdbd58e67 100644 --- a/pkg/indicator/pivotlow.go +++ b/pkg/indicator/pivotlow.go @@ -24,12 +24,12 @@ func (inc *PivotLow) Length() int { return inc.Values.Length() } -func (inc *PivotLow) Last() float64 { +func (inc *PivotLow) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } - return inc.Values.Last() + return inc.Values.Last(0) } func (inc *PivotLow) Update(value float64) { @@ -60,7 +60,7 @@ func (inc *PivotLow) PushK(k types.KLine) { inc.Update(k.Low.Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func calculatePivotHigh(highs floats.Slice, left, right int) (float64, bool) { diff --git a/pkg/indicator/psar.go b/pkg/indicator/psar.go index e39e3fb2ce..5154c1b547 100644 --- a/pkg/indicator/psar.go +++ b/pkg/indicator/psar.go @@ -34,11 +34,11 @@ type PSAR struct { UpdateCallbacks []func(value float64) } -func (inc *PSAR) Last() float64 { +func (inc *PSAR) Last(int) float64 { if len(inc.Values) == 0 { return 0 } - return inc.Values.Last() + return inc.Values.Last(0) } func (inc *PSAR) Length() int { @@ -46,8 +46,8 @@ func (inc *PSAR) Length() int { } func (inc *PSAR) falling() bool { - up := inc.High.Last() - inc.High.Index(1) - dn := inc.Low.Index(1) - inc.Low.Last() + up := inc.High.Last(0) - inc.High.Index(1) + dn := inc.Low.Index(1) - inc.Low.Last(0) return (dn > up) && (dn > 0) } @@ -66,7 +66,7 @@ func (inc *PSAR) Update(high, low float64) { inc.High.Update(high) inc.Low.Update(low) if !isFirst { - ppsar := inc.Values.Last() + ppsar := inc.Values.Last(0) if inc.Falling { // falling formula psar := ppsar - inc.AF*(ppsar-inc.EP) h := inc.High.Shift(1).Highest(2) diff --git a/pkg/indicator/psar_test.go b/pkg/indicator/psar_test.go index 6914f7e547..a791a28cc5 100644 --- a/pkg/indicator/psar_test.go +++ b/pkg/indicator/psar_test.go @@ -36,5 +36,5 @@ func Test_PSAR(t *testing.T) { } assert.Equal(t, psar.Length(), 29) assert.Equal(t, psar.AF, 0.04) - assert.Equal(t, psar.Last(), 0.16) + assert.Equal(t, psar.Last(0), 0.16) } diff --git a/pkg/indicator/rma.go b/pkg/indicator/rma.go index 6785a54ec4..a06ad18af9 100644 --- a/pkg/indicator/rma.go +++ b/pkg/indicator/rma.go @@ -78,16 +78,12 @@ func (inc *RMA) Update(x float64) { } } -func (inc *RMA) Last() float64 { - return inc.Values.Last() +func (inc *RMA) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *RMA) Index(i int) float64 { - length := len(inc.Values) - if length == 0 || length-i-1 < 0 { - return 0 - } - return inc.Values[length-i-1] + return inc.Last(i) } func (inc *RMA) Length() int { @@ -116,7 +112,7 @@ func (inc *RMA) CalculateAndUpdate(kLines []types.KLine) { inc.PushK(last) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func (inc *RMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { diff --git a/pkg/indicator/rsi.go b/pkg/indicator/rsi.go index 8689abbdf6..acae219c34 100644 --- a/pkg/indicator/rsi.go +++ b/pkg/indicator/rsi.go @@ -65,19 +65,12 @@ func (inc *RSI) Update(price float64) { inc.PreviousAvgLoss = avgLoss } -func (inc *RSI) Last() float64 { - if len(inc.Values) == 0 { - return 0.0 - } - return inc.Values[len(inc.Values)-1] +func (inc *RSI) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *RSI) Index(i int) float64 { - length := len(inc.Values) - if length <= 0 || length-i-1 < 0 { - return 0.0 - } - return inc.Values[length-i-1] + return inc.Last(i) } func (inc *RSI) Length() int { @@ -99,7 +92,7 @@ func (inc *RSI) CalculateAndUpdate(kLines []types.KLine) { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index 508a0ce7eb..cc3bb1bc70 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -22,19 +22,12 @@ type SMA struct { UpdateCallbacks []func(value float64) } -func (inc *SMA) Last() float64 { - if inc.Values.Length() == 0 { - return 0.0 - } - return inc.Values.Last() +func (inc *SMA) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *SMA) Index(i int) float64 { - if i >= inc.Values.Length() { - return 0.0 - } - - return inc.Values.Index(i) + return inc.Last(i) } func (inc *SMA) Length() int { @@ -81,7 +74,7 @@ func (inc *SMA) PushK(k types.KLine) { inc.Update(k.Close.Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Values.Last()) + inc.EmitUpdate(inc.Values.Last(0)) } func (inc *SMA) LoadK(allKLines []types.KLine) { diff --git a/pkg/indicator/sma_test.go b/pkg/indicator/sma_test.go index a6ecc13241..c577af2153 100644 --- a/pkg/indicator/sma_test.go +++ b/pkg/indicator/sma_test.go @@ -58,10 +58,10 @@ func Test_SMA(t *testing.T) { sma.PushK(k) } - assert.InDelta(t, tt.want, sma.Last(), Delta) + assert.InDelta(t, tt.want, sma.Last(0), Delta) assert.InDelta(t, tt.next, sma.Index(1), Delta) sma.Update(tt.update) - assert.InDelta(t, tt.updateResult, sma.Last(), Delta) + assert.InDelta(t, tt.updateResult, sma.Last(0), Delta) assert.Equal(t, tt.all, sma.Length()) }) } diff --git a/pkg/indicator/ssf.go b/pkg/indicator/ssf.go index 9458e16e86..3a0f71bb35 100644 --- a/pkg/indicator/ssf.go +++ b/pkg/indicator/ssf.go @@ -50,9 +50,9 @@ func (inc *SSF) Update(value float64) { } result := inc.c1*value + - inc.c2*inc.Values.Index(0) + - inc.c3*inc.Values.Index(1) + - inc.c4*inc.Values.Index(2) + inc.c2*inc.Values.Last(0) + + inc.c3*inc.Values.Last(1) + + inc.c4*inc.Values.Last(2) inc.Values.Push(result) } else { // poles == 2 if inc.Values == nil { @@ -65,17 +65,18 @@ func (inc *SSF) Update(value float64) { inc.Values = floats.Slice{} } result := inc.c1*value + - inc.c2*inc.Values.Index(0) + - inc.c3*inc.Values.Index(1) + inc.c2*inc.Values.Last(0) + + inc.c3*inc.Values.Last(1) inc.Values.Push(result) } } +func (inc *SSF) Last(i int) float64 { + return inc.Values.Last(i) +} + func (inc *SSF) Index(i int) float64 { - if inc.Values == nil { - return 0.0 - } - return inc.Values.Index(i) + return inc.Last(i) } func (inc *SSF) Length() int { @@ -85,13 +86,6 @@ func (inc *SSF) Length() int { return inc.Values.Length() } -func (inc *SSF) Last() float64 { - if inc.Values == nil { - return 0.0 - } - return inc.Values.Last() -} - var _ types.SeriesExtend = &SSF{} func (inc *SSF) PushK(k types.KLine) { @@ -102,12 +96,12 @@ func (inc *SSF) CalculateAndUpdate(allKLines []types.KLine) { if inc.Values != nil { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) return } for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } diff --git a/pkg/indicator/ssf_test.go b/pkg/indicator/ssf_test.go index 253d722204..49f10c30f8 100644 --- a/pkg/indicator/ssf_test.go +++ b/pkg/indicator/ssf_test.go @@ -63,7 +63,7 @@ func Test_SSF(t *testing.T) { Poles: tt.poles, } ssf.CalculateAndUpdate(tt.kLines) - assert.InDelta(t, tt.want, ssf.Last(), Delta) + assert.InDelta(t, tt.want, ssf.Last(0), Delta) assert.InDelta(t, tt.next, ssf.Index(1), Delta) assert.Equal(t, tt.all, ssf.Length()) }) diff --git a/pkg/indicator/stddev.go b/pkg/indicator/stddev.go index a63aa89ef2..7e06c448c4 100644 --- a/pkg/indicator/stddev.go +++ b/pkg/indicator/stddev.go @@ -21,19 +21,12 @@ type StdDev struct { updateCallbacks []func(value float64) } -func (inc *StdDev) Last() float64 { - if inc.Values.Length() == 0 { - return 0.0 - } - return inc.Values.Last() +func (inc *StdDev) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *StdDev) Index(i int) float64 { - if i >= inc.Values.Length() { - return 0.0 - } - - return inc.Values.Index(i) + return inc.Last(i) } func (inc *StdDev) Length() int { @@ -76,7 +69,7 @@ func (inc *StdDev) CalculateAndUpdate(allKLines []types.KLine) { inc.PushK(last) } - inc.EmitUpdate(inc.Values.Last()) + inc.EmitUpdate(inc.Values.Last(0)) } func (inc *StdDev) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { diff --git a/pkg/indicator/supertrend.go b/pkg/indicator/supertrend.go index 4ab050dea8..bd891dd446 100644 --- a/pkg/indicator/supertrend.go +++ b/pkg/indicator/supertrend.go @@ -50,16 +50,12 @@ type Supertrend struct { UpdateCallbacks []func(value float64) } -func (inc *Supertrend) Last() float64 { - return inc.trendPrices.Last() +func (inc *Supertrend) Last(i int) float64 { + return inc.trendPrices.Last(i) } func (inc *Supertrend) Index(i int) float64 { - length := inc.Length() - if length == 0 || length-i-1 < 0 { - return 0 - } - return inc.trendPrices[length-i-1] + return inc.Last(i) } func (inc *Supertrend) Length() int { @@ -94,13 +90,13 @@ func (inc *Supertrend) Update(highPrice, lowPrice, closePrice float64) { src := (highPrice + lowPrice) / 2 // Update uptrend - inc.uptrendPrice = src - inc.AverageTrueRange.Last()*inc.ATRMultiplier + inc.uptrendPrice = src - inc.AverageTrueRange.Last(0)*inc.ATRMultiplier if inc.previousClosePrice > inc.previousUptrendPrice { inc.uptrendPrice = math.Max(inc.uptrendPrice, inc.previousUptrendPrice) } // Update downtrend - inc.downtrendPrice = src + inc.AverageTrueRange.Last()*inc.ATRMultiplier + inc.downtrendPrice = src + inc.AverageTrueRange.Last(0)*inc.ATRMultiplier if inc.previousClosePrice < inc.previousDowntrendPrice { inc.downtrendPrice = math.Min(inc.downtrendPrice, inc.previousDowntrendPrice) } @@ -115,7 +111,7 @@ func (inc *Supertrend) Update(highPrice, lowPrice, closePrice float64) { } // Update signal - if inc.AverageTrueRange.Last() <= 0 { + if inc.AverageTrueRange.Last(0) <= 0 { inc.tradeSignal = types.DirectionNone } else if inc.trend == types.DirectionUp && inc.previousTrend == types.DirectionDown { inc.tradeSignal = types.DirectionUp @@ -138,7 +134,7 @@ func (inc *Supertrend) Update(highPrice, lowPrice, closePrice float64) { logst.Debugf("Update supertrend result: closePrice: %v, uptrendPrice: %v, downtrendPrice: %v, trend: %v,"+ " tradeSignal: %v, AverageTrueRange.Last(): %v", inc.closePrice, inc.uptrendPrice, inc.downtrendPrice, - inc.trend, inc.tradeSignal, inc.AverageTrueRange.Last()) + inc.trend, inc.tradeSignal, inc.AverageTrueRange.Last(0)) } func (inc *Supertrend) GetSignal() types.Direction { @@ -152,12 +148,12 @@ func (inc *Supertrend) Direction() types.Direction { // LastSupertrendSupport return the current supertrend support func (inc *Supertrend) LastSupertrendSupport() float64 { - return inc.supportLine.Last() + return inc.supportLine.Last(0) } // LastSupertrendResistance return the current supertrend resistance func (inc *Supertrend) LastSupertrendResistance() float64 { - return inc.resistanceLine.Last() + return inc.resistanceLine.Last(0) } var _ types.SeriesExtend = &Supertrend{} @@ -169,7 +165,7 @@ func (inc *Supertrend) PushK(k types.KLine) { inc.Update(k.GetHigh().Float64(), k.GetLow().Float64(), k.GetClose().Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } @@ -192,7 +188,7 @@ func (inc *Supertrend) CalculateAndUpdate(kLines []types.KLine) { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } diff --git a/pkg/indicator/supertrendPivot.go b/pkg/indicator/supertrendPivot.go index 14b62925d9..85b68ac92f 100644 --- a/pkg/indicator/supertrendPivot.go +++ b/pkg/indicator/supertrendPivot.go @@ -50,16 +50,12 @@ type PivotSupertrend struct { UpdateCallbacks []func(value float64) } -func (inc *PivotSupertrend) Last() float64 { - return inc.trendPrices.Last() +func (inc *PivotSupertrend) Last(i int) float64 { + return inc.trendPrices.Last(i) } func (inc *PivotSupertrend) Index(i int) float64 { - length := inc.Length() - if length == 0 || length-i-1 < 0 { - return 0 - } - return inc.trendPrices[length-i-1] + return inc.Last(i) } func (inc *PivotSupertrend) Length() int { @@ -80,8 +76,8 @@ func (inc *PivotSupertrend) Update(highPrice, lowPrice, closePrice float64) { inc.trend = types.DirectionUp } - inc.previousPivotLow = inc.PivotLow.Last() - inc.previousPivotHigh = inc.PivotHigh.Last() + inc.previousPivotLow = inc.PivotLow.Last(0) + inc.previousPivotHigh = inc.PivotHigh.Last(0) // Update High / Low pivots inc.PivotLow.Update(lowPrice) @@ -101,9 +97,9 @@ func (inc *PivotSupertrend) Update(highPrice, lowPrice, closePrice float64) { // Initialize lastPp as soon as pivots are made if inc.lastPp == 0 || math.IsNaN(inc.lastPp) { if inc.PivotHigh.Length() > 0 { - inc.lastPp = inc.PivotHigh.Last() + inc.lastPp = inc.PivotHigh.Last(0) } else if inc.PivotLow.Length() > 0 { - inc.lastPp = inc.PivotLow.Last() + inc.lastPp = inc.PivotLow.Last(0) } else { inc.lastPp = math.NaN() return @@ -111,28 +107,28 @@ func (inc *PivotSupertrend) Update(highPrice, lowPrice, closePrice float64) { } // Set lastPp to the latest pivotPoint (only changed when new pivot is found) - if inc.PivotHigh.Last() != inc.previousPivotHigh { - inc.lastPp = inc.PivotHigh.Last() - } else if inc.PivotLow.Last() != inc.previousPivotLow { - inc.lastPp = inc.PivotLow.Last() + if inc.PivotHigh.Last(0) != inc.previousPivotHigh { + inc.lastPp = inc.PivotHigh.Last(0) + } else if inc.PivotLow.Last(0) != inc.previousPivotLow { + inc.lastPp = inc.PivotLow.Last(0) } // calculate the Center line using pivot points if inc.src == 0 || math.IsNaN(inc.src) { inc.src = inc.lastPp } else { - //weighted calculation + // weighted calculation inc.src = (inc.src*2 + inc.lastPp) / 3 } // Update uptrend - inc.uptrendPrice = inc.src - inc.AverageTrueRange.Last()*inc.ATRMultiplier + inc.uptrendPrice = inc.src - inc.AverageTrueRange.Last(0)*inc.ATRMultiplier if inc.previousClosePrice > inc.previousUptrendPrice { inc.uptrendPrice = math.Max(inc.uptrendPrice, inc.previousUptrendPrice) } // Update downtrend - inc.downtrendPrice = inc.src + inc.AverageTrueRange.Last()*inc.ATRMultiplier + inc.downtrendPrice = inc.src + inc.AverageTrueRange.Last(0)*inc.ATRMultiplier if inc.previousClosePrice < inc.previousDowntrendPrice { inc.downtrendPrice = math.Min(inc.downtrendPrice, inc.previousDowntrendPrice) } @@ -147,7 +143,7 @@ func (inc *PivotSupertrend) Update(highPrice, lowPrice, closePrice float64) { } // Update signal - if inc.AverageTrueRange.Last() <= 0 { + if inc.AverageTrueRange.Last(0) <= 0 { inc.tradeSignal = types.DirectionNone } else if inc.trend == types.DirectionUp && inc.previousTrend == types.DirectionDown { inc.tradeSignal = types.DirectionUp @@ -170,7 +166,7 @@ func (inc *PivotSupertrend) Update(highPrice, lowPrice, closePrice float64) { logpst.Debugf("Update pivot point supertrend result: closePrice: %v, uptrendPrice: %v, downtrendPrice: %v, trend: %v,"+ " tradeSignal: %v, AverageTrueRange.Last(): %v", inc.closePrice, inc.uptrendPrice, inc.downtrendPrice, - inc.trend, inc.tradeSignal, inc.AverageTrueRange.Last()) + inc.trend, inc.tradeSignal, inc.AverageTrueRange.Last(0)) } // GetSignal returns signal (Down, None or Up) @@ -185,12 +181,12 @@ func (inc *PivotSupertrend) Direction() types.Direction { // LastSupertrendSupport return the current supertrend support value func (inc *PivotSupertrend) LastSupertrendSupport() float64 { - return inc.supportLine.Last() + return inc.supportLine.Last(0) } // LastSupertrendResistance return the current supertrend resistance value func (inc *PivotSupertrend) LastSupertrendResistance() float64 { - return inc.resistanceLine.Last() + return inc.resistanceLine.Last(0) } var _ types.SeriesExtend = &PivotSupertrend{} @@ -202,7 +198,7 @@ func (inc *PivotSupertrend) PushK(k types.KLine) { inc.Update(k.GetHigh().Float64(), k.GetLow().Float64(), k.GetClose().Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func (inc *PivotSupertrend) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { diff --git a/pkg/indicator/tema.go b/pkg/indicator/tema.go index a3a7024092..f22a4c93a2 100644 --- a/pkg/indicator/tema.go +++ b/pkg/indicator/tema.go @@ -36,26 +36,20 @@ func (inc *TEMA) Update(value float64) { inc.A3 = &EWMA{IntervalWindow: inc.IntervalWindow} } inc.A1.Update(value) - a1 := inc.A1.Last() + a1 := inc.A1.Last(0) inc.A2.Update(a1) - a2 := inc.A2.Last() + a2 := inc.A2.Last(0) inc.A3.Update(a2) - a3 := inc.A3.Last() + a3 := inc.A3.Last(0) inc.Values.Push(3*a1 - 3*a2 + a3) } -func (inc *TEMA) Last() float64 { - if len(inc.Values) > 0 { - return inc.Values[len(inc.Values)-1] - } - return 0.0 +func (inc *TEMA) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *TEMA) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - return inc.Values[len(inc.Values)-i-1] + return inc.Last(i) } func (inc *TEMA) Length() int { @@ -72,12 +66,12 @@ func (inc *TEMA) CalculateAndUpdate(allKLines []types.KLine) { if inc.A1 == nil { for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } diff --git a/pkg/indicator/tema_test.go b/pkg/indicator/tema_test.go index b50c72ca75..3012f55391 100644 --- a/pkg/indicator/tema_test.go +++ b/pkg/indicator/tema_test.go @@ -47,7 +47,7 @@ func Test_TEMA(t *testing.T) { t.Run(tt.name, func(t *testing.T) { tema := TEMA{IntervalWindow: types.IntervalWindow{Window: 16}} tema.CalculateAndUpdate(tt.kLines) - last := tema.Last() + last := tema.Last(0) assert.InDelta(t, tt.want, last, Delta) assert.InDelta(t, tt.next, tema.Index(1), Delta) assert.Equal(t, tt.all, tema.Length()) diff --git a/pkg/indicator/till.go b/pkg/indicator/till.go index c7fca34dcb..828175dca5 100644 --- a/pkg/indicator/till.go +++ b/pkg/indicator/till.go @@ -57,28 +57,18 @@ func (inc *TILL) Update(value float64) { } inc.e1.Update(value) - inc.e2.Update(inc.e1.Last()) - inc.e3.Update(inc.e2.Last()) - inc.e4.Update(inc.e3.Last()) - inc.e5.Update(inc.e4.Last()) - inc.e6.Update(inc.e5.Last()) + inc.e2.Update(inc.e1.Last(0)) + inc.e3.Update(inc.e2.Last(0)) + inc.e4.Update(inc.e3.Last(0)) + inc.e5.Update(inc.e4.Last(0)) + inc.e6.Update(inc.e5.Last(0)) } -func (inc *TILL) Last() float64 { - if inc.e1 == nil || inc.e1.Length() == 0 { - return 0 - } - e3 := inc.e3.Last() - e4 := inc.e4.Last() - e5 := inc.e5.Last() - e6 := inc.e6.Last() - return inc.c1*e6 + inc.c2*e5 + inc.c3*e4 + inc.c4*e3 -} - -func (inc *TILL) Index(i int) float64 { +func (inc *TILL) Last(i int) float64 { if inc.e1 == nil || inc.e1.Length() <= i { return 0 } + e3 := inc.e3.Index(i) e4 := inc.e4.Index(i) e5 := inc.e5.Index(i) @@ -86,6 +76,10 @@ func (inc *TILL) Index(i int) float64 { return inc.c1*e6 + inc.c2*e5 + inc.c3*e4 + inc.c4*e3 } +func (inc *TILL) Index(i int) float64 { + return inc.Last(i) +} + func (inc *TILL) Length() int { if inc.e1 == nil { return 0 @@ -101,7 +95,7 @@ func (inc *TILL) PushK(k types.KLine) { } inc.Update(k.Close.Float64()) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func (inc *TILL) LoadK(allKLines []types.KLine) { diff --git a/pkg/indicator/till_test.go b/pkg/indicator/till_test.go index 1b03017b80..e78e6620a7 100644 --- a/pkg/indicator/till_test.go +++ b/pkg/indicator/till_test.go @@ -57,7 +57,7 @@ func Test_TILL(t *testing.T) { t.Run(tt.name, func(t *testing.T) { till := TILL{IntervalWindow: types.IntervalWindow{Window: 16}} till.CalculateAndUpdate(tt.kLines) - last := till.Last() + last := till.Last(0) assert.InDelta(t, tt.want, last, Delta) assert.InDelta(t, tt.next, till.Index(1), Delta) assert.Equal(t, tt.all, till.Length()) diff --git a/pkg/indicator/tma.go b/pkg/indicator/tma.go index 97c5997d57..fd1c416dba 100644 --- a/pkg/indicator/tma.go +++ b/pkg/indicator/tma.go @@ -6,6 +6,7 @@ import ( // Refer: Triangular Moving Average // Refer URL: https://ja.wikipedia.org/wiki/移動平均 +// //go:generate callbackgen -type TMA type TMA struct { types.SeriesBase @@ -24,21 +25,15 @@ func (inc *TMA) Update(value float64) { } inc.s1.Update(value) - inc.s2.Update(inc.s1.Last()) + inc.s2.Update(inc.s1.Last(0)) } -func (inc *TMA) Last() float64 { - if inc.s2 == nil { - return 0 - } - return inc.s2.Last() +func (inc *TMA) Last(i int) float64 { + return inc.s2.Last(i) } func (inc *TMA) Index(i int) float64 { - if inc.s2 == nil { - return 0 - } - return inc.s2.Index(i) + return inc.Last(i) } func (inc *TMA) Length() int { @@ -58,12 +53,12 @@ func (inc *TMA) CalculateAndUpdate(allKLines []types.KLine) { if inc.s1 == nil { for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } diff --git a/pkg/indicator/tsi.go b/pkg/indicator/tsi.go index 4cdb6c7508..fa4546aa74 100644 --- a/pkg/indicator/tsi.go +++ b/pkg/indicator/tsi.go @@ -9,6 +9,7 @@ import ( // Refer: True Strength Index // Refer URL: https://www.investopedia.com/terms/t/tsi.asp +// //go:generate callbackgen -type TSI type TSI struct { types.SeriesBase @@ -66,10 +67,10 @@ func (inc *TSI) Update(value float64) { apc := math.Abs(pc) inc.Apcs.Update(apc) - inc.Pcds.Update(inc.Pcs.Last()) - inc.Apcds.Update(inc.Apcs.Last()) + inc.Pcds.Update(inc.Pcs.Last(0)) + inc.Apcds.Update(inc.Apcs.Last(0)) - tsi := (inc.Pcds.Last() / inc.Apcds.Last()) * 100. + tsi := (inc.Pcds.Last(0) / inc.Apcds.Last(0)) * 100. inc.Values.Push(tsi) if inc.Values.Length() > MaxNumOfEWMA { inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] @@ -80,12 +81,12 @@ func (inc *TSI) Length() int { return inc.Values.Length() } -func (inc *TSI) Last() float64 { - return inc.Values.Last() +func (inc *TSI) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *TSI) Index(i int) float64 { - return inc.Values.Index(i) + return inc.Last(i) } func (inc *TSI) PushK(k types.KLine) { diff --git a/pkg/indicator/tsi_test.go b/pkg/indicator/tsi_test.go index dc7c9c3cb1..872cfbca00 100644 --- a/pkg/indicator/tsi_test.go +++ b/pkg/indicator/tsi_test.go @@ -32,5 +32,5 @@ func Test_TSI(t *testing.T) { } assert.Equal(t, tsi.Length(), 29) Delta := 1.5e-2 - assert.InDelta(t, tsi.Last(), 22.89, Delta) + assert.InDelta(t, tsi.Last(0), 22.89, Delta) } diff --git a/pkg/indicator/utBotAlert.go b/pkg/indicator/utBotAlert.go index 9d974ca421..4a9c5cda47 100644 --- a/pkg/indicator/utBotAlert.go +++ b/pkg/indicator/utBotAlert.go @@ -70,20 +70,20 @@ func (inc *UtBotAlert) Update(highPrice, lowPrice, closePrice float64) { // Update ATR inc.AverageTrueRange.Update(highPrice, lowPrice, closePrice) - nLoss := inc.AverageTrueRange.Last() * inc.KeyValue + nLoss := inc.AverageTrueRange.Last(0) * inc.KeyValue // xATRTrailingStop if inc.xATRTrailingStop.Length() == 0 { // For first run inc.xATRTrailingStop.Update(0) - } else if closePrice > inc.xATRTrailingStop.Index(1) && inc.previousClosePrice > inc.xATRTrailingStop.Index(1) { - inc.xATRTrailingStop.Update(math.Max(inc.xATRTrailingStop.Index(1), closePrice-nLoss)) + } else if closePrice > inc.xATRTrailingStop.Last(1) && inc.previousClosePrice > inc.xATRTrailingStop.Last(1) { + inc.xATRTrailingStop.Update(math.Max(inc.xATRTrailingStop.Last(1), closePrice-nLoss)) - } else if closePrice < inc.xATRTrailingStop.Index(1) && inc.previousClosePrice < inc.xATRTrailingStop.Index(1) { - inc.xATRTrailingStop.Update(math.Min(inc.xATRTrailingStop.Index(1), closePrice+nLoss)) + } else if closePrice < inc.xATRTrailingStop.Last(1) && inc.previousClosePrice < inc.xATRTrailingStop.Last(1) { + inc.xATRTrailingStop.Update(math.Min(inc.xATRTrailingStop.Last(1), closePrice+nLoss)) - } else if closePrice > inc.xATRTrailingStop.Index(1) { + } else if closePrice > inc.xATRTrailingStop.Last(1) { inc.xATRTrailingStop.Update(closePrice - nLoss) } else { @@ -91,19 +91,19 @@ func (inc *UtBotAlert) Update(highPrice, lowPrice, closePrice float64) { } // pos - if inc.previousClosePrice < inc.xATRTrailingStop.Index(1) && closePrice > inc.xATRTrailingStop.Index(1) { + if inc.previousClosePrice < inc.xATRTrailingStop.Last(1) && closePrice > inc.xATRTrailingStop.Last(1) { inc.pos = types.DirectionUp - } else if inc.previousClosePrice > inc.xATRTrailingStop.Index(1) && closePrice < inc.xATRTrailingStop.Index(1) { + } else if inc.previousClosePrice > inc.xATRTrailingStop.Last(1) && closePrice < inc.xATRTrailingStop.Last(1) { inc.pos = types.DirectionDown } else { inc.pos = inc.previousPos } - above := closePrice > inc.xATRTrailingStop.Last() && inc.previousClosePrice < inc.xATRTrailingStop.Index(1) - below := closePrice < inc.xATRTrailingStop.Last() && inc.previousClosePrice > inc.xATRTrailingStop.Index(1) + above := closePrice > inc.xATRTrailingStop.Last(0) && inc.previousClosePrice < inc.xATRTrailingStop.Last(1) + below := closePrice < inc.xATRTrailingStop.Last(0) && inc.previousClosePrice > inc.xATRTrailingStop.Last(1) - buy := closePrice > inc.xATRTrailingStop.Last() && above // buy - sell := closePrice < inc.xATRTrailingStop.Last() && below // sell + buy := closePrice > inc.xATRTrailingStop.Last(0) && above // buy + sell := closePrice < inc.xATRTrailingStop.Last(0) && below // sell inc.buyValue.Push(buy) inc.sellValue.Push(sell) diff --git a/pkg/indicator/v2_atr_test.go b/pkg/indicator/v2_atr_test.go index 1dd49501a2..bacbc78612 100644 --- a/pkg/indicator/v2_atr_test.go +++ b/pkg/indicator/v2_atr_test.go @@ -71,7 +71,7 @@ func Test_ATR2(t *testing.T) { stream.EmitKLineClosed(k) } - got := atr.Last() + got := atr.Last(0) diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { t.Errorf("ATR2() = %v, want %v", got, tt.want) diff --git a/pkg/indicator/v2_ewma.go b/pkg/indicator/v2_ewma.go index e452aaedae..afd0752ad7 100644 --- a/pkg/indicator/v2_ewma.go +++ b/pkg/indicator/v2_ewma.go @@ -30,7 +30,7 @@ func (s *EWMAStream) calculateAndPush(v float64) { } func (s *EWMAStream) calculate(v float64) float64 { - last := s.slice.Last() + last := s.slice.Last(0) m := s.multiplier return (1.0-m)*last + m*v } diff --git a/pkg/indicator/v2_rsi.go b/pkg/indicator/v2_rsi.go index 16732f2bdc..583e68bafe 100644 --- a/pkg/indicator/v2_rsi.go +++ b/pkg/indicator/v2_rsi.go @@ -32,8 +32,8 @@ func (s *RSIStream) calculate(_ float64) float64 { var sourceLen = s.source.Length() var limit = min(s.window, sourceLen) for i := 0; i < limit; i++ { - value := s.source.Index(i) - prev := s.source.Index(i + 1) + value := s.source.Last(i) + prev := s.source.Last(i + 1) change := value - prev if change >= 0 { gainSum += change diff --git a/pkg/indicator/v2_tr_test.go b/pkg/indicator/v2_tr_test.go index 38195f55d7..c6b46332ab 100644 --- a/pkg/indicator/v2_tr_test.go +++ b/pkg/indicator/v2_tr_test.go @@ -72,7 +72,7 @@ func Test_TR_and_RMA(t *testing.T) { stream.EmitKLineClosed(k) } - got := rma.Last() + got := rma.Last(0) diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { t.Errorf("RMA(TR()) = %v, want %v", got, tt.want) diff --git a/pkg/indicator/vidya.go b/pkg/indicator/vidya.go index 311e47f4fb..5f21be1c0f 100644 --- a/pkg/indicator/vidya.go +++ b/pkg/indicator/vidya.go @@ -58,18 +58,18 @@ func (inc *VIDYA) Update(value float64) { change := types.Change(&inc.input) CMO := math.Abs(types.Sum(change, inc.Window) / types.Sum(types.Abs(change), inc.Window)) alpha := 2. / float64(inc.Window+1) - inc.Values.Push(value*alpha*CMO + inc.Values.Last()*(1.-alpha*CMO)) + inc.Values.Push(value*alpha*CMO + inc.Values.Last(0)*(1.-alpha*CMO)) if inc.Values.Length() > MaxNumOfEWMA { inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] } } -func (inc *VIDYA) Last() float64 { - return inc.Values.Last() +func (inc *VIDYA) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *VIDYA) Index(i int) float64 { - return inc.Values.Index(i) + return inc.Last(i) } func (inc *VIDYA) Length() int { @@ -86,12 +86,12 @@ func (inc *VIDYA) CalculateAndUpdate(allKLines []types.KLine) { if inc.input.Length() == 0 { for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } diff --git a/pkg/indicator/vidya_test.go b/pkg/indicator/vidya_test.go index 8ac1df255d..164173441d 100644 --- a/pkg/indicator/vidya_test.go +++ b/pkg/indicator/vidya_test.go @@ -10,10 +10,10 @@ import ( func Test_VIDYA(t *testing.T) { vidya := &VIDYA{IntervalWindow: types.IntervalWindow{Window: 16}} vidya.Update(1) - assert.Equal(t, vidya.Last(), 1.) + assert.Equal(t, vidya.Last(0), 1.) vidya.Update(2) newV := 2./17.*2. + 1.*(1.-2./17.) - assert.Equal(t, vidya.Last(), newV) + assert.Equal(t, vidya.Last(0), newV) vidya.Update(1) - assert.Equal(t, vidya.Last(), vidya.Index(1)) + assert.Equal(t, vidya.Last(0), vidya.Index(1)) } diff --git a/pkg/indicator/volatility.go b/pkg/indicator/volatility.go index 6b3dcd7cb1..3c5efce962 100644 --- a/pkg/indicator/volatility.go +++ b/pkg/indicator/volatility.go @@ -26,18 +26,12 @@ type Volatility struct { UpdateCallbacks []func(value float64) } -func (inc *Volatility) Last() float64 { - if len(inc.Values) == 0 { - return 0.0 - } - return inc.Values[len(inc.Values)-1] +func (inc *Volatility) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *Volatility) Index(i int) float64 { - if len(inc.Values)-i <= 0 { - return 0.0 - } - return inc.Values[len(inc.Values)-i-1] + return inc.Last(i) } func (inc *Volatility) Length() int { diff --git a/pkg/indicator/vwap.go b/pkg/indicator/vwap.go index 805def8285..3f2e08085c 100644 --- a/pkg/indicator/vwap.go +++ b/pkg/indicator/vwap.go @@ -56,20 +56,12 @@ func (inc *VWAP) Update(price, volume float64) { inc.Values.Push(vwap) } -func (inc *VWAP) Last() float64 { - if len(inc.Values) == 0 { - return 0.0 - } - return inc.Values[len(inc.Values)-1] +func (inc *VWAP) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *VWAP) Index(i int) float64 { - length := len(inc.Values) - if length == 0 || length-i-1 < 0 { - return 0 - } - - return inc.Values[length-i-1] + return inc.Last(i) } func (inc *VWAP) Length() int { @@ -91,7 +83,7 @@ func (inc *VWAP) CalculateAndUpdate(allKLines []types.KLine) { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) inc.EndTime = allKLines[len(allKLines)-1].EndTime.Time() } @@ -112,5 +104,5 @@ func calculateVWAP(klines []types.KLine, priceF KLineValueMapper, window int) fl for _, k := range klines { vwap.Update(priceF(k), k.Volume.Float64()) } - return vwap.Last() + return vwap.Last(0) } diff --git a/pkg/indicator/vwma.go b/pkg/indicator/vwma.go index a8a328e20f..ed383531bc 100644 --- a/pkg/indicator/vwma.go +++ b/pkg/indicator/vwma.go @@ -14,7 +14,7 @@ import ( // VWMA = SMA(pv, window) / SMA(volumes, window) // // Volume Weighted Moving Average -//- https://www.motivewave.com/studies/volume_weighted_moving_average.htm +// - https://www.motivewave.com/studies/volume_weighted_moving_average.htm // // The Volume Weighted Moving Average (VWMA) is a technical analysis indicator that is used to smooth price data and reduce the lag // associated with traditional moving averages. It is calculated by taking the weighted moving average of the input data, with the @@ -36,19 +36,12 @@ type VWMA struct { updateCallbacks []func(value float64) } -func (inc *VWMA) Last() float64 { - if len(inc.Values) == 0 { - return 0.0 - } - return inc.Values[len(inc.Values)-1] +func (inc *VWMA) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *VWMA) Index(i int) float64 { - length := len(inc.Values) - if length == 0 || length-i-1 < 0 { - return 0 - } - return inc.Values[length-i-1] + return inc.Last(i) } func (inc *VWMA) Length() int { @@ -70,8 +63,8 @@ func (inc *VWMA) Update(price, volume float64) { inc.PriceVolumeSMA.Update(price * volume) inc.VolumeSMA.Update(volume) - pv := inc.PriceVolumeSMA.Last() - v := inc.VolumeSMA.Last() + pv := inc.PriceVolumeSMA.Last(0) + v := inc.VolumeSMA.Last(0) vwma := pv / v inc.Values.Push(vwma) } @@ -104,7 +97,7 @@ func (inc *VWMA) CalculateAndUpdate(allKLines []types.KLine) { } inc.EndTime = last.EndTime.Time() - inc.EmitUpdate(inc.Values.Last()) + inc.EmitUpdate(inc.Values.Last(0)) } func (inc *VWMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { diff --git a/pkg/indicator/wdrift.go b/pkg/indicator/wdrift.go index 61060a7470..d9b6675a06 100644 --- a/pkg/indicator/wdrift.go +++ b/pkg/indicator/wdrift.go @@ -10,6 +10,7 @@ import ( // Refer: https://tradingview.com/script/aDymGrFx-Drift-Study-Inspired-by-Monte-Carlo-Simulations-with-BM-KL/ // Brownian Motion's drift factor // could be used in Monte Carlo Simulations +// //go:generate callbackgen -type WeightedDrift type WeightedDrift struct { types.SeriesBase @@ -54,7 +55,7 @@ func (inc *WeightedDrift) Update(value float64, weight float64) { } if inc.chng.Length() >= inc.Window { stdev := types.Stdev(inc.chng, inc.Window) - drift := inc.MA.Last() - stdev*stdev*0.5 + drift := inc.MA.Last(0) - stdev*stdev*0.5 inc.Values.Push(drift) } } @@ -77,7 +78,7 @@ func (inc *WeightedDrift) ZeroPoint() float64 { } else { return N2 }*/ - return inc.LastValue * math.Exp(window*(0.5*stdev*stdev)+chng-inc.MA.Last()*window) + return inc.LastValue * math.Exp(window*(0.5*stdev*stdev)+chng-inc.MA.Last(0)*window) } func (inc *WeightedDrift) Clone() (out *WeightedDrift) { @@ -100,17 +101,11 @@ func (inc *WeightedDrift) TestUpdate(value float64, weight float64) *WeightedDri } func (inc *WeightedDrift) Index(i int) float64 { - if inc.Values == nil { - return 0 - } - return inc.Values.Index(i) + return inc.Last(i) } -func (inc *WeightedDrift) Last() float64 { - if inc.Values.Length() == 0 { - return 0 - } - return inc.Values.Last() +func (inc *WeightedDrift) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *WeightedDrift) Length() int { @@ -130,12 +125,12 @@ func (inc *WeightedDrift) CalculateAndUpdate(allKLines []types.KLine) { if inc.chng == nil { for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } diff --git a/pkg/indicator/wwma.go b/pkg/indicator/wwma.go index 0dbc67ca1e..f8fe954aa1 100644 --- a/pkg/indicator/wwma.go +++ b/pkg/indicator/wwma.go @@ -33,25 +33,17 @@ func (inc *WWMA) Update(value float64) { inc.Values = inc.Values[MaxNumOfWWMATruncateSize-1:] } - last := inc.Last() + last := inc.Last(0) wma := last + (value-last)/float64(inc.Window) inc.Values.Push(wma) } -func (inc *WWMA) Last() float64 { - if len(inc.Values) == 0 { - return 0 - } - - return inc.Values[len(inc.Values)-1] +func (inc *WWMA) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *WWMA) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - - return inc.Values[len(inc.Values)-1-i] + return inc.Last(i) } func (inc *WWMA) Length() int { @@ -76,7 +68,7 @@ func (inc *WWMA) CalculateAndUpdate(allKLines []types.KLine) { if doable { inc.PushK(k) inc.LastOpenTime = k.StartTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } } diff --git a/pkg/indicator/zlema.go b/pkg/indicator/zlema.go index 5c7279f619..182cf29eb5 100644 --- a/pkg/indicator/zlema.go +++ b/pkg/indicator/zlema.go @@ -27,17 +27,14 @@ type ZLEMA struct { } func (inc *ZLEMA) Index(i int) float64 { - if inc.zlema == nil { - return 0 - } - return inc.zlema.Index(i) + return inc.Last(i) } -func (inc *ZLEMA) Last() float64 { +func (inc *ZLEMA) Last(i int) float64 { if inc.zlema == nil { return 0 } - return inc.zlema.Last() + return inc.zlema.Last(i) } func (inc *ZLEMA) Length() int { @@ -74,12 +71,12 @@ func (inc *ZLEMA) CalculateAndUpdate(allKLines []types.KLine) { if inc.zlema == nil { for _, k := range allKLines { inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } diff --git a/pkg/indicator/zlema_test.go b/pkg/indicator/zlema_test.go index 4560f4276b..6f2ff31946 100644 --- a/pkg/indicator/zlema_test.go +++ b/pkg/indicator/zlema_test.go @@ -46,7 +46,7 @@ func Test_ZLEMA(t *testing.T) { t.Run(tt.name, func(t *testing.T) { zlema := ZLEMA{IntervalWindow: types.IntervalWindow{Window: 16}} zlema.CalculateAndUpdate(tt.kLines) - last := zlema.Last() + last := zlema.Last(0) assert.InDelta(t, tt.want, last, Delta) assert.InDelta(t, tt.next, zlema.Index(1), Delta) assert.Equal(t, tt.all, zlema.Length()) diff --git a/pkg/risk/dynamicrisk/dynamic_exposure.go b/pkg/risk/dynamicrisk/dynamic_exposure.go index 71eb121557..415a287a3b 100644 --- a/pkg/risk/dynamicrisk/dynamic_exposure.go +++ b/pkg/risk/dynamicrisk/dynamic_exposure.go @@ -59,9 +59,9 @@ func (d *DynamicExposureBollBand) initialize(symbol string, session *bbgo.Exchan // getMaxExposure returns the max exposure func (d *DynamicExposureBollBand) getMaxExposure(price float64, trend types.Direction) (fixedpoint.Value, error) { - downBand := d.dynamicExposureBollBand.DownBand.Last() - upBand := d.dynamicExposureBollBand.UpBand.Last() - sma := d.dynamicExposureBollBand.SMA.Last() + downBand := d.dynamicExposureBollBand.DownBand.Last(0) + upBand := d.dynamicExposureBollBand.UpBand.Last(0) + sma := d.dynamicExposureBollBand.SMA.Last(0) log.Infof("dynamicExposureBollBand bollinger band: up %f sma %f down %f", upBand, sma, downBand) bandPercentage := 0.0 diff --git a/pkg/risk/dynamicrisk/dynamic_spread.go b/pkg/risk/dynamicrisk/dynamic_spread.go index 2b5cbbeb89..f6fab9cb6f 100644 --- a/pkg/risk/dynamicrisk/dynamic_spread.go +++ b/pkg/risk/dynamicrisk/dynamic_spread.go @@ -114,7 +114,7 @@ func (ds *DynamicAmpSpread) update(kline types.KLine) { func (ds *DynamicAmpSpread) getAskSpread() (askSpread float64, err error) { if ds.AskSpreadScale != nil && ds.dynamicAskSpread.Length() >= ds.Window { - askSpread, err = ds.AskSpreadScale.Scale(ds.dynamicAskSpread.Last()) + askSpread, err = ds.AskSpreadScale.Scale(ds.dynamicAskSpread.Last(0)) if err != nil { log.WithError(err).Errorf("can not calculate dynamicAskSpread") return 0, err @@ -128,7 +128,7 @@ func (ds *DynamicAmpSpread) getAskSpread() (askSpread float64, err error) { func (ds *DynamicAmpSpread) getBidSpread() (bidSpread float64, err error) { if ds.BidSpreadScale != nil && ds.dynamicBidSpread.Length() >= ds.Window { - bidSpread, err = ds.BidSpreadScale.Scale(ds.dynamicBidSpread.Last()) + bidSpread, err = ds.BidSpreadScale.Scale(ds.dynamicBidSpread.Last(0)) if err != nil { log.WithError(err).Errorf("can not calculate dynamicBidSpread") return 0, err @@ -224,12 +224,12 @@ func (ds *DynamicSpreadBollWidthRatio) getWeightedBBWidthRatio(positiveSigmoid b // - To ask spread, the higher neutral band get greater ratio // - To bid spread, the lower neutral band get greater ratio - defaultMid := ds.defaultBoll.SMA.Last() - defaultUpper := ds.defaultBoll.UpBand.Last() - defaultLower := ds.defaultBoll.DownBand.Last() + defaultMid := ds.defaultBoll.SMA.Last(0) + defaultUpper := ds.defaultBoll.UpBand.Last(0) + defaultLower := ds.defaultBoll.DownBand.Last(0) defaultWidth := defaultUpper - defaultLower - neutralUpper := ds.neutralBoll.UpBand.Last() - neutralLower := ds.neutralBoll.DownBand.Last() + neutralUpper := ds.neutralBoll.UpBand.Last(0) + neutralLower := ds.neutralBoll.DownBand.Last(0) factor := defaultWidth / ds.Sensitivity var weightedUpper, weightedLower, weightedDivUpper, weightedDivLower float64 if positiveSigmoid { diff --git a/pkg/strategy/audacitymaker/orderflow.go b/pkg/strategy/audacitymaker/orderflow.go index b8b0d80c7d..94de688805 100644 --- a/pkg/strategy/audacitymaker/orderflow.go +++ b/pkg/strategy/audacitymaker/orderflow.go @@ -92,7 +92,7 @@ func (s *PerTrade) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener // min-max scaling ofsMax := orderFlowSize.Tail(100).Max() ofsMin := orderFlowSize.Tail(100).Min() - ofsMinMax := (orderFlowSize.Last() - ofsMin) / (ofsMax - ofsMin) + ofsMinMax := (orderFlowSize.Last(0) - ofsMin) / (ofsMax - ofsMin) // preserves temporal dependency via polar encoded angles orderFlowSizeMinMax.Push(ofsMinMax) } @@ -102,7 +102,7 @@ func (s *PerTrade) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener // min-max scaling ofnMax := orderFlowNumber.Tail(100).Max() ofnMin := orderFlowNumber.Tail(100).Min() - ofnMinMax := (orderFlowNumber.Last() - ofnMin) / (ofnMax - ofnMin) + ofnMinMax := (orderFlowNumber.Last(0) - ofnMin) / (ofnMax - ofnMin) // preserves temporal dependency via polar encoded angles orderFlowNumberMinMax.Push(ofnMinMax) } @@ -167,9 +167,9 @@ func (s *PerTrade) placeTrade(ctx context.Context, side types.SideType, quantity func outlier(fs floats.Slice, multiplier float64) int { stddev := stat.StdDev(fs, nil) - if fs.Last() > fs.Mean()+multiplier*stddev { + if fs.Last(0) > fs.Mean()+multiplier*stddev { return 1 - } else if fs.Last() < fs.Mean()-multiplier*stddev { + } else if fs.Last(0) < fs.Mean()-multiplier*stddev { return -1 } return 0 diff --git a/pkg/strategy/bollgrid/strategy.go b/pkg/strategy/bollgrid/strategy.go index 61be952022..2474c7b9cc 100644 --- a/pkg/strategy/bollgrid/strategy.go +++ b/pkg/strategy/bollgrid/strategy.go @@ -135,7 +135,7 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types ema99 := s.StandardIndicatorSet.EWMA(types.IntervalWindow{Interval: s.Interval, Window: 99}) ema25 := s.StandardIndicatorSet.EWMA(types.IntervalWindow{Interval: s.Interval, Window: 25}) ema7 := s.StandardIndicatorSet.EWMA(types.IntervalWindow{Interval: s.Interval, Window: 7}) - if ema7.Last() > ema25.Last()*1.001 && ema25.Last() > ema99.Last()*1.0005 { + if ema7.Last(0) > ema25.Last(0)*1.001 && ema25.Last(0) > ema99.Last(0)*1.0005 { log.Infof("all ema lines trend up, skip buy") return nil, nil } @@ -202,7 +202,7 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type ema99 := s.StandardIndicatorSet.EWMA(types.IntervalWindow{Interval: s.Interval, Window: 99}) ema25 := s.StandardIndicatorSet.EWMA(types.IntervalWindow{Interval: s.Interval, Window: 25}) ema7 := s.StandardIndicatorSet.EWMA(types.IntervalWindow{Interval: s.Interval, Window: 7}) - if ema7.Last() < ema25.Last()*(1-0.004) && ema25.Last() < ema99.Last()*(1-0.0005) { + if ema7.Last(0) < ema25.Last(0)*(1-0.004) && ema25.Last(0) < ema99.Last(0)*(1-0.0005) { log.Infof("all ema lines trend down, skip sell") return nil, nil } diff --git a/pkg/strategy/bollmaker/dynamic_spread.go b/pkg/strategy/bollmaker/dynamic_spread.go index f7583d1105..51785f3df9 100644 --- a/pkg/strategy/bollmaker/dynamic_spread.go +++ b/pkg/strategy/bollmaker/dynamic_spread.go @@ -126,7 +126,7 @@ func (ds *DynamicSpreadAmpSettings) update(kline types.KLine) { func (ds *DynamicSpreadAmpSettings) getAskSpread() (askSpread float64, err error) { if ds.AskSpreadScale != nil && ds.dynamicAskSpread.Length() >= ds.Window { - askSpread, err = ds.AskSpreadScale.Scale(ds.dynamicAskSpread.Last()) + askSpread, err = ds.AskSpreadScale.Scale(ds.dynamicAskSpread.Last(0)) if err != nil { log.WithError(err).Errorf("can not calculate dynamicAskSpread") return 0, err @@ -140,7 +140,7 @@ func (ds *DynamicSpreadAmpSettings) getAskSpread() (askSpread float64, err error func (ds *DynamicSpreadAmpSettings) getBidSpread() (bidSpread float64, err error) { if ds.BidSpreadScale != nil && ds.dynamicBidSpread.Length() >= ds.Window { - bidSpread, err = ds.BidSpreadScale.Scale(ds.dynamicBidSpread.Last()) + bidSpread, err = ds.BidSpreadScale.Scale(ds.dynamicBidSpread.Last(0)) if err != nil { log.WithError(err).Errorf("can not calculate dynamicBidSpread") return 0, err @@ -224,12 +224,12 @@ func (ds *DynamicSpreadBollWidthRatioSettings) getWeightedBBWidthRatio(positiveS // - To ask spread, the higher neutral band get greater ratio // - To bid spread, the lower neutral band get greater ratio - defaultMid := ds.defaultBoll.SMA.Last() - defaultUpper := ds.defaultBoll.UpBand.Last() - defaultLower := ds.defaultBoll.DownBand.Last() + defaultMid := ds.defaultBoll.SMA.Last(0) + defaultUpper := ds.defaultBoll.UpBand.Last(0) + defaultLower := ds.defaultBoll.DownBand.Last(0) defaultWidth := defaultUpper - defaultLower - neutralUpper := ds.neutralBoll.UpBand.Last() - neutralLower := ds.neutralBoll.DownBand.Last() + neutralUpper := ds.neutralBoll.UpBand.Last(0) + neutralLower := ds.neutralBoll.DownBand.Last(0) factor := defaultWidth / ds.Sensitivity var weightedUpper, weightedLower, weightedDivUpper, weightedDivLower float64 if positiveSigmoid { diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 92b3fc4af7..efd1742d21 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -278,9 +278,9 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] - downBand := s.defaultBoll.DownBand.Last() - upBand := s.defaultBoll.UpBand.Last() - sma := s.defaultBoll.SMA.Last() + downBand := s.defaultBoll.DownBand.Last(0) + upBand := s.defaultBoll.UpBand.Last(0) + sma := s.defaultBoll.SMA.Last(0) log.Infof("%s bollinger band: up %f sma %f down %f", s.Symbol, upBand, sma, downBand) bandPercentage := calculateBandPercentage(upBand, downBand, sma, midPrice.Float64()) @@ -351,7 +351,7 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k // WHEN: price breaks the upper band (price > window 2) == strongUpTrend // THEN: we apply strongUpTrend skew if s.TradeInBand { - if !inBetween(midPrice.Float64(), s.neutralBoll.DownBand.Last(), s.neutralBoll.UpBand.Last()) { + if !inBetween(midPrice.Float64(), s.neutralBoll.DownBand.Last(0), s.neutralBoll.UpBand.Last(0)) { log.Infof("tradeInBand is set, skip placing orders when the price is outside of the band") return } @@ -410,7 +410,7 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k canSell = false } - if s.BuyBelowNeutralSMA && midPrice.Float64() > s.neutralBoll.SMA.Last() { + if s.BuyBelowNeutralSMA && midPrice.Float64() > s.neutralBoll.SMA.Last(0) { canBuy = false } diff --git a/pkg/strategy/bollmaker/trend.go b/pkg/strategy/bollmaker/trend.go index 33167967b5..4c0cb7fcdf 100644 --- a/pkg/strategy/bollmaker/trend.go +++ b/pkg/strategy/bollmaker/trend.go @@ -12,7 +12,7 @@ const ( ) func detectPriceTrend(inc *indicator.BOLL, price float64) PriceTrend { - if inBetween(price, inc.DownBand.Last(), inc.UpBand.Last()) { + if inBetween(price, inc.DownBand.Last(0), inc.UpBand.Last(0)) { return NeutralTrend } diff --git a/pkg/strategy/drift/driftma.go b/pkg/strategy/drift/driftma.go index 0ff0a9c70b..ba0468cdcb 100644 --- a/pkg/strategy/drift/driftma.go +++ b/pkg/strategy/drift/driftma.go @@ -17,19 +17,19 @@ func (s *DriftMA) Update(value, weight float64) { if s.ma1.Length() == 0 { return } - s.drift.Update(s.ma1.Last(), weight) + s.drift.Update(s.ma1.Last(0), weight) if s.drift.Length() == 0 { return } - s.ma2.Update(s.drift.Last()) + s.ma2.Update(s.drift.Last(0)) } -func (s *DriftMA) Last() float64 { - return s.ma2.Last() +func (s *DriftMA) Last(int) float64 { + return s.ma2.Last(0) } func (s *DriftMA) Index(i int) float64 { - return s.ma2.Index(i) + return s.ma2.Last(i) } func (s *DriftMA) Length() int { diff --git a/pkg/strategy/drift/stoploss.go b/pkg/strategy/drift/stoploss.go index bd78efa1dc..56d985f08d 100644 --- a/pkg/strategy/drift/stoploss.go +++ b/pkg/strategy/drift/stoploss.go @@ -9,7 +9,7 @@ func (s *Strategy) CheckStopLoss() bool { } } if s.UseAtr { - atr := s.atr.Last() + atr := s.atr.Last(0) if s.sellPrice > 0 && s.sellPrice+atr <= s.highestPrice || s.buyPrice > 0 && s.buyPrice-atr >= s.lowestPrice { return true diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 21f748e4cd..cf9c53e8e9 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -261,8 +261,8 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { high := kline.High.Float64() low := kline.Low.Float64() s.ma.Update(source) - s.stdevHigh.Update(high - s.ma.Last()) - s.stdevLow.Update(s.ma.Last() - low) + s.stdevHigh.Update(high - s.ma.Last(0)) + s.stdevLow.Update(s.ma.Last(0) - low) s.drift.Update(source, kline.Volume.Abs().Float64()) s.trendLine.Update(source) s.atr.PushK(kline) @@ -485,7 +485,7 @@ func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine, count return } // for doing the trailing stoploss during backtesting - atr := s.atr.Last() + atr := s.atr.Last(0) price := s.getLastPrice() pricef := price.Float64() @@ -538,7 +538,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter s.drift.Update(sourcef, kline.Volume.Abs().Float64()) s.atr.PushK(kline) - atr := s.atr.Last() + atr := s.atr.Last(0) price := kline.Close // s.getLastPrice() pricef := price.Float64() @@ -563,7 +563,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter return } - log.Infof("highdiff: %3.2f open: %8v, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(), kline.Open, kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime) + log.Infof("highdiff: %3.2f open: %8v, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(0), kline.Open, kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime) s.positionLock.Lock() if s.lowestPrice > 0 && lowf < s.lowestPrice { @@ -596,7 +596,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter shortCondition := drift[1] >= 0 && drift[0] <= 0 || (drift[1] >= drift[0] && drift[1] <= 0) || ddrift[1] >= 0 && ddrift[0] <= 0 || (ddrift[1] >= ddrift[0] && ddrift[1] <= 0) longCondition := drift[1] <= 0 && drift[0] >= 0 || (drift[1] <= drift[0] && drift[1] >= 0) || ddrift[1] <= 0 && ddrift[0] >= 0 || (ddrift[1] <= ddrift[0] && ddrift[1] >= 0) if shortCondition && longCondition { - if s.priceLines.Index(1) > s.priceLines.Last() { + if s.priceLines.Index(1) > s.priceLines.Last(0) { longCondition = false } else { shortCondition = false @@ -625,7 +625,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter } if longCondition { - source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier)) + source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last(0) * s.HighLowVarianceMultiplier)) if source.Compare(price) > 0 { source = price } @@ -654,7 +654,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter return } - log.Infof("source in long %v %v %f", source, price, s.stdevLow.Last()) + log.Infof("source in long %v %v %f", source, price, s.stdevLow.Last(0)) o, err := s.SubmitOrder(ctx, *submitOrder) if err != nil { @@ -675,12 +675,12 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter return } if shortCondition { - source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier)) + source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last(0) * s.HighLowVarianceMultiplier)) if source.Compare(price) < 0 { source = price } - log.Infof("source in short: %v %v %f", source, price, s.stdevLow.Last()) + log.Infof("source in short: %v %v %f", source, price, s.stdevLow.Last(0)) opt := s.OpenPositionOptions opt.Short = true diff --git a/pkg/strategy/elliottwave/ewo.go b/pkg/strategy/elliottwave/ewo.go index ff6b7cf2ec..f86706b183 100644 --- a/pkg/strategy/elliottwave/ewo.go +++ b/pkg/strategy/elliottwave/ewo.go @@ -11,8 +11,8 @@ func (s *ElliottWave) Index(i int) float64 { return s.maQuick.Index(i)/s.maSlow.Index(i) - 1.0 } -func (s *ElliottWave) Last() float64 { - return s.maQuick.Last()/s.maSlow.Last() - 1.0 +func (s *ElliottWave) Last(int) float64 { + return s.maQuick.Last(0)/s.maSlow.Last(0) - 1.0 } func (s *ElliottWave) Length() int { diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go index feabe13b55..c307f054c0 100644 --- a/pkg/strategy/elliottwave/strategy.go +++ b/pkg/strategy/elliottwave/strategy.go @@ -199,12 +199,12 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int { if s.counter-s.orderPendingCounter[order.OrderID] >= s.PendingMinInterval { toCancel = true } else if order.Side == types.SideTypeBuy { - if order.Price.Float64()+s.atr.Last()*2 <= pricef { + if order.Price.Float64()+s.atr.Last(0)*2 <= pricef { toCancel = true } } else if order.Side == types.SideTypeSell { // 75% of the probability - if order.Price.Float64()-s.atr.Last()*2 >= pricef { + if order.Price.Float64()-s.atr.Last(0)*2 >= pricef { toCancel = true } } else { @@ -425,7 +425,7 @@ func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) { stoploss := s.Stoploss.Float64() price := s.getLastPrice() pricef := price.Float64() - atr := s.atr.Last() + atr := s.atr.Last(0) numPending := s.smartCancel(ctx, pricef) if numPending > 0 { @@ -476,7 +476,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { s.smartCancel(ctx, pricef) - atr := s.atr.Last() + atr := s.atr.Last(0) ewo := types.Array(s.ewo, 4) if len(ewo) < 4 { return diff --git a/pkg/strategy/emastop/strategy.go b/pkg/strategy/emastop/strategy.go index 2326f5f78c..d87ff5d445 100644 --- a/pkg/strategy/emastop/strategy.go +++ b/pkg/strategy/emastop/strategy.go @@ -93,7 +93,7 @@ func (s *Strategy) clear(ctx context.Context, orderExecutor bbgo.OrderExecutor) func (s *Strategy) place(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession, indicator types.Float64Indicator, closePrice fixedpoint.Value) { closePriceF := closePrice.Float64() - movingAveragePriceF := indicator.Last() + movingAveragePriceF := indicator.Last(0) // skip it if it's near zero because it's not loaded yet if movingAveragePriceF < 0.0001 { diff --git a/pkg/strategy/ewoDgtrd/heikinashi.go b/pkg/strategy/ewoDgtrd/heikinashi.go index fca1934c03..345d8e512c 100644 --- a/pkg/strategy/ewoDgtrd/heikinashi.go +++ b/pkg/strategy/ewoDgtrd/heikinashi.go @@ -27,11 +27,11 @@ func NewHeikinAshi(size int) *HeikinAshi { func (s *HeikinAshi) Print() string { return fmt.Sprintf("Heikin c: %.3f, o: %.3f, h: %.3f, l: %.3f, v: %.3f", - s.Close.Last(), - s.Open.Last(), - s.High.Last(), - s.Low.Last(), - s.Volume.Last()) + s.Close.Last(0), + s.Open.Last(0), + s.High.Last(0), + s.Low.Last(0), + s.Volume.Last(0)) } func (inc *HeikinAshi) Update(kline types.KLine) { @@ -40,7 +40,7 @@ func (inc *HeikinAshi) Update(kline types.KLine) { high := kline.High.Float64() low := kline.Low.Float64() newClose := (open + high + low + cloze) / 4. - newOpen := (inc.Open.Last() + inc.Close.Last()) / 2. + newOpen := (inc.Open.Last(0) + inc.Close.Last(0)) / 2. inc.Close.Update(newClose) inc.Open.Update(newOpen) inc.High.Update(math.Max(math.Max(high, newOpen), newClose)) diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index e13b84b0e1..50af7f4bf2 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -139,7 +139,7 @@ func NewCCISTOCH(i types.Interval, filterHigh, filterLow float64) *CCISTOCH { func (inc *CCISTOCH) Update(cloze float64) { inc.cci.Update(cloze) - inc.stoch.Update(inc.cci.Last(), inc.cci.Last(), inc.cci.Last()) + inc.stoch.Update(inc.cci.Last(0), inc.cci.Last(0), inc.cci.Last(0)) inc.ma.Update(inc.stoch.LastD()) } @@ -180,19 +180,19 @@ type VWEMA struct { V types.UpdatableSeries } -func (inc *VWEMA) Last() float64 { - return inc.PV.Last() / inc.V.Last() +func (inc *VWEMA) Last(int) float64 { + return inc.PV.Last(0) / inc.V.Last(0) } func (inc *VWEMA) Index(i int) float64 { if i >= inc.PV.Length() { return 0 } - vi := inc.V.Index(i) + vi := inc.V.Last(i) if vi == 0 { return 0 } - return inc.PV.Index(i) / vi + return inc.PV.Last(i) / vi } func (inc *VWEMA) Length() int { @@ -262,11 +262,11 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { if s.heikinAshi.Close.Length() == 0 { for _, kline := range window { s.heikinAshi.Update(kline) - s.ccis.Update(getSource(window).Last()) + s.ccis.Update(getSource(window).Last(0)) } } else { s.heikinAshi.Update(window[len(window)-1]) - s.ccis.Update(getSource(window).Last()) + s.ccis.Update(getSource(window).Last(0)) } }) if s.UseEma { @@ -283,7 +283,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { ema34.Update(cloze) } } else { - cloze := getSource(window).Last() + cloze := getSource(window).Last(0) ema5.Update(cloze) ema34.Update(cloze) } @@ -306,7 +306,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { sma34.Update(cloze) } } else { - cloze := getSource(window).Last() + cloze := getSource(window).Last(0) sma5.Update(cloze) sma34.Update(cloze) } @@ -330,14 +330,14 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { vols := getVol(window) if evwma5.PV.Length() == 0 { for i := clozes.Length() - 1; i >= 0; i-- { - price := clozes.Index(i) - vol := vols.Index(i) + price := clozes.Last(i) + vol := vols.Last(i) evwma5.UpdateVal(price, vol) evwma34.UpdateVal(price, vol) } } else { - price := clozes.Last() - vol := vols.Last() + price := clozes.Last(0) + vol := vols.Last(0) evwma5.UpdateVal(price, vol) evwma34.UpdateVal(price, vol) } @@ -363,7 +363,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { sig.Update(ewoValue) } } else { - sig.Update(s.ewo.Last()) + sig.Update(s.ewo.Last(0)) } }) s.ewoSignal = sig @@ -381,7 +381,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { sig.Update(ewoValue) } } else { - sig.Update(s.ewo.Last()) + sig.Update(s.ewo.Last(0)) } }) s.ewoSignal = sig @@ -398,13 +398,13 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { // lazy init ewoVals := s.ewo.Reverse() for i, ewoValue := range ewoVals { - vol := window.Volume().Index(i) + vol := window.Volume().Last(i) sig.PV.Update(ewoValue * vol) sig.V.Update(vol) } } else { - vol := window.Volume().Last() - sig.PV.Update(s.ewo.Last() * vol) + vol := window.Volume().Last(0) + sig.PV.Update(s.ewo.Last(0) * vol) sig.V.Update(vol) } }) @@ -663,11 +663,13 @@ func (s *Strategy) GetLastPrice() fixedpoint.Value { // - TP by (lastprice < peak price - atr) || (lastprice > bottom price + atr) // - SL by s.StopLoss (Abs(price_diff / price) > s.StopLoss) // - entry condition on ewo(Elliott wave oscillator) Crosses ewoSignal(ma on ewo, signalWindow) -// * buy signal on (crossover on previous K bar and no crossunder on latest K bar) -// * sell signal on (crossunder on previous K bar and no crossunder on latest K bar) +// - buy signal on (crossover on previous K bar and no crossunder on latest K bar) +// - sell signal on (crossunder on previous K bar and no crossunder on latest K bar) +// // - and filtered by the following rules: -// * buy: buy signal ON, kline Close > Open, Close > ma5, Close > ma34, CCI Stochastic Buy signal -// * sell: sell signal ON, kline Close < Open, Close < ma5, Close < ma34, CCI Stochastic Sell signal +// - buy: buy signal ON, kline Close > Open, Close > ma5, Close > ma34, CCI Stochastic Buy signal +// - sell: sell signal ON, kline Close < Open, Close < ma5, Close < ma34, CCI Stochastic Sell signal +// // - or entry when ma34 +- atr * 3 gets touched // - entry price: latestPrice +- atr / 2 (short,long), close at market price // Cancel non-fully filled orders on new signal (either in same direction or not) @@ -784,8 +786,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.SetupIndicators(store) // local peak of ewo - shortSig := s.ewo.Last() < s.ewo.Index(1) && s.ewo.Index(1) > s.ewo.Index(2) - longSig := s.ewo.Last() > s.ewo.Index(1) && s.ewo.Index(1) < s.ewo.Index(2) + shortSig := s.ewo.Last(0) < s.ewo.Last(1) && s.ewo.Last(1) > s.ewo.Last(2) + longSig := s.ewo.Last(0) > s.ewo.Last(1) && s.ewo.Last(1) < s.ewo.Last(2) sellOrderTPSL := func(price fixedpoint.Value) { lastPrice := s.GetLastPrice() @@ -798,8 +800,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } balances := session.GetAccount().Balances() quoteBalance := balances[s.Market.QuoteCurrency].Available - atr := fixedpoint.NewFromFloat(s.atr.Last()) - atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) + atr := fixedpoint.NewFromFloat(s.atr.Last(0)) + atrx2 := fixedpoint.NewFromFloat(s.atr.Last(0) * 2) buyall := false if s.bottomPrice.IsZero() || s.bottomPrice.Compare(price) > 0 { s.bottomPrice = price @@ -809,7 +811,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se spBack := s.sellPrice reason := -1 if quoteBalance.Div(lastPrice).Compare(s.Market.MinQuantity) >= 0 && quoteBalance.Compare(s.Market.MinNotional) >= 0 { - base := fixedpoint.NewFromFloat(s.ma34.Last()) + base := fixedpoint.NewFromFloat(s.ma34.Last(0)) // TP if lastPrice.Compare(s.sellPrice) < 0 && (longSig || (!atrx2.IsZero() && base.Sub(atrx2).Compare(lastPrice) >= 0)) { @@ -903,8 +905,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } balances := session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available - atr := fixedpoint.NewFromFloat(s.atr.Last()) - atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) + atr := fixedpoint.NewFromFloat(s.atr.Last(0)) + atrx2 := fixedpoint.NewFromFloat(s.atr.Last(0) * 2) sellall := false if s.peakPrice.IsZero() || s.peakPrice.Compare(price) < 0 { s.peakPrice = price @@ -915,7 +917,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se reason := -1 if baseBalance.Compare(s.Market.MinQuantity) >= 0 && baseBalance.Mul(lastPrice).Compare(s.Market.MinNotional) >= 0 { // TP - base := fixedpoint.NewFromFloat(s.ma34.Last()) + base := fixedpoint.NewFromFloat(s.ma34.Last(0)) if lastPrice.Compare(s.buyPrice) > 0 && (shortSig || (!atrx2.IsZero() && base.Add(atrx2).Compare(lastPrice) <= 0)) { sellall = true @@ -1089,10 +1091,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se balances := session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Total() quoteBalance := balances[s.Market.QuoteCurrency].Total() - atr := fixedpoint.NewFromFloat(s.atr.Last()) + atr := fixedpoint.NewFromFloat(s.atr.Last(0)) if !s.Environment.IsBackTesting() { log.Infof("Get last price: %v, ewo %f, ewoSig %f, ccis: %f, atr %v, kline: %v, balance[base]: %v balance[quote]: %v", - lastPrice, s.ewo.Last(), s.ewoSignal.Last(), s.ccis.ma.Last(), atr, kline, baseBalance, quoteBalance) + lastPrice, s.ewo.Last(0), s.ewoSignal.Last(0), s.ccis.ma.Last(0), atr, kline, baseBalance, quoteBalance) } if kline.Interval != s.Interval { @@ -1104,21 +1106,21 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se priceChangeRate := (priceHighest - priceLowest) / priceHighest / 14 ewoHighest := types.Highest(s.ewoHistogram, 233) - s.ewoChangeRate = math.Abs(s.ewoHistogram.Last() / ewoHighest * priceChangeRate) + s.ewoChangeRate = math.Abs(s.ewoHistogram.Last(0) / ewoHighest * priceChangeRate) longSignal := types.CrossOver(s.ewo, s.ewoSignal) shortSignal := types.CrossUnder(s.ewo, s.ewoSignal) - base := s.ma34.Last() - sellLine := base + s.atr.Last()*3 - buyLine := base - s.atr.Last()*3 + base := s.ma34.Last(0) + sellLine := base + s.atr.Last(0)*3 + buyLine := base - s.atr.Last(0)*3 clozes := getClose(window) opens := getOpen(window) // get trend flags - bull := clozes.Last() > opens.Last() - breakThrough := clozes.Last() > s.ma5.Last() && clozes.Last() > s.ma34.Last() - breakDown := clozes.Last() < s.ma5.Last() && clozes.Last() < s.ma34.Last() + bull := clozes.Last(0) > opens.Last(0) + breakThrough := clozes.Last(0) > s.ma5.Last(0) && clozes.Last(0) > s.ma34.Last(0) + breakDown := clozes.Last(0) < s.ma5.Last(0) && clozes.Last(0) < s.ma34.Last(0) // kline breakthrough ma5, ma34 trend up, and cci Stochastic bull IsBull := bull && breakThrough && s.ccis.BuySignal() && s.ewoChangeRate < s.EwoChangeFilterHigh && s.ewoChangeRate > s.EwoChangeFilterLow @@ -1141,7 +1143,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // backup, since the s.sellPrice will be cleared when doing ClosePosition sellPrice := s.sellPrice - log.Errorf("ewoChangeRate %v, emv %v", s.ewoChangeRate, s.emv.Last()) + log.Errorf("ewoChangeRate %v, emv %v", s.ewoChangeRate, s.emv.Last(0)) // calculate report if closeOrder, _ := s.PlaceBuyOrder(ctx, price); closeOrder != nil { @@ -1175,7 +1177,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // backup, since the s.buyPrice will be cleared when doing ClosePosition buyPrice := s.buyPrice - log.Errorf("ewoChangeRate: %v, emv %v", s.ewoChangeRate, s.emv.Last()) + log.Errorf("ewoChangeRate: %v, emv %v", s.ewoChangeRate, s.emv.Last(0)) // calculate report if closeOrder, _ := s.PlaceSellOrder(ctx, price); closeOrder != nil { diff --git a/pkg/strategy/factorzoo/factors/momentum.go b/pkg/strategy/factorzoo/factors/momentum.go index c9dc34877b..827af09760 100644 --- a/pkg/strategy/factorzoo/factors/momentum.go +++ b/pkg/strategy/factorzoo/factors/momentum.go @@ -34,14 +34,14 @@ func (inc *MOM) Index(i int) float64 { if inc.Values == nil { return 0 } - return inc.Values.Index(i) + return inc.Values.Last(i) } -func (inc *MOM) Last() float64 { +func (inc *MOM) Last(int) float64 { if inc.Values.Length() == 0 { return 0 } - return inc.Values.Last() + return inc.Values.Last(0) } func (inc *MOM) Length() int { @@ -51,7 +51,7 @@ func (inc *MOM) Length() int { return inc.Values.Length() } -//var _ types.SeriesExtend = &MOM{} +// var _ types.SeriesExtend = &MOM{} func (inc *MOM) Update(open, close float64) { if inc.SeriesBase.Series == nil { @@ -62,7 +62,7 @@ func (inc *MOM) Update(open, close float64) { inc.opens.Update(open) inc.closes.Update(close) if inc.opens.Length() >= inc.Window && inc.closes.Length() >= inc.Window { - gap := inc.opens.Last()/inc.closes.Index(1) - 1 + gap := inc.opens.Last(0)/inc.closes.Index(1) - 1 inc.Values.Push(gap) } } @@ -72,11 +72,11 @@ func (inc *MOM) CalculateAndUpdate(allKLines []types.KLine) { for _, k := range allKLines { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } @@ -99,7 +99,7 @@ func (inc *MOM) PushK(k types.KLine) { inc.Update(k.Open.Float64(), k.Close.Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func calculateMomentum(klines []types.KLine, window int, valA KLineValueMapper, valB KLineValueMapper) (float64, error) { diff --git a/pkg/strategy/factorzoo/factors/price_mean_reversion.go b/pkg/strategy/factorzoo/factors/price_mean_reversion.go index 2d2bd5ce89..7143e547cb 100644 --- a/pkg/strategy/factorzoo/factors/price_mean_reversion.go +++ b/pkg/strategy/factorzoo/factors/price_mean_reversion.go @@ -35,12 +35,12 @@ func (inc *PMR) Update(price float64) { } inc.SMA.Update(price) if inc.SMA.Length() >= inc.Window { - reversion := inc.SMA.Last() / price + reversion := inc.SMA.Last(0) / price inc.Values.Push(reversion) } } -func (inc *PMR) Last() float64 { +func (inc *PMR) Last(int) float64 { if len(inc.Values) == 0 { return 0 } @@ -65,11 +65,11 @@ func (inc *PMR) CalculateAndUpdate(allKLines []types.KLine) { for _, k := range allKLines { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } @@ -92,7 +92,7 @@ func (inc *PMR) PushK(k types.KLine) { inc.Update(indicator.KLineClosePriceMapper(k)) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func CalculateKLinesPMR(allKLines []types.KLine, window int) float64 { diff --git a/pkg/strategy/factorzoo/factors/price_volume_divergence.go b/pkg/strategy/factorzoo/factors/price_volume_divergence.go index 54b07a2659..adc20e02a3 100644 --- a/pkg/strategy/factorzoo/factors/price_volume_divergence.go +++ b/pkg/strategy/factorzoo/factors/price_volume_divergence.go @@ -47,7 +47,7 @@ func (inc *PVD) Update(price float64, volume float64) { } } -func (inc *PVD) Last() float64 { +func (inc *PVD) Last(int) float64 { if len(inc.Values) == 0 { return 0 } @@ -72,11 +72,11 @@ func (inc *PVD) CalculateAndUpdate(allKLines []types.KLine) { for _, k := range allKLines { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } @@ -99,7 +99,7 @@ func (inc *PVD) PushK(k types.KLine) { inc.Update(indicator.KLineClosePriceMapper(k), indicator.KLineVolumeMapper(k)) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func CalculateKLinesPVD(allKLines []types.KLine, window int) float64 { diff --git a/pkg/strategy/factorzoo/factors/return_rate.go b/pkg/strategy/factorzoo/factors/return_rate.go index 057f2ac5c2..3fe3d41565 100644 --- a/pkg/strategy/factorzoo/factors/return_rate.go +++ b/pkg/strategy/factorzoo/factors/return_rate.go @@ -30,12 +30,12 @@ func (inc *RR) Update(price float64) { inc.prices = types.NewQueue(inc.Window) } inc.prices.Update(price) - irr := inc.prices.Last()/inc.prices.Index(1) - 1 + irr := inc.prices.Last(0)/inc.prices.Index(1) - 1 inc.Values.Push(irr) } -func (inc *RR) Last() float64 { +func (inc *RR) Last(int) float64 { if len(inc.Values) == 0 { return 0 } @@ -60,11 +60,11 @@ func (inc *RR) CalculateAndUpdate(allKLines []types.KLine) { for _, k := range allKLines { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } @@ -91,14 +91,14 @@ func (inc *RR) PushK(k types.KLine) { inc.Update(indicator.KLineClosePriceMapper(k)) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func (inc *RR) LoadK(allKLines []types.KLine) { for _, k := range allKLines { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } //func calculateReturn(klines []types.KLine, window int, val KLineValueMapper) (float64, error) { diff --git a/pkg/strategy/factorzoo/factors/volume_momentum.go b/pkg/strategy/factorzoo/factors/volume_momentum.go index 602065273c..87029ed9ed 100644 --- a/pkg/strategy/factorzoo/factors/volume_momentum.go +++ b/pkg/strategy/factorzoo/factors/volume_momentum.go @@ -33,14 +33,14 @@ func (inc *VMOM) Index(i int) float64 { if inc.Values == nil { return 0 } - return inc.Values.Index(i) + return inc.Values.Last(i) } -func (inc *VMOM) Last() float64 { +func (inc *VMOM) Last(int) float64 { if inc.Values.Length() == 0 { return 0 } - return inc.Values.Last() + return inc.Values.Last(0) } func (inc *VMOM) Length() int { @@ -59,7 +59,7 @@ func (inc *VMOM) Update(volume float64) { } inc.volumes.Update(volume) if inc.volumes.Length() >= inc.Window { - v := inc.volumes.Last() / inc.volumes.Mean() + v := inc.volumes.Last(0) / inc.volumes.Mean() inc.Values.Push(v) } } @@ -69,11 +69,11 @@ func (inc *VMOM) CalculateAndUpdate(allKLines []types.KLine) { for _, k := range allKLines { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } else { k := allKLines[len(allKLines)-1] inc.PushK(k) - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } } @@ -96,7 +96,7 @@ func (inc *VMOM) PushK(k types.KLine) { inc.Update(k.Volume.Float64()) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func calculateVolumeMomentum(klines []types.KLine, window int, valV KLineValueMapper, valP KLineValueMapper) (float64, error) { @@ -110,7 +110,7 @@ func calculateVolumeMomentum(klines []types.KLine, window int, valV KLineValueMa vma += valV(p) } vma /= float64(window) - momentum := valV(klines[length-1]) / vma //* (valP(klines[length-1-2]) / valP(klines[length-1])) + momentum := valV(klines[length-1]) / vma // * (valP(klines[length-1-2]) / valP(klines[length-1])) return momentum, nil } diff --git a/pkg/strategy/factorzoo/linear_regression.go b/pkg/strategy/factorzoo/linear_regression.go index 80c84e6870..670e4dcea8 100644 --- a/pkg/strategy/factorzoo/linear_regression.go +++ b/pkg/strategy/factorzoo/linear_regression.go @@ -104,11 +104,11 @@ func (s *Linear) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.General // use the last value from indicators, or the SeriesExtends' predict function. (e.g., look back: 5) input := []float64{ - s.divergence.Last(), - s.reversion.Last(), - s.drift.Last(), - s.momentum.Last(), - s.volume.Last(), + s.divergence.Last(0), + s.reversion.Last(0), + s.drift.Last(0), + s.momentum.Last(0), + s.volume.Last(0), } pred := model.Predict(input) predLst.Update(pred) diff --git a/pkg/strategy/fixedmaker/strategy.go b/pkg/strategy/fixedmaker/strategy.go index d316334062..0910e536d6 100644 --- a/pkg/strategy/fixedmaker/strategy.go +++ b/pkg/strategy/fixedmaker/strategy.go @@ -213,7 +213,7 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) ([]types.SubmitOrde log.Infof("mid price: %+v", midPrice) if s.ATRMultiplier.Float64() > 0 { - atr := fixedpoint.NewFromFloat(s.atr.Last()) + atr := fixedpoint.NewFromFloat(s.atr.Last(0)) log.Infof("atr: %s", atr.String()) s.HalfSpreadRatio = s.ATRMultiplier.Mul(atr).Div(midPrice) log.Infof("half spread ratio: %s", s.HalfSpreadRatio.String()) diff --git a/pkg/strategy/flashcrash/strategy.go b/pkg/strategy/flashcrash/strategy.go index aff86f3ca5..d4db389254 100644 --- a/pkg/strategy/flashcrash/strategy.go +++ b/pkg/strategy/flashcrash/strategy.go @@ -75,7 +75,7 @@ func (s *Strategy) updateBidOrders(orderExecutor bbgo.OrderExecutor, session *bb return } - var startPrice = fixedpoint.NewFromFloat(s.ewma.Last()).Mul(s.Percentage) + var startPrice = fixedpoint.NewFromFloat(s.ewma.Last(0)).Mul(s.Percentage) var submitOrders []types.SubmitOrder for i := 0; i < s.GridNum; i++ { diff --git a/pkg/strategy/fmaker/A18.go b/pkg/strategy/fmaker/A18.go index 6e7e603a00..35dc7368e4 100644 --- a/pkg/strategy/fmaker/A18.go +++ b/pkg/strategy/fmaker/A18.go @@ -21,7 +21,7 @@ type A18 struct { UpdateCallbacks []func(val float64) } -func (inc *A18) Last() float64 { +func (inc *A18) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } @@ -84,8 +84,8 @@ func calculateA18(klines []types.KLine, valClose KLineValueMapper) (float64, err closes.Push(valClose(k)) } - delay5 := closes.Index(4) - curr := closes.Index(0) + delay5 := closes.Last(4) + curr := closes.Last(0) alpha := curr / delay5 return alpha, nil diff --git a/pkg/strategy/fmaker/A2.go b/pkg/strategy/fmaker/A2.go index 0c9cc4b3bf..cf674e1a6b 100644 --- a/pkg/strategy/fmaker/A2.go +++ b/pkg/strategy/fmaker/A2.go @@ -21,7 +21,7 @@ type A2 struct { UpdateCallbacks []func(val float64) } -func (inc *A2) Last() float64 { +func (inc *A2) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } @@ -88,8 +88,8 @@ func calculateA2(klines []types.KLine, valLow KLineValueMapper, valHigh KLineVal closes.Push(valClose(k)) } - prev := ((closes.Index(1) - lows.Index(1)) - (highs.Index(1) - closes.Index(1))) / (highs.Index(1) - lows.Index(1)) - curr := ((closes.Index(0) - lows.Index(0)) - (highs.Index(0) - closes.Index(0))) / (highs.Index(0) - lows.Index(0)) + prev := ((closes.Last(1) - lows.Index(1)) - (highs.Index(1) - closes.Index(1))) / (highs.Index(1) - lows.Index(1)) + curr := ((closes.Last(0) - lows.Index(0)) - (highs.Index(0) - closes.Index(0))) / (highs.Index(0) - lows.Index(0)) alpha := (curr - prev) * -1 // delta(1 interval) return alpha, nil diff --git a/pkg/strategy/fmaker/A3.go b/pkg/strategy/fmaker/A3.go index 945b06b670..02118def87 100644 --- a/pkg/strategy/fmaker/A3.go +++ b/pkg/strategy/fmaker/A3.go @@ -22,7 +22,7 @@ type A3 struct { UpdateCallbacks []func(val float64) } -func (inc *A3) Last() float64 { +func (inc *A3) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } diff --git a/pkg/strategy/fmaker/A34.go b/pkg/strategy/fmaker/A34.go index eb01799e3e..ba60aeedd1 100644 --- a/pkg/strategy/fmaker/A34.go +++ b/pkg/strategy/fmaker/A34.go @@ -21,7 +21,7 @@ type A34 struct { UpdateCallbacks []func(val float64) } -func (inc *A34) Last() float64 { +func (inc *A34) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } @@ -83,7 +83,7 @@ func calculateA34(klines []types.KLine, valClose KLineValueMapper) (float64, err closes.Push(valClose(k)) } - c := closes.Last() + c := closes.Last(0) sumC := 0. for i := 1; i <= 12; i++ { diff --git a/pkg/strategy/fmaker/R.go b/pkg/strategy/fmaker/R.go index d3aa0eca21..dc4f0eabfe 100644 --- a/pkg/strategy/fmaker/R.go +++ b/pkg/strategy/fmaker/R.go @@ -25,7 +25,7 @@ type R struct { UpdateCallbacks []func(val float64) } -func (inc *R) Last() float64 { +func (inc *R) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } diff --git a/pkg/strategy/fmaker/S0.go b/pkg/strategy/fmaker/S0.go index 9c934ccd2c..7d2eda1589 100644 --- a/pkg/strategy/fmaker/S0.go +++ b/pkg/strategy/fmaker/S0.go @@ -21,7 +21,7 @@ type S0 struct { UpdateCallbacks []func(val float64) } -func (inc *S0) Last() float64 { +func (inc *S0) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } @@ -84,7 +84,7 @@ func calculateS0(klines []types.KLine, valClose KLineValueMapper) (float64, erro } sma := floats.Slice.Sum(closes[len(closes)-window:len(closes)-1]) / float64(window) - alpha := sma / closes.Last() + alpha := sma / closes.Last(0) return alpha, nil } diff --git a/pkg/strategy/fmaker/S1.go b/pkg/strategy/fmaker/S1.go index 498efec63e..68962a579e 100644 --- a/pkg/strategy/fmaker/S1.go +++ b/pkg/strategy/fmaker/S1.go @@ -19,7 +19,7 @@ type S1 struct { UpdateCallbacks []func(value float64) } -func (inc *S1) Last() float64 { +func (inc *S1) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } diff --git a/pkg/strategy/fmaker/S2.go b/pkg/strategy/fmaker/S2.go index 960b3c5a83..d63488a985 100644 --- a/pkg/strategy/fmaker/S2.go +++ b/pkg/strategy/fmaker/S2.go @@ -19,7 +19,7 @@ type S2 struct { UpdateCallbacks []func(value float64) } -func (inc *S2) Last() float64 { +func (inc *S2) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } diff --git a/pkg/strategy/fmaker/S3.go b/pkg/strategy/fmaker/S3.go index 238cc62ea3..e00f83625e 100644 --- a/pkg/strategy/fmaker/S3.go +++ b/pkg/strategy/fmaker/S3.go @@ -21,7 +21,7 @@ type S3 struct { UpdateCallbacks []func(val float64) } -func (inc *S3) Last() float64 { +func (inc *S3) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } diff --git a/pkg/strategy/fmaker/S4.go b/pkg/strategy/fmaker/S4.go index 9d122c4734..b7f8cfd8ce 100644 --- a/pkg/strategy/fmaker/S4.go +++ b/pkg/strategy/fmaker/S4.go @@ -21,7 +21,7 @@ type S4 struct { UpdateCallbacks []func(val float64) } -func (inc *S4) Last() float64 { +func (inc *S4) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } diff --git a/pkg/strategy/fmaker/S5.go b/pkg/strategy/fmaker/S5.go index 046733b4f8..8fd2edfb75 100644 --- a/pkg/strategy/fmaker/S5.go +++ b/pkg/strategy/fmaker/S5.go @@ -21,7 +21,7 @@ type S5 struct { UpdateCallbacks []func(val float64) } -func (inc *S5) Last() float64 { +func (inc *S5) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } @@ -83,7 +83,7 @@ func calculateS5(klines []types.KLine, valVolume KLineValueMapper) (float64, err volumes.Push(valVolume(k)) } - v := volumes.Last() + v := volumes.Last(0) sumV := 0. for i := 1; i <= 10; i++ { diff --git a/pkg/strategy/fmaker/S6.go b/pkg/strategy/fmaker/S6.go index 4bb20b158d..8c7f73c16a 100644 --- a/pkg/strategy/fmaker/S6.go +++ b/pkg/strategy/fmaker/S6.go @@ -21,7 +21,7 @@ type S6 struct { UpdateCallbacks []func(val float64) } -func (inc *S6) Last() float64 { +func (inc *S6) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } @@ -90,10 +90,10 @@ func calculateS6(klines []types.KLine, valHigh KLineValueMapper, valLow KLineVal } - H := highs.Last() - L := lows.Last() - C := closes.Last() - V := volumes.Last() + H := highs.Last(0) + L := lows.Last(0) + C := closes.Last(0) + V := volumes.Last(0) alpha := (H + L + C) / 3 * V return alpha, nil diff --git a/pkg/strategy/fmaker/S7.go b/pkg/strategy/fmaker/S7.go index 7000e6897f..048078fe27 100644 --- a/pkg/strategy/fmaker/S7.go +++ b/pkg/strategy/fmaker/S7.go @@ -21,7 +21,7 @@ type S7 struct { UpdateCallbacks []func(val float64) } -func (inc *S7) Last() float64 { +func (inc *S7) Last(int) float64 { if len(inc.Values) == 0 { return 0.0 } @@ -86,8 +86,8 @@ func calculateS7(klines []types.KLine, valOpen KLineValueMapper, valClose KLineV } - O := opens.Last() - C := closes.Last() + O := opens.Last(0) + C := closes.Last(0) alpha := -(1 - O/C) return alpha, nil diff --git a/pkg/strategy/fmaker/strategy.go b/pkg/strategy/fmaker/strategy.go index 0df9b68e20..d03b9bf37a 100644 --- a/pkg/strategy/fmaker/strategy.go +++ b/pkg/strategy/fmaker/strategy.go @@ -341,7 +341,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // } r.Train(rdps...) r.Run() - er, _ := r.Predict(floats2.Slice{s.S0.Last(), s.S1.Last(), s.S2.Last(), s.S4.Last(), s.S5.Last(), s.S6.Last(), s.S7.Last(), s.A2.Last(), s.A3.Last(), s.A18.Last(), s.A34.Last()}) + er, _ := r.Predict(floats2.Slice{s.S0.Last(0), s.S1.Last(0), s.S2.Last(0), s.S4.Last(0), s.S5.Last(0), s.S6.Last(0), s.S7.Last(0), s.A2.Last(0), s.A3.Last(0), s.A18.Last(0), s.A34.Last(0)}) log.Infof("Expected Return Rate: %f", er) q := new(regression.Regression) @@ -399,7 +399,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se q.Run() - log.Info(s.S0.Last(), s.S1.Last(), s.S2.Last(), s.S3.Last(), s.S4.Last(), s.S5.Last(), s.S6.Last(), s.S7.Last(), s.A2.Last(), s.A3.Last(), s.A18.Last(), s.A34.Last()) + log.Info(s.S0.Last(0), s.S1.Last(0), s.S2.Last(0), s.S3.Last(0), s.S4.Last(0), s.S5.Last(0), s.S6.Last(0), s.S7.Last(0), s.A2.Last(0), s.A3.Last(0), s.A18.Last(0), s.A34.Last(0)) log.Infof("Return Rate Regression formula:\n%v", r.Formula) log.Infof("Order Quantity Regression formula:\n%v", q.Formula) @@ -416,7 +416,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // a34 := preprocessing(s.A18.Values[len(s.A18.Values)-20 : len(s.A18.Values)-1-outlook]) // er, _ := r.Predict(types.Float64Slice{s0, s1, s2, s4, s5, a2, a3, a18, a34}) // eq, _ := q.Predict(types.Float64Slice{s0, s1, s2, s4, s5, a2, a3, a18, a34}) - eq, _ := q.Predict(floats2.Slice{s.S0.Last(), s.S1.Last(), s.S2.Last(), s.S4.Last(), s.S5.Last(), s.S6.Last(), s.S7.Last(), s.A2.Last(), s.A3.Last(), s.A18.Last(), s.A34.Last(), er}) + eq, _ := q.Predict(floats2.Slice{s.S0.Last(0), s.S1.Last(0), s.S2.Last(0), s.S4.Last(0), s.S5.Last(0), s.S6.Last(0), s.S7.Last(0), s.A2.Last(0), s.A3.Last(0), s.A18.Last(0), s.A34.Last(0), er}) log.Infof("Expected Order Quantity: %f", eq) // if float64(s.Position.GetBase().Sign())*er < 0 { // s.ClosePosition(ctx, fixedpoint.One, kline.Close) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b8a614e17c..dc45dffd04 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1849,8 +1849,8 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. interval := s.AutoRange.Interval() pivotLow := indicatorSet.PivotLow(types.IntervalWindow{Interval: interval, Window: s.AutoRange.Num}) pivotHigh := indicatorSet.PivotHigh(types.IntervalWindow{Interval: interval, Window: s.AutoRange.Num}) - s.UpperPrice = fixedpoint.NewFromFloat(pivotHigh.Last()) - s.LowerPrice = fixedpoint.NewFromFloat(pivotLow.Last()) + s.UpperPrice = fixedpoint.NewFromFloat(pivotHigh.Last(0)) + s.LowerPrice = fixedpoint.NewFromFloat(pivotLow.Last(0)) s.logger.Infof("autoRange is enabled, using pivot high %f and pivot low %f", s.UpperPrice.Float64(), s.LowerPrice.Float64()) } diff --git a/pkg/strategy/harmonic/shark.go b/pkg/strategy/harmonic/shark.go index 630d4f4bf7..d7bc287c38 100644 --- a/pkg/strategy/harmonic/shark.go +++ b/pkg/strategy/harmonic/shark.go @@ -51,7 +51,7 @@ func (inc *SHARK) Update(high, low, price float64) { } -func (inc *SHARK) Last() float64 { +func (inc *SHARK) Last(int) float64 { if len(inc.Values) == 0 { return 0 } @@ -82,14 +82,14 @@ func (inc *SHARK) PushK(k types.KLine) { inc.Update(indicator.KLineHighPriceMapper(k), indicator.KLineLowPriceMapper(k), indicator.KLineClosePriceMapper(k)) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func (inc *SHARK) LoadK(allKLines []types.KLine) { for _, k := range allKLines { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func (inc SHARK) SharkLong(highs, lows floats.Slice, p float64, lookback int) float64 { diff --git a/pkg/strategy/harmonic/strategy.go b/pkg/strategy/harmonic/strategy.go index 4d98fda34a..c73a3d7363 100644 --- a/pkg/strategy/harmonic/strategy.go +++ b/pkg/strategy/harmonic/strategy.go @@ -145,7 +145,7 @@ func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { // Accumulated profit MA r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) - r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last()) + r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last(0)) // Accumulated Fee r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) @@ -341,11 +341,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se states.Update(0) s.session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - log.Infof("shark score: %f, current price: %f", s.shark.Last(), kline.Close.Float64()) + log.Infof("shark score: %f, current price: %f", s.shark.Last(0), kline.Close.Float64()) nextState := hmm(s.shark.Array(s.Window), states.Array(s.Window), s.Window) states.Update(nextState) - log.Infof("Denoised signal via HMM: %f", states.Last()) + log.Infof("Denoised signal via HMM: %f", states.Last(0)) if states.Length() < s.Window { return diff --git a/pkg/strategy/irr/neg_return_rate.go b/pkg/strategy/irr/neg_return_rate.go index 473f2ecddc..72fe420feb 100644 --- a/pkg/strategy/irr/neg_return_rate.go +++ b/pkg/strategy/irr/neg_return_rate.go @@ -44,16 +44,16 @@ func (inc *NRR) Update(openPrice, closePrice float64) { irr := (closePrice - openPrice) / openPrice if inc.prices.Length() >= inc.Window && inc.delay { // D1 - nirr = -1 * ((inc.prices.Last() / inc.prices.Index(inc.Window-1)) - 1) - irr = (inc.prices.Last() / inc.prices.Index(inc.Window-1)) - 1 + nirr = -1 * ((inc.prices.Last(0) / inc.prices.Index(inc.Window-1)) - 1) + irr = (inc.prices.Last(0) / inc.prices.Index(inc.Window-1)) - 1 } - inc.Values.Push(nirr) // neg ret here - inc.RankedValues.Push(inc.Rank(inc.RankingWindow).Last() / float64(inc.RankingWindow)) // ranked neg ret here + inc.Values.Push(nirr) // neg ret here + inc.RankedValues.Push(inc.Rank(inc.RankingWindow).Last(0) / float64(inc.RankingWindow)) // ranked neg ret here inc.ReturnValues.Push(irr) } -func (inc *NRR) Last() float64 { +func (inc *NRR) Last(int) float64 { if len(inc.Values) == 0 { return 0 } @@ -84,12 +84,12 @@ func (inc *NRR) PushK(k types.KLine) { inc.Update(indicator.KLineOpenPriceMapper(k), indicator.KLineClosePriceMapper(k)) inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } func (inc *NRR) LoadK(allKLines []types.KLine) { for _, k := range allKLines { inc.PushK(k) } - inc.EmitUpdate(inc.Last()) + inc.EmitUpdate(inc.Last(0)) } diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go index 679c50ef52..0e7616c951 100644 --- a/pkg/strategy/irr/strategy.go +++ b/pkg/strategy/irr/strategy.go @@ -152,7 +152,7 @@ func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { // Accumulated profit MA r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) - r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last()) + r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last(0)) // Accumulated Fee r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 34ff4fb2d8..b8e5f9be05 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -257,12 +257,12 @@ func (s *Strategy) isAllowOppositePosition() bool { return false } - if (s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last() < 0 && s.SlowLinReg.Last() < 0) || - (s.mainTrendCurrent == types.DirectionDown && s.FastLinReg.Last() > 0 && s.SlowLinReg.Last() > 0) { - log.Infof("%s allow opposite position is enabled: MainTrend %v, FastLinReg: %f, SlowLinReg: %f", s.Symbol, s.mainTrendCurrent, s.FastLinReg.Last(), s.SlowLinReg.Last()) + if (s.mainTrendCurrent == types.DirectionUp && s.FastLinReg.Last(0) < 0 && s.SlowLinReg.Last(0) < 0) || + (s.mainTrendCurrent == types.DirectionDown && s.FastLinReg.Last(0) > 0 && s.SlowLinReg.Last(0) > 0) { + log.Infof("%s allow opposite position is enabled: MainTrend %v, FastLinReg: %f, SlowLinReg: %f", s.Symbol, s.mainTrendCurrent, s.FastLinReg.Last(0), s.SlowLinReg.Last(0)) return true } - log.Infof("%s allow opposite position is disabled: MainTrend %v, FastLinReg: %f, SlowLinReg: %f", s.Symbol, s.mainTrendCurrent, s.FastLinReg.Last(), s.SlowLinReg.Last()) + log.Infof("%s allow opposite position is disabled: MainTrend %v, FastLinReg: %f, SlowLinReg: %f", s.Symbol, s.mainTrendCurrent, s.FastLinReg.Last(0), s.SlowLinReg.Last(0)) return false } @@ -390,10 +390,10 @@ func (s *Strategy) getOrderQuantities(askPrice fixedpoint.Value, bidPrice fixedp } // Faster position decrease - if s.mainTrendCurrent == types.DirectionUp && s.SlowLinReg.Last() < 0 { + if s.mainTrendCurrent == types.DirectionUp && s.SlowLinReg.Last(0) < 0 { sellQuantity = sellQuantity.Mul(s.FasterDecreaseRatio) log.Infof("faster %s position decrease: sell qty %v", s.Symbol, sellQuantity) - } else if s.mainTrendCurrent == types.DirectionDown && s.SlowLinReg.Last() > 0 { + } else if s.mainTrendCurrent == types.DirectionDown && s.SlowLinReg.Last(0) > 0 { buyQuantity = buyQuantity.Mul(s.FasterDecreaseRatio) log.Infof("faster %s position decrease: buy qty %v", s.Symbol, buyQuantity) } @@ -475,12 +475,12 @@ func (s *Strategy) getCanBuySell(buyQuantity, bidPrice, sellQuantity, askPrice, // Check TradeInBand if s.TradeInBand { // Price too high - if bidPrice.Float64() > s.neutralBoll.UpBand.Last() { + if bidPrice.Float64() > s.neutralBoll.UpBand.Last(0) { canBuy = false log.Infof("tradeInBand is set, skip buy due to the price is higher than the neutralBB") } // Price too low in uptrend - if askPrice.Float64() < s.neutralBoll.DownBand.Last() { + if askPrice.Float64() < s.neutralBoll.DownBand.Last(0) { canSell = false log.Infof("tradeInBand is set, skip sell due to the price is lower than the neutralBB") } @@ -700,7 +700,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se closePrice = price } } - priceReverseEMA := fixedpoint.NewFromFloat(s.ReverseEMA.Last()) + priceReverseEMA := fixedpoint.NewFromFloat(s.ReverseEMA.Last(0)) // Main trend by ReverseEMA if closePrice.Compare(priceReverseEMA) > 0 { @@ -715,7 +715,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // closePrice is the close price of current kline closePrice := kline.GetClose() // priceReverseEMA is the current ReverseEMA price - priceReverseEMA := fixedpoint.NewFromFloat(s.ReverseEMA.Last()) + priceReverseEMA := fixedpoint.NewFromFloat(s.ReverseEMA.Last(0)) // Main trend by ReverseEMA s.mainTrendPrevious = s.mainTrendCurrent diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index 91f9f0eed9..55b6bed964 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -104,7 +104,7 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener // update pivot low data session.MarketDataStream.OnStart(func() { if s.updatePivotLow() { - bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last()) + bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last(0)) } s.pilotQuantityCalculation() @@ -117,7 +117,7 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener return } - bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last()) + bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last(0)) } })) @@ -260,7 +260,7 @@ func (s *BreakLow) pilotQuantityCalculation() { } func (s *BreakLow) updatePivotLow() bool { - low := fixedpoint.NewFromFloat(s.pivotLow.Last()) + low := fixedpoint.NewFromFloat(s.pivotLow.Last(0)) if low.IsZero() { return false } @@ -273,7 +273,7 @@ func (s *BreakLow) updatePivotLow() bool { s.pivotLowPrices = append(s.pivotLowPrices, low) } - fastLow := fixedpoint.NewFromFloat(s.fastPivotLow.Last()) + fastLow := fixedpoint.NewFromFloat(s.fastPivotLow.Last(0)) if !fastLow.IsZero() { if fastLow.Compare(s.lastLow) < 0 { s.lastLowInvalidated = true diff --git a/pkg/strategy/pivotshort/failedbreakhigh.go b/pkg/strategy/pivotshort/failedbreakhigh.go index 30ad0a32ef..f60cddfe8b 100644 --- a/pkg/strategy/pivotshort/failedbreakhigh.go +++ b/pkg/strategy/pivotshort/failedbreakhigh.go @@ -142,7 +142,7 @@ func (s *FailedBreakHigh) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg // update pivot low data session.MarketDataStream.OnStart(func() { if s.updatePivotHigh() { - bbgo.Notify("%s new pivot high: %f", s.Symbol, s.pivotHigh.Last()) + bbgo.Notify("%s new pivot high: %f", s.Symbol, s.pivotHigh.Last(0)) } s.pilotQuantityCalculation() @@ -155,7 +155,7 @@ func (s *FailedBreakHigh) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg return } - bbgo.Notify("%s new pivot high: %f", s.Symbol, s.pivotHigh.Last()) + bbgo.Notify("%s new pivot high: %f", s.Symbol, s.pivotHigh.Last(0)) } })) @@ -237,7 +237,7 @@ func (s *FailedBreakHigh) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg } if s.vwma != nil { - vma := fixedpoint.NewFromFloat(s.vwma.Last()) + vma := fixedpoint.NewFromFloat(s.vwma.Last(0)) if kline.Volume.Compare(vma) < 0 { bbgo.Notify("%s %s kline volume %f is less than VMA %f, skip failed break high short", kline.Symbol, kline.Interval, kline.Volume.Float64(), vma.Float64()) return @@ -378,7 +378,7 @@ func (s *FailedBreakHigh) detectMacdDivergence() { } func (s *FailedBreakHigh) updatePivotHigh() bool { - high := fixedpoint.NewFromFloat(s.pivotHigh.Last()) + high := fixedpoint.NewFromFloat(s.pivotHigh.Last(0)) if high.IsZero() { return false } @@ -390,7 +390,7 @@ func (s *FailedBreakHigh) updatePivotHigh() bool { s.pivotHighPrices = append(s.pivotHighPrices, high) } - fastHigh := fixedpoint.NewFromFloat(s.fastPivotHigh.Last()) + fastHigh := fixedpoint.NewFromFloat(s.fastPivotHigh.Last(0)) if !fastHigh.IsZero() { if fastHigh.Compare(s.lastHigh) > 0 { // invalidate the last low diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index 744abefba7..357ff49abe 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -72,7 +72,7 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg s.resistancePivot = session.StandardIndicatorSet(s.Symbol).PivotLow(s.IntervalWindow) // use the last kline from the history before we get the next closed kline - s.updateResistanceOrders(fixedpoint.NewFromFloat(s.resistancePivot.Last())) + s.updateResistanceOrders(fixedpoint.NewFromFloat(s.resistancePivot.Last(0))) session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { // StrategyController diff --git a/pkg/strategy/pricedrop/strategy.go b/pkg/strategy/pricedrop/strategy.go index c8a9b16648..4d29330cd7 100644 --- a/pkg/strategy/pricedrop/strategy.go +++ b/pkg/strategy/pricedrop/strategy.go @@ -74,8 +74,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - if kline.Close.Float64() > ema.Last() { - log.Warnf("kline close price %v is above EMA %s %f", kline.Close, ema.IntervalWindow, ema.Last()) + if kline.Close.Float64() > ema.Last(0) { + log.Warnf("kline close price %v is above EMA %s %f", kline.Close, ema.IntervalWindow, ema.Last(0)) return } diff --git a/pkg/strategy/rsmaker/strategy.go b/pkg/strategy/rsmaker/strategy.go index 2dd477a8fb..76f92438f1 100644 --- a/pkg/strategy/rsmaker/strategy.go +++ b/pkg/strategy/rsmaker/strategy.go @@ -329,9 +329,9 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k // baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] // quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] - downBand := s.defaultBoll.DownBand.Last() - upBand := s.defaultBoll.UpBand.Last() - sma := s.defaultBoll.SMA.Last() + downBand := s.defaultBoll.DownBand.Last(0) + upBand := s.defaultBoll.UpBand.Last(0) + sma := s.defaultBoll.SMA.Last(0) log.Infof("bollinger band: up %f sma %f down %f", upBand, sma, downBand) bandPercentage := calculateBandPercentage(upBand, downBand, sma, midPrice.Float64()) diff --git a/pkg/strategy/schedule/strategy.go b/pkg/strategy/schedule/strategy.go index a647592373..5137ae01a7 100644 --- a/pkg/strategy/schedule/strategy.go +++ b/pkg/strategy/schedule/strategy.go @@ -127,7 +127,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se match := false // if any of the conditions satisfies then we execute order - if belowMA != nil && closePriceF < belowMA.Last() { + if belowMA != nil && closePriceF < belowMA.Last(0) { match = true if s.BelowMovingAverage != nil { if s.BelowMovingAverage.Side != nil { @@ -139,7 +139,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se quantity = s.BelowMovingAverage.QuantityOrAmount.CalculateQuantity(closePrice) } } - } else if aboveMA != nil && closePriceF > aboveMA.Last() { + } else if aboveMA != nil && closePriceF > aboveMA.Last(0) { match = true if s.AboveMovingAverage != nil { if s.AboveMovingAverage.Side != nil { diff --git a/pkg/strategy/skeleton/strategy.go b/pkg/strategy/skeleton/strategy.go index 536aaae10b..6651151fb1 100644 --- a/pkg/strategy/skeleton/strategy.go +++ b/pkg/strategy/skeleton/strategy.go @@ -98,7 +98,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // when a kline is closed, we will do something callback := func(kline types.KLine) { // get the latest ATR value from the indicator object that we just defined. - atrValue := atr.Last() + atrValue := atr.Last(0) log.Infof("atr %f", atrValue) // Update our counter and sync the changes to the persistence layer on time diff --git a/pkg/strategy/supertrend/double_dema.go b/pkg/strategy/supertrend/double_dema.go index e711f89f42..bbe1adde69 100644 --- a/pkg/strategy/supertrend/double_dema.go +++ b/pkg/strategy/supertrend/double_dema.go @@ -21,9 +21,9 @@ type DoubleDema struct { func (dd *DoubleDema) getDemaSignal(openPrice float64, closePrice float64) types.Direction { var demaSignal types.Direction = types.DirectionNone - if closePrice > dd.fastDEMA.Last() && closePrice > dd.slowDEMA.Last() && !(openPrice > dd.fastDEMA.Last() && openPrice > dd.slowDEMA.Last()) { + if closePrice > dd.fastDEMA.Last(0) && closePrice > dd.slowDEMA.Last(0) && !(openPrice > dd.fastDEMA.Last(0) && openPrice > dd.slowDEMA.Last(0)) { demaSignal = types.DirectionUp - } else if closePrice < dd.fastDEMA.Last() && closePrice < dd.slowDEMA.Last() && !(openPrice < dd.fastDEMA.Last() && openPrice < dd.slowDEMA.Last()) { + } else if closePrice < dd.fastDEMA.Last(0) && closePrice < dd.slowDEMA.Last(0) && !(openPrice < dd.fastDEMA.Last(0) && openPrice < dd.slowDEMA.Last(0)) { demaSignal = types.DirectionDown } diff --git a/pkg/strategy/supertrend/linreg.go b/pkg/strategy/supertrend/linreg.go index eb47c2c7b3..65813aa7f9 100644 --- a/pkg/strategy/supertrend/linreg.go +++ b/pkg/strategy/supertrend/linreg.go @@ -19,11 +19,11 @@ type LinReg struct { } // Last slope of linear regression baseline -func (lr *LinReg) Last() float64 { +func (lr *LinReg) Last(int) float64 { if lr.Values.Length() == 0 { return 0.0 } - return lr.Values.Last() + return lr.Values.Last(0) } // Index returns the slope of specified index @@ -68,7 +68,7 @@ func (lr *LinReg) Update(kline types.KLine) { startPrice := endPrice + slope*(length-1) lr.Values.Push((endPrice - startPrice) / (length - 1)) - log.Debugf("linear regression baseline slope: %f", lr.Last()) + log.Debugf("linear regression baseline slope: %f", lr.Last(0)) } func (lr *LinReg) BindK(target indicator.KLineClosedEmitter, symbol string, interval types.Interval) { @@ -96,9 +96,9 @@ func (lr *LinReg) GetSignal() types.Direction { var lrSignal types.Direction = types.DirectionNone switch { - case lr.Last() > 0: + case lr.Last(0) > 0: lrSignal = types.DirectionUp - case lr.Last() < 0: + case lr.Last(0) < 0: lrSignal = types.DirectionDown } diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 1003c621ee..ac39434563 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -110,7 +110,7 @@ func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { // Accumulated profit MA r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) - r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last()) + r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last(0)) // Accumulated Fee r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) @@ -606,14 +606,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.currentStopLossPrice = kline.GetLow() } if s.TakeProfitAtrMultiplier > 0 { - s.currentTakeProfitPrice = closePrice.Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier)) + s.currentTakeProfitPrice = closePrice.Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last(0) * s.TakeProfitAtrMultiplier)) } } else if side == types.SideTypeSell { if s.StopLossByTriggeringK { s.currentStopLossPrice = kline.GetHigh() } if s.TakeProfitAtrMultiplier > 0 { - s.currentTakeProfitPrice = closePrice.Sub(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier)) + s.currentTakeProfitPrice = closePrice.Sub(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last(0) * s.TakeProfitAtrMultiplier)) } } diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index d59788a3a0..b688d0cb4f 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -477,19 +477,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } - if s.longTermEMA != nil && closePrice.Float64() < s.longTermEMA.Last() { + if s.longTermEMA != nil && closePrice.Float64() < s.longTermEMA.Last(0) { bbgo.Notify("%s: closed price is below the long term moving average line %f, skipping this support", s.Symbol, - s.longTermEMA.Last(), + s.longTermEMA.Last(0), kline, ) return } - if s.triggerEMA != nil && closePrice.Float64() > s.triggerEMA.Last() { + if s.triggerEMA != nil && closePrice.Float64() > s.triggerEMA.Last(0) { bbgo.Notify("%s: closed price is above the trigger moving average line %f, skipping this support", s.Symbol, - s.triggerEMA.Last(), + s.triggerEMA.Last(0), kline, ) return @@ -499,8 +499,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.Notify("Found %s support: the close price %s is below trigger EMA %f and above long term EMA %f and volume %s > minimum volume %s", s.Symbol, closePrice.String(), - s.triggerEMA.Last(), - s.longTermEMA.Last(), + s.triggerEMA.Last(0), + s.longTermEMA.Last(0), kline.Volume.String(), s.MinVolume.String(), kline) diff --git a/pkg/strategy/swing/strategy.go b/pkg/strategy/swing/strategy.go index da85591ce2..fdb1234b4f 100644 --- a/pkg/strategy/swing/strategy.go +++ b/pkg/strategy/swing/strategy.go @@ -15,7 +15,7 @@ const ID = "swing" // Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data. type Float64Indicator interface { - Last() float64 + Last(int) float64 } func init() { @@ -104,7 +104,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - movingAveragePrice := inc.Last() + movingAveragePrice := inc.Last(0) // skip it if it's near zero if movingAveragePrice < 0.0001 { diff --git a/pkg/strategy/techsignal/strategy.go b/pkg/strategy/techsignal/strategy.go index bf3326dd37..29a7c56be8 100644 --- a/pkg/strategy/techsignal/strategy.go +++ b/pkg/strategy/techsignal/strategy.go @@ -187,7 +187,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) } - var lastMA = ma.Last() + var lastMA = ma.Last(0) // skip if the closed price is above the moving average if closePrice.Float64() > lastMA { diff --git a/pkg/strategy/trendtrader/trend.go b/pkg/strategy/trendtrader/trend.go index be12f575a9..81876ef30a 100644 --- a/pkg/strategy/trendtrader/trend.go +++ b/pkg/strategy/trendtrader/trend.go @@ -65,15 +65,15 @@ func (s *TrendLine) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gene supportSlope2 := 0. session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - if s.pivotHigh.Last() != resistancePrices.Last() { - resistancePrices.Update(s.pivotHigh.Last()) + if s.pivotHigh.Last(0) != resistancePrices.Last(0) { + resistancePrices.Update(s.pivotHigh.Last(0)) resistanceDuration.Update(pivotHighDurationCounter) pivotHighDurationCounter = 0 } else { pivotHighDurationCounter++ } - if s.pivotLow.Last() != supportPrices.Last() { - supportPrices.Update(s.pivotLow.Last()) + if s.pivotLow.Last(0) != supportPrices.Last(0) { + supportPrices.Update(s.pivotLow.Last(0)) supportDuration.Update(pivotLowDurationCounter) pivotLowDurationCounter = 0 } else { @@ -95,8 +95,8 @@ func (s *TrendLine) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gene if converge(resistanceSlope, supportSlope) { // y = mx+b - currentResistance := resistanceSlope*pivotHighDurationCounter + resistancePrices.Last() - currentSupport := supportSlope*pivotLowDurationCounter + supportPrices.Last() + currentResistance := resistanceSlope*pivotHighDurationCounter + resistancePrices.Last(0) + currentSupport := supportSlope*pivotLowDurationCounter + supportPrices.Last(0) log.Info(currentResistance, currentSupport, kline.Close) if kline.High.Float64() > currentResistance { diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 7ff87ce99e..a912cfeb17 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -302,8 +302,8 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or var pips = s.Pips if s.EnableBollBandMargin { - lastDownBand := fixedpoint.NewFromFloat(s.boll.DownBand.Last()) - lastUpBand := fixedpoint.NewFromFloat(s.boll.UpBand.Last()) + lastDownBand := fixedpoint.NewFromFloat(s.boll.DownBand.Last(0)) + lastUpBand := fixedpoint.NewFromFloat(s.boll.UpBand.Last(0)) if lastUpBand.IsZero() || lastDownBand.IsZero() { log.Warnf("bollinger band value is zero, skipping") diff --git a/pkg/types/filter.go b/pkg/types/filter.go index 58e0a966ec..bdb9e5de07 100644 --- a/pkg/types/filter.go +++ b/pkg/types/filter.go @@ -7,11 +7,7 @@ type FilterResult struct { c []int } -func (f *FilterResult) Last() float64 { - return f.Index(0) -} - -func (f *FilterResult) Index(j int) float64 { +func (f *FilterResult) Last(j int) float64 { if j >= f.length { return 0 } @@ -37,6 +33,10 @@ func (f *FilterResult) Index(j int) float64 { return 0 } +func (f *FilterResult) Index(j int) float64 { + return f.Last(j) +} + func (f *FilterResult) Length() int { return f.length } diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index 92c751a2ae..d8fe3870c6 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -12,186 +12,17 @@ import ( "github.com/c9s/bbgo/pkg/datatype/floats" ) -// Super basic Series type that simply holds the float64 data -// with size limit (the only difference compare to float64slice) -type Queue struct { - SeriesBase - arr []float64 - size int -} - -func NewQueue(size int) *Queue { - out := &Queue{ - arr: make([]float64, 0, size), - size: size, - } - out.SeriesBase.Series = out - return out -} - -func (inc *Queue) Last() float64 { - if len(inc.arr) == 0 { - return 0 - } - return inc.arr[len(inc.arr)-1] -} - -func (inc *Queue) Index(i int) float64 { - if len(inc.arr)-i-1 < 0 { - return 0 - } - return inc.arr[len(inc.arr)-i-1] -} - -func (inc *Queue) Length() int { - return len(inc.arr) -} - -func (inc *Queue) Clone() *Queue { - out := &Queue{ - arr: inc.arr[:], - size: inc.size, - } - out.SeriesBase.Series = out - return out -} - -func (inc *Queue) Update(v float64) { - inc.arr = append(inc.arr, v) - if len(inc.arr) > inc.size { - inc.arr = inc.arr[len(inc.arr)-inc.size:] - } -} - -var _ UpdatableSeriesExtend = &Queue{} - // Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data. type Float64Indicator interface { - Last() float64 -} - -// The interface maps to pinescript basic type `series` -// Access the internal historical data from the latest to the oldest -// Index(0) always maps to Last() -type Series interface { - Last() float64 - Index(int) float64 - Length() int -} - -type SeriesExtend interface { - Series - Sum(limit ...int) float64 - Mean(limit ...int) float64 - Abs() SeriesExtend - Predict(lookback int, offset ...int) float64 - NextCross(b Series, lookback int) (int, float64, bool) - CrossOver(b Series) BoolSeries - CrossUnder(b Series) BoolSeries - Highest(lookback int) float64 - Lowest(lookback int) float64 - Add(b interface{}) SeriesExtend - Minus(b interface{}) SeriesExtend - Div(b interface{}) SeriesExtend - Mul(b interface{}) SeriesExtend - Dot(b interface{}, limit ...int) float64 - Array(limit ...int) (result []float64) - Reverse(limit ...int) (result floats.Slice) - Change(offset ...int) SeriesExtend - PercentageChange(offset ...int) SeriesExtend - Stdev(params ...int) float64 - Rolling(window int) *RollingResult - Shift(offset int) SeriesExtend - Skew(length int) float64 - Variance(length int) float64 - Covariance(b Series, length int) float64 - Correlation(b Series, length int, method ...CorrFunc) float64 - AutoCorrelation(length int, lag ...int) float64 - Rank(length int) SeriesExtend - Sigmoid() SeriesExtend - Softmax(window int) SeriesExtend - Entropy(window int) float64 - CrossEntropy(b Series, window int) float64 - Filter(b func(i int, value float64) bool, length int) SeriesExtend -} - -func NewSeries(a Series) SeriesExtend { - return &SeriesBase{ - Series: a, - } -} - -type UpdatableSeries interface { - Series - Update(float64) -} - -type UpdatableSeriesExtend interface { - SeriesExtend - Update(float64) -} - -func Clone(u UpdatableSeriesExtend) UpdatableSeriesExtend { - method, ok := reflect.TypeOf(u).MethodByName("Clone") - if ok { - out := method.Func.Call([]reflect.Value{reflect.ValueOf(u)}) - return out[0].Interface().(UpdatableSeriesExtend) - } - panic("method Clone not exist") -} - -func TestUpdate(u UpdatableSeriesExtend, input float64) UpdatableSeriesExtend { - method, ok := reflect.TypeOf(u).MethodByName("TestUpdate") - if ok { - out := method.Func.Call([]reflect.Value{reflect.ValueOf(u), reflect.ValueOf(input)}) - return out[0].Interface().(UpdatableSeriesExtend) - } - panic("method TestUpdate not exist") -} - -// The interface maps to pinescript basic type `series` for bool type -// Access the internal historical data from the latest to the oldest -// Index(0) always maps to Last() -type BoolSeries interface { - Last() bool - Index(int) bool - Length() int -} - -// Calculate sum of the series -// if limit is given, will only sum first limit numbers (a.Index[0..limit]) -// otherwise will sum all elements -func Sum(a Series, limit ...int) (sum float64) { - l := a.Length() - if len(limit) > 0 && limit[0] < l { - l = limit[0] - } - for i := 0; i < l; i++ { - sum += a.Index(i) - } - return sum -} - -// Calculate the average value of the series -// if limit is given, will only calculate the average of first limit numbers (a.Index[0..limit]) -// otherwise will operate on all elements -func Mean(a Series, limit ...int) (mean float64) { - l := a.Length() - if l == 0 { - return 0 - } - if len(limit) > 0 && limit[0] < l { - l = limit[0] - } - return Sum(a, l) / float64(l) + Last(i int) float64 } type AbsResult struct { a Series } -func (a *AbsResult) Last() float64 { - return math.Abs(a.a.Last()) +func (a *AbsResult) Last(i int) float64 { + return math.Abs(a.a.Last(i)) } func (a *AbsResult) Index(i int) float64 { @@ -282,9 +113,9 @@ func (c *CrossResult) Last() bool { return false } if c.isOver { - return c.a.Last()-c.b.Last() > 0 && c.a.Index(1)-c.b.Index(1) < 0 + return c.a.Last(0)-c.b.Last(0) > 0 && c.a.Index(1)-c.b.Index(1) < 0 } else { - return c.a.Last()-c.b.Last() < 0 && c.a.Index(1)-c.b.Index(1) > 0 + return c.a.Last(0)-c.b.Last(0) < 0 && c.a.Index(1)-c.b.Index(1) > 0 } } @@ -328,7 +159,7 @@ func Highest(a Series, lookback int) float64 { if lookback > a.Length() { lookback = a.Length() } - highest := a.Last() + highest := a.Last(0) for i := 1; i < lookback; i++ { current := a.Index(i) if highest < current { @@ -342,7 +173,7 @@ func Lowest(a Series, lookback int) float64 { if lookback > a.Length() { lookback = a.Length() } - lowest := a.Last() + lowest := a.Last(0) for i := 1; i < lookback; i++ { current := a.Index(i) if lowest > current { @@ -354,7 +185,7 @@ func Lowest(a Series, lookback int) float64 { type NumberSeries float64 -func (a NumberSeries) Last() float64 { +func (a NumberSeries) Last(_ int) float64 { return float64(a) } @@ -384,12 +215,12 @@ func Add(a interface{}, b interface{}) SeriesExtend { return NewSeries(&AddSeriesResult{aa, bb}) } -func (a *AddSeriesResult) Last() float64 { - return a.a.Last() + a.b.Last() +func (a *AddSeriesResult) Last(i int) float64 { + return a.a.Last(i) + a.b.Last(i) } func (a *AddSeriesResult) Index(i int) float64 { - return a.a.Index(i) + a.b.Index(i) + return a.Last(i) } func (a *AddSeriesResult) Length() int { @@ -415,12 +246,12 @@ func Sub(a interface{}, b interface{}) SeriesExtend { return NewSeries(&MinusSeriesResult{aa, bb}) } -func (a *MinusSeriesResult) Last() float64 { - return a.a.Last() - a.b.Last() +func (a *MinusSeriesResult) Last(i int) float64 { + return a.a.Index(i) - a.b.Index(i) } func (a *MinusSeriesResult) Index(i int) float64 { - return a.a.Index(i) - a.b.Index(i) + return a.Last(i) } func (a *MinusSeriesResult) Length() int { @@ -471,12 +302,12 @@ type DivSeriesResult struct { b Series } -func (a *DivSeriesResult) Last() float64 { - return a.a.Last() / a.b.Last() +func (a *DivSeriesResult) Last(i int) float64 { + return a.a.Index(i) / a.b.Index(i) } func (a *DivSeriesResult) Index(i int) float64 { - return a.a.Index(i) / a.b.Index(i) + return a.Last(i) } func (a *DivSeriesResult) Length() int { @@ -502,12 +333,12 @@ type MulSeriesResult struct { b Series } -func (a *MulSeriesResult) Last() float64 { - return a.a.Last() * a.b.Last() +func (a *MulSeriesResult) Last(i int) float64 { + return a.a.Index(i) * a.b.Index(i) } func (a *MulSeriesResult) Index(i int) float64 { - return a.a.Index(i) * a.b.Index(i) + return a.Last(i) } func (a *MulSeriesResult) Length() int { @@ -654,18 +485,15 @@ type ChangeResult struct { offset int } -func (c *ChangeResult) Last() float64 { - if c.offset >= c.a.Length() { +func (c *ChangeResult) Last(i int) float64 { + if i+c.offset >= c.a.Length() { return 0 } - return c.a.Last() - c.a.Index(c.offset) + return c.a.Index(i) - c.a.Index(i+c.offset) } func (c *ChangeResult) Index(i int) float64 { - if i+c.offset >= c.a.Length() { - return 0 - } - return c.a.Index(i) - c.a.Index(i+c.offset) + return c.Last(i) } func (c *ChangeResult) Length() int { @@ -692,18 +520,15 @@ type PercentageChangeResult struct { offset int } -func (c *PercentageChangeResult) Last() float64 { - if c.offset >= c.a.Length() { +func (c *PercentageChangeResult) Last(i int) float64 { + if i+c.offset >= c.a.Length() { return 0 } - return c.a.Last()/c.a.Index(c.offset) - 1 + return c.a.Index(i)/c.a.Index(i+c.offset) - 1 } func (c *PercentageChangeResult) Index(i int) float64 { - if i+c.offset >= c.a.Length() { - return 0 - } - return c.a.Index(i)/c.a.Index(i+c.offset) - 1 + return c.Last(i) } func (c *PercentageChangeResult) Length() int { @@ -902,25 +727,21 @@ type ShiftResult struct { offset int } -func (inc *ShiftResult) Last() float64 { - if inc.offset < 0 { - return 0 - } - if inc.offset > inc.a.Length() { - return 0 - } - return inc.a.Index(inc.offset) -} -func (inc *ShiftResult) Index(i int) float64 { +func (inc *ShiftResult) Last(i int) float64 { if inc.offset+i < 0 { return 0 } if inc.offset+i > inc.a.Length() { return 0 } + return inc.a.Index(inc.offset + i) } +func (inc *ShiftResult) Index(i int) float64 { + return inc.Last(i) +} + func (inc *ShiftResult) Length() int { return inc.a.Length() - inc.offset } @@ -940,14 +761,16 @@ type SliceView struct { length int } -func (s *SliceView) Last() float64 { - return s.a.Index(s.start) -} -func (s *SliceView) Index(i int) float64 { +func (s *SliceView) Last(i int) float64 { if i >= s.length { return 0 } - return s.a.Index(i + s.start) + + return s.a.Last(i + s.start) +} + +func (s *SliceView) Index(i int) float64 { + return s.Last(i) } func (s *SliceView) Length() int { @@ -980,30 +803,6 @@ func Rolling(a Series, window int) *RollingResult { return &RollingResult{a, window} } -type SigmoidResult struct { - a Series -} - -func (s *SigmoidResult) Last() float64 { - return 1. / (1. + math.Exp(-s.a.Last())) -} - -func (s *SigmoidResult) Index(i int) float64 { - return 1. / (1. + math.Exp(-s.a.Index(i))) -} - -func (s *SigmoidResult) Length() int { - return s.a.Length() -} - -// Sigmoid returns the input values in range of -1 to 1 -// along the sigmoid or s-shaped curve. -// Commonly used in machine learning while training neural networks -// as an activation function. -func Sigmoid(a Series) SeriesExtend { - return NewSeries(&SigmoidResult{a}) -} - // SoftMax returns the input value in the range of 0 to 1 // with sum of all the probabilities being equal to one. // It is commonly used in machine learning neural networks. diff --git a/pkg/types/indicator_test.go b/pkg/types/indicator_test.go index 458d9dcf25..19c265ea81 100644 --- a/pkg/types/indicator_test.go +++ b/pkg/types/indicator_test.go @@ -15,7 +15,7 @@ import ( func TestQueue(t *testing.T) { zeroq := NewQueue(0) - assert.Equal(t, zeroq.Last(), 0.) + assert.Equal(t, zeroq.Last(0), 0.) assert.Equal(t, zeroq.Index(0), 0.) zeroq.Update(1.) assert.Equal(t, zeroq.Length(), 0) @@ -23,7 +23,7 @@ func TestQueue(t *testing.T) { func TestFloat(t *testing.T) { var a Series = Sub(3., 2.) - assert.Equal(t, a.Last(), 1.) + assert.Equal(t, a.Last(0), 1.) assert.Equal(t, a.Index(100), 1.) } @@ -47,7 +47,7 @@ func TestFloat64Slice(t *testing.T) { var c Series = Sub(&a, &b) a = append(a, 4.0) b = append(b, 3.0) - assert.Equal(t, c.Last(), 1.) + assert.Equal(t, c.Last(0), 1.) } /* @@ -128,7 +128,7 @@ func TestSigmoid(t *testing.T) { out := Sigmoid(&a) r := floats.Slice{0.9525741268224334, 0.7310585786300049, 0.8909031788043871} for i := 0; i < out.Length(); i++ { - assert.InDelta(t, r.Index(i), out.Index(i), 0.001) + assert.InDelta(t, r.Index(i), out.Index(i), 0.001, "i=%d", i) } } @@ -142,7 +142,7 @@ func TestAdd(t *testing.T) { var a NumberSeries = 3.0 var b NumberSeries = 2.0 out := Add(&a, &b) - assert.Equal(t, out.Last(), 5.0) + assert.Equal(t, out.Last(0), 5.0) assert.Equal(t, out.Index(0), 5.0) assert.Equal(t, out.Length(), math.MaxInt32) } @@ -151,16 +151,16 @@ func TestDiv(t *testing.T) { a := floats.Slice{3.0, 1.0, 2.0} b := NumberSeries(2.0) out := Div(&a, &b) - assert.Equal(t, out.Last(), 1.0) - assert.Equal(t, out.Length(), 3) - assert.Equal(t, out.Index(1), 0.5) + assert.Equal(t, 1.0, out.Last(0)) + assert.Equal(t, 3, out.Length()) + assert.Equal(t, 0.5, out.Index(1)) } func TestMul(t *testing.T) { a := floats.Slice{3.0, 1.0, 2.0} b := NumberSeries(2.0) out := Mul(&a, &b) - assert.Equal(t, out.Last(), 4.0) + assert.Equal(t, out.Last(0), 4.0) assert.Equal(t, out.Length(), 3) assert.Equal(t, out.Index(1), 2.0) } @@ -183,11 +183,11 @@ func TestSwitchInterface(t *testing.T) { var d float32 = 4.0 var df float64 = 4.0 var e float64 = 5.0 - assert.Equal(t, switchIface(a).Last(), af) - assert.Equal(t, switchIface(b).Last(), bf) - assert.Equal(t, switchIface(c).Last(), cf) - assert.Equal(t, switchIface(d).Last(), df) - assert.Equal(t, switchIface(e).Last(), e) + assert.Equal(t, switchIface(a).Last(0), af) + assert.Equal(t, switchIface(b).Last(0), bf) + assert.Equal(t, switchIface(c).Last(0), cf) + assert.Equal(t, switchIface(d).Last(0), df) + assert.Equal(t, switchIface(e).Last(0), e) } // from https://en.wikipedia.org/wiki/Logistic_regression @@ -223,8 +223,8 @@ func TestClone(t *testing.T) { a.Update(3.) b := Clone(a) b.Update(4.) - assert.Equal(t, a.Last(), 3.) - assert.Equal(t, b.Last(), 4.) + assert.Equal(t, a.Last(0), 3.) + assert.Equal(t, b.Last(0), 4.) } func TestPlot(t *testing.T) { @@ -244,6 +244,6 @@ func TestFilter(t *testing.T) { return val > 0 }, 4) assert.Equal(t, b.Length(), 4) - assert.Equal(t, b.Last(), 1000.) + assert.Equal(t, b.Last(0), 1000.) assert.Equal(t, b.Sum(3), 1200.) } diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 9f8dd5d724..fcc358176b 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -597,7 +597,7 @@ type KLineSeries struct { kv KValueType } -func (k *KLineSeries) Last() float64 { +func (k *KLineSeries) Last(int) float64 { length := len(*k.lines) switch k.kv { case kOpUnknown: diff --git a/pkg/types/queue.go b/pkg/types/queue.go new file mode 100644 index 0000000000..5205f802f8 --- /dev/null +++ b/pkg/types/queue.go @@ -0,0 +1,51 @@ +package types + +// Super basic Series type that simply holds the float64 data +// with size limit (the only difference compare to float64slice) +type Queue struct { + SeriesBase + arr []float64 + size int +} + +func NewQueue(size int) *Queue { + out := &Queue{ + arr: make([]float64, 0, size), + size: size, + } + out.SeriesBase.Series = out + return out +} + +func (inc *Queue) Last(i int) float64 { + if i < 0 || len(inc.arr)-i-1 < 0 { + return 0 + } + return inc.arr[len(inc.arr)-1-i] +} + +func (inc *Queue) Index(i int) float64 { + return inc.Last(i) +} + +func (inc *Queue) Length() int { + return len(inc.arr) +} + +func (inc *Queue) Clone() *Queue { + out := &Queue{ + arr: inc.arr[:], + size: inc.size, + } + out.SeriesBase.Series = out + return out +} + +func (inc *Queue) Update(v float64) { + inc.arr = append(inc.arr, v) + if len(inc.arr) > inc.size { + inc.arr = inc.arr[len(inc.arr)-inc.size:] + } +} + +var _ UpdatableSeriesExtend = &Queue{} diff --git a/pkg/types/series.go b/pkg/types/series.go new file mode 100644 index 0000000000..1c4289ee28 --- /dev/null +++ b/pkg/types/series.go @@ -0,0 +1,123 @@ +package types + +import ( + "reflect" + + "github.com/c9s/bbgo/pkg/datatype/floats" +) + +// The interface maps to pinescript basic type `series` +// Access the internal historical data from the latest to the oldest +// Index(0) always maps to Last() +type Series interface { + Last(i int) float64 + Index(i int) float64 + Length() int +} + +type SeriesExtend interface { + Series + Sum(limit ...int) float64 + Mean(limit ...int) float64 + Abs() SeriesExtend + Predict(lookback int, offset ...int) float64 + NextCross(b Series, lookback int) (int, float64, bool) + CrossOver(b Series) BoolSeries + CrossUnder(b Series) BoolSeries + Highest(lookback int) float64 + Lowest(lookback int) float64 + Add(b interface{}) SeriesExtend + Minus(b interface{}) SeriesExtend + Div(b interface{}) SeriesExtend + Mul(b interface{}) SeriesExtend + Dot(b interface{}, limit ...int) float64 + Array(limit ...int) (result []float64) + Reverse(limit ...int) (result floats.Slice) + Change(offset ...int) SeriesExtend + PercentageChange(offset ...int) SeriesExtend + Stdev(params ...int) float64 + Rolling(window int) *RollingResult + Shift(offset int) SeriesExtend + Skew(length int) float64 + Variance(length int) float64 + Covariance(b Series, length int) float64 + Correlation(b Series, length int, method ...CorrFunc) float64 + AutoCorrelation(length int, lag ...int) float64 + Rank(length int) SeriesExtend + Sigmoid() SeriesExtend + Softmax(window int) SeriesExtend + Entropy(window int) float64 + CrossEntropy(b Series, window int) float64 + Filter(b func(i int, value float64) bool, length int) SeriesExtend +} + +func NewSeries(a Series) SeriesExtend { + return &SeriesBase{ + Series: a, + } +} + +type UpdatableSeries interface { + Series + Update(float64) +} + +type UpdatableSeriesExtend interface { + SeriesExtend + Update(float64) +} + +func Clone(u UpdatableSeriesExtend) UpdatableSeriesExtend { + method, ok := reflect.TypeOf(u).MethodByName("Clone") + if ok { + out := method.Func.Call([]reflect.Value{reflect.ValueOf(u)}) + return out[0].Interface().(UpdatableSeriesExtend) + } + panic("method Clone not exist") +} + +func TestUpdate(u UpdatableSeriesExtend, input float64) UpdatableSeriesExtend { + method, ok := reflect.TypeOf(u).MethodByName("TestUpdate") + if ok { + out := method.Func.Call([]reflect.Value{reflect.ValueOf(u), reflect.ValueOf(input)}) + return out[0].Interface().(UpdatableSeriesExtend) + } + panic("method TestUpdate not exist") +} + +// The interface maps to pinescript basic type `series` for bool type +// Access the internal historical data from the latest to the oldest +// Index(0) always maps to Last() +type BoolSeries interface { + Last() bool + Index(int) bool + Length() int +} + +// Calculate sum of the series +// if limit is given, will only sum first limit numbers (a.Index[0..limit]) +// otherwise will sum all elements +func Sum(a Series, limit ...int) (sum float64) { + l := a.Length() + if len(limit) > 0 && limit[0] < l { + l = limit[0] + } + for i := 0; i < l; i++ { + sum += a.Index(i) + } + return sum +} + +// Calculate the average value of the series +// if limit is given, will only calculate the average of first limit numbers (a.Index[0..limit]) +// otherwise will operate on all elements +func Mean(a Series, limit ...int) (mean float64) { + l := a.Length() + if l == 0 { + return 0 + } + if len(limit) > 0 && limit[0] < l { + l = limit[0] + } + return Sum(a, l) / float64(l) +} diff --git a/pkg/types/seriesbase_imp.go b/pkg/types/seriesbase_imp.go index feb95645af..1e52ce5449 100644 --- a/pkg/types/seriesbase_imp.go +++ b/pkg/types/seriesbase_imp.go @@ -1,6 +1,8 @@ package types -import "github.com/c9s/bbgo/pkg/datatype/floats" +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" +) // SeriesBase is a wrapper of the Series interface // You can assign a data container that implements the Series interface @@ -13,14 +15,14 @@ func (s *SeriesBase) Index(i int) float64 { if s.Series == nil { return 0 } - return s.Series.Index(i) + return s.Series.Last(i) } -func (s *SeriesBase) Last() float64 { +func (s *SeriesBase) Last(int) float64 { if s.Series == nil { return 0 } - return s.Series.Last() + return s.Series.Last(0) } func (s *SeriesBase) Length() int { diff --git a/pkg/types/sigmoid.go b/pkg/types/sigmoid.go new file mode 100644 index 0000000000..4b28bb23b9 --- /dev/null +++ b/pkg/types/sigmoid.go @@ -0,0 +1,27 @@ +package types + +import "math" + +type SigmoidResult struct { + a Series +} + +func (s *SigmoidResult) Last(i int) float64 { + return 1. / (1. + math.Exp(-s.a.Last(i))) +} + +func (s *SigmoidResult) Index(i int) float64 { + return s.Last(i) +} + +func (s *SigmoidResult) Length() int { + return s.a.Length() +} + +// Sigmoid returns the input values in range of -1 to 1 +// along the sigmoid or s-shaped curve. +// Commonly used in machine learning while training neural networks +// as an activation function. +func Sigmoid(a Series) SeriesExtend { + return NewSeries(&SigmoidResult{a}) +} From c9c13b201384eef40642e90d80175e9f3de46c14 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 07:46:50 +0800 Subject: [PATCH 0922/1392] all: replace all Index(i) callers --- pkg/indicator/ad.go | 12 +-- pkg/indicator/alma.go | 10 +-- pkg/indicator/atr.go | 5 +- pkg/indicator/atrp.go | 5 +- pkg/indicator/emv.go | 11 +-- pkg/indicator/ewma.go | 4 - pkg/indicator/ghfilter.go | 14 +--- pkg/indicator/hull.go | 7 +- pkg/indicator/pivothigh.go | 8 +- pkg/indicator/pivotlow.go | 8 +- pkg/indicator/psar.go | 7 +- pkg/strategy/drift/driftma.go | 4 +- pkg/strategy/elliottwave/ewo.go | 6 +- pkg/strategy/ewoDgtrd/strategy.go | 6 +- pkg/strategy/factorzoo/factors/momentum.go | 15 +--- .../factorzoo/factors/price_mean_reversion.go | 18 ++--- .../factors/price_volume_divergence.go | 18 ++--- pkg/strategy/factorzoo/factors/return_rate.go | 18 ++--- .../factorzoo/factors/volume_momentum.go | 15 +--- pkg/strategy/harmonic/shark.go | 46 +++++------- pkg/strategy/irr/neg_return_rate.go | 14 +--- pkg/strategy/supertrend/linreg.go | 13 +--- pkg/types/filter.go | 4 +- pkg/types/indicator.go | 74 +++++++++---------- pkg/types/indicator_test.go | 15 ++-- pkg/types/kline.go | 21 +----- pkg/types/omega.go | 2 +- pkg/types/pca.go | 5 +- pkg/types/queue.go | 1 + pkg/types/series.go | 2 +- pkg/types/seriesbase_imp.go | 9 +-- pkg/types/sortino.go | 8 +- 32 files changed, 139 insertions(+), 266 deletions(-) diff --git a/pkg/indicator/ad.go b/pkg/indicator/ad.go index 463e304ebb..31536c8f5b 100644 --- a/pkg/indicator/ad.go +++ b/pkg/indicator/ad.go @@ -40,19 +40,11 @@ func (inc *AD) Update(high, low, cloze, volume float64) { } func (inc *AD) Last(i int) float64 { - length := len(inc.Values) - if length == 0 || length-i-1 < 0 { - return 0 - } - return inc.Values[length-i-1] + return inc.Values.Last(i) } func (inc *AD) Index(i int) float64 { - length := len(inc.Values) - if length == 0 || length-i-1 < 0 { - return 0 - } - return inc.Values[length-i-1] + return inc.Last(i) } func (inc *AD) Length() int { diff --git a/pkg/indicator/alma.go b/pkg/indicator/alma.go index 6837db7669..7f3c806b62 100644 --- a/pkg/indicator/alma.go +++ b/pkg/indicator/alma.go @@ -66,17 +66,11 @@ func (inc *ALMA) Update(value float64) { } func (inc *ALMA) Last(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - return inc.Values[len(inc.Values)-i-1] + return inc.Values.Last(i) } func (inc *ALMA) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - return inc.Values[len(inc.Values)-i-1] + return inc.Last(i) } func (inc *ALMA) Length() int { diff --git a/pkg/indicator/atr.go b/pkg/indicator/atr.go index 335c36e2a5..d0ed64b839 100644 --- a/pkg/indicator/atr.go +++ b/pkg/indicator/atr.go @@ -89,10 +89,7 @@ func (inc *ATR) Last(i int) float64 { } func (inc *ATR) Index(i int) float64 { - if inc.RMA == nil { - return 0 - } - return inc.RMA.Index(i) + return inc.Last(i) } func (inc *ATR) Length() int { diff --git a/pkg/indicator/atrp.go b/pkg/indicator/atrp.go index 4e85589047..d97bb0af8b 100644 --- a/pkg/indicator/atrp.go +++ b/pkg/indicator/atrp.go @@ -81,10 +81,7 @@ func (inc *ATRP) Last(i int) float64 { } func (inc *ATRP) Index(i int) float64 { - if inc.RMA == nil { - return 0 - } - return inc.RMA.Index(i) + return inc.Last(i) } func (inc *ATRP) Length() int { diff --git a/pkg/indicator/emv.go b/pkg/indicator/emv.go index a391a08e70..36d6a36b00 100644 --- a/pkg/indicator/emv.go +++ b/pkg/indicator/emv.go @@ -48,13 +48,6 @@ func (inc *EMV) Update(high, low, vol float64) { inc.Values.Update(result) } -func (inc *EMV) Index(i int) float64 { - if inc.Values == nil { - return 0 - } - return inc.Values.Index(i) -} - func (inc *EMV) Last(i int) float64 { if inc.Values == nil { return 0 @@ -63,6 +56,10 @@ func (inc *EMV) Last(i int) float64 { return inc.Values.Last(i) } +func (inc *EMV) Index(i int) float64 { + return inc.Last(i) +} + func (inc *EMV) Length() int { if inc.Values == nil { return 0 diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index e05f30a3cb..8d7b136989 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -55,10 +55,6 @@ func (inc *EWMA) Update(value float64) { } func (inc *EWMA) Last(i int) float64 { - if len(inc.Values) == 0 { - return 0 - } - return inc.Values.Last(i) } diff --git a/pkg/indicator/ghfilter.go b/pkg/indicator/ghfilter.go index e5dea876f2..52ea82bba2 100644 --- a/pkg/indicator/ghfilter.go +++ b/pkg/indicator/ghfilter.go @@ -45,24 +45,18 @@ func (inc *GHFilter) update(value, uncertainty float64) { inc.lastMeasurement = value } -func (inc *GHFilter) Index(i int) float64 { - return inc.Last(i) -} - func (inc *GHFilter) Length() int { - if inc.Values == nil { - return 0 - } return inc.Values.Length() } func (inc *GHFilter) Last(i int) float64 { - if inc.Values == nil { - return 0.0 - } return inc.Values.Last(i) } +func (inc *GHFilter) Index(i int) float64 { + return inc.Last(i) +} + // interfaces implementation check var _ Simple = &GHFilter{} var _ types.SeriesExtend = &GHFilter{} diff --git a/pkg/indicator/hull.go b/pkg/indicator/hull.go index 7a432d39af..994d5d30d4 100644 --- a/pkg/indicator/hull.go +++ b/pkg/indicator/hull.go @@ -44,14 +44,11 @@ func (inc *HULL) Last(i int) float64 { if inc.result == nil { return 0 } - return inc.result.Index(i) + return inc.result.Last(i) } func (inc *HULL) Index(i int) float64 { - if inc.result == nil { - return 0 - } - return inc.result.Index(i) + return inc.Last(i) } func (inc *HULL) Length() int { diff --git a/pkg/indicator/pivothigh.go b/pkg/indicator/pivothigh.go index 52fe7be0b4..ec90f57f79 100644 --- a/pkg/indicator/pivothigh.go +++ b/pkg/indicator/pivothigh.go @@ -24,12 +24,8 @@ func (inc *PivotHigh) Length() int { return inc.Values.Length() } -func (inc *PivotHigh) Last(int) float64 { - if len(inc.Values) == 0 { - return 0.0 - } - - return inc.Values.Last(0) +func (inc *PivotHigh) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *PivotHigh) Update(value float64) { diff --git a/pkg/indicator/pivotlow.go b/pkg/indicator/pivotlow.go index 7bdbd58e67..2023fc9417 100644 --- a/pkg/indicator/pivotlow.go +++ b/pkg/indicator/pivotlow.go @@ -24,12 +24,8 @@ func (inc *PivotLow) Length() int { return inc.Values.Length() } -func (inc *PivotLow) Last(int) float64 { - if len(inc.Values) == 0 { - return 0.0 - } - - return inc.Values.Last(0) +func (inc *PivotLow) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *PivotLow) Update(value float64) { diff --git a/pkg/indicator/psar.go b/pkg/indicator/psar.go index 5154c1b547..0d19363ac6 100644 --- a/pkg/indicator/psar.go +++ b/pkg/indicator/psar.go @@ -34,11 +34,8 @@ type PSAR struct { UpdateCallbacks []func(value float64) } -func (inc *PSAR) Last(int) float64 { - if len(inc.Values) == 0 { - return 0 - } - return inc.Values.Last(0) +func (inc *PSAR) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *PSAR) Length() int { diff --git a/pkg/strategy/drift/driftma.go b/pkg/strategy/drift/driftma.go index ba0468cdcb..a737ed4d1c 100644 --- a/pkg/strategy/drift/driftma.go +++ b/pkg/strategy/drift/driftma.go @@ -24,8 +24,8 @@ func (s *DriftMA) Update(value, weight float64) { s.ma2.Update(s.drift.Last(0)) } -func (s *DriftMA) Last(int) float64 { - return s.ma2.Last(0) +func (s *DriftMA) Last(i int) float64 { + return s.ma2.Last(i) } func (s *DriftMA) Index(i int) float64 { diff --git a/pkg/strategy/elliottwave/ewo.go b/pkg/strategy/elliottwave/ewo.go index f86706b183..bbe83f4882 100644 --- a/pkg/strategy/elliottwave/ewo.go +++ b/pkg/strategy/elliottwave/ewo.go @@ -8,11 +8,11 @@ type ElliottWave struct { } func (s *ElliottWave) Index(i int) float64 { - return s.maQuick.Index(i)/s.maSlow.Index(i) - 1.0 + return s.Last(i) } -func (s *ElliottWave) Last(int) float64 { - return s.maQuick.Last(0)/s.maSlow.Last(0) - 1.0 +func (s *ElliottWave) Last(i int) float64 { + return s.maQuick.Index(i)/s.maSlow.Index(i) - 1.0 } func (s *ElliottWave) Length() int { diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 50af7f4bf2..b642036392 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -180,11 +180,11 @@ type VWEMA struct { V types.UpdatableSeries } -func (inc *VWEMA) Last(int) float64 { - return inc.PV.Last(0) / inc.V.Last(0) +func (inc *VWEMA) Index(i int) float64 { + return inc.Last(i) } -func (inc *VWEMA) Index(i int) float64 { +func (inc *VWEMA) Last(i int) float64 { if i >= inc.PV.Length() { return 0 } diff --git a/pkg/strategy/factorzoo/factors/momentum.go b/pkg/strategy/factorzoo/factors/momentum.go index 827af09760..e491a3848e 100644 --- a/pkg/strategy/factorzoo/factors/momentum.go +++ b/pkg/strategy/factorzoo/factors/momentum.go @@ -31,23 +31,14 @@ type MOM struct { } func (inc *MOM) Index(i int) float64 { - if inc.Values == nil { - return 0 - } - return inc.Values.Last(i) + return inc.Last(i) } -func (inc *MOM) Last(int) float64 { - if inc.Values.Length() == 0 { - return 0 - } - return inc.Values.Last(0) +func (inc *MOM) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *MOM) Length() int { - if inc.Values == nil { - return 0 - } return inc.Values.Length() } diff --git a/pkg/strategy/factorzoo/factors/price_mean_reversion.go b/pkg/strategy/factorzoo/factors/price_mean_reversion.go index 7143e547cb..fbc4c5ad4f 100644 --- a/pkg/strategy/factorzoo/factors/price_mean_reversion.go +++ b/pkg/strategy/factorzoo/factors/price_mean_reversion.go @@ -19,8 +19,8 @@ type PMR struct { types.IntervalWindow types.SeriesBase - Values floats.Slice - SMA *indicator.SMA + Values floats.Slice + SMA *indicator.SMA EndTime time.Time updateCallbacks []func(value float64) @@ -40,20 +40,12 @@ func (inc *PMR) Update(price float64) { } } -func (inc *PMR) Last(int) float64 { - if len(inc.Values) == 0 { - return 0 - } - - return inc.Values[len(inc.Values)-1] +func (inc *PMR) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *PMR) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - - return inc.Values[len(inc.Values)-1-i] + return inc.Last(i) } func (inc *PMR) Length() int { diff --git a/pkg/strategy/factorzoo/factors/price_volume_divergence.go b/pkg/strategy/factorzoo/factors/price_volume_divergence.go index adc20e02a3..f531eb214d 100644 --- a/pkg/strategy/factorzoo/factors/price_volume_divergence.go +++ b/pkg/strategy/factorzoo/factors/price_volume_divergence.go @@ -23,8 +23,8 @@ type PVD struct { types.IntervalWindow types.SeriesBase - Values floats.Slice - Prices *types.Queue + Values floats.Slice + Prices *types.Queue Volumes *types.Queue EndTime time.Time @@ -47,20 +47,12 @@ func (inc *PVD) Update(price float64, volume float64) { } } -func (inc *PVD) Last(int) float64 { - if len(inc.Values) == 0 { - return 0 - } - - return inc.Values[len(inc.Values)-1] +func (inc *PVD) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *PVD) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - - return inc.Values[len(inc.Values)-1-i] + return inc.Last(i) } func (inc *PVD) Length() int { diff --git a/pkg/strategy/factorzoo/factors/return_rate.go b/pkg/strategy/factorzoo/factors/return_rate.go index 3fe3d41565..114e4b9d90 100644 --- a/pkg/strategy/factorzoo/factors/return_rate.go +++ b/pkg/strategy/factorzoo/factors/return_rate.go @@ -35,20 +35,12 @@ func (inc *RR) Update(price float64) { } -func (inc *RR) Last(int) float64 { - if len(inc.Values) == 0 { - return 0 - } - - return inc.Values[len(inc.Values)-1] +func (inc *RR) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *RR) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - - return inc.Values[len(inc.Values)-1-i] + return inc.Last(i) } func (inc *RR) Length() int { @@ -101,7 +93,7 @@ func (inc *RR) LoadK(allKLines []types.KLine) { inc.EmitUpdate(inc.Last(0)) } -//func calculateReturn(klines []types.KLine, window int, val KLineValueMapper) (float64, error) { +// func calculateReturn(klines []types.KLine, window int, val KLineValueMapper) (float64, error) { // length := len(klines) // if length == 0 || length < window { // return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) @@ -110,4 +102,4 @@ func (inc *RR) LoadK(allKLines []types.KLine) { // rate := val(klines[length-1])/val(klines[length-2]) - 1 // // return rate, nil -//} +// } diff --git a/pkg/strategy/factorzoo/factors/volume_momentum.go b/pkg/strategy/factorzoo/factors/volume_momentum.go index 87029ed9ed..02a4c32fe2 100644 --- a/pkg/strategy/factorzoo/factors/volume_momentum.go +++ b/pkg/strategy/factorzoo/factors/volume_momentum.go @@ -30,23 +30,14 @@ type VMOM struct { } func (inc *VMOM) Index(i int) float64 { - if inc.Values == nil { - return 0 - } - return inc.Values.Last(i) + return inc.Last(i) } -func (inc *VMOM) Last(int) float64 { - if inc.Values.Length() == 0 { - return 0 - } - return inc.Values.Last(0) +func (inc *VMOM) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *VMOM) Length() int { - if inc.Values == nil { - return 0 - } return inc.Values.Length() } diff --git a/pkg/strategy/harmonic/shark.go b/pkg/strategy/harmonic/shark.go index d7bc287c38..a23d7e62e7 100644 --- a/pkg/strategy/harmonic/shark.go +++ b/pkg/strategy/harmonic/shark.go @@ -51,20 +51,12 @@ func (inc *SHARK) Update(high, low, price float64) { } -func (inc *SHARK) Last(int) float64 { - if len(inc.Values) == 0 { - return 0 - } - - return inc.Values[len(inc.Values)-1] +func (inc *SHARK) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *SHARK) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - - return inc.Values[len(inc.Values)-1-i] + return inc.Last(i) } func (inc *SHARK) Length() int { @@ -107,7 +99,7 @@ func (inc SHARK) SharkLong(highs, lows floats.Slice, p float64, lookback int) fl if lows.Index(b-1) > lows.Index(b) && lows.Index(b) < lows.Index(b+1) { B := lows.Index(b) if hB > B && B > lB { - //log.Infof("got point B:%f", B) + // log.Infof("got point B:%f", B) AB := math.Abs(A - B) hC := B + 1.618*AB lC := B + 1.13*AB @@ -115,24 +107,24 @@ func (inc SHARK) SharkLong(highs, lows floats.Slice, p float64, lookback int) fl if highs.Index(c-1) < highs.Index(c) && highs.Index(c) > highs.Index(c+1) { C := highs.Index(c) if hC > C && C > lC { - //log.Infof("got point C:%f", C) + // log.Infof("got point C:%f", C) XC := math.Abs(X - C) hD := C - 0.886*XC lD := C - 1.13*XC - //for d := 1; d < c; d++ { - //if lows.Index(d-1) > lows.Index(d) && lows.Index(d) < lows.Index(d+1) { - D := p //lows.Index(d) + // for d := 1; d < c; d++ { + // if lows.Index(d-1) > lows.Index(d) && lows.Index(d) < lows.Index(d+1) { + D := p // lows.Index(d) if hD > D && D > lD { BC := math.Abs(B - C) hD2 := C - 1.618*BC lD2 := C - 2.24*BC if hD2 > D && D > lD2 { - //log.Infof("got point D:%f", D) + // log.Infof("got point D:%f", D) score++ } } - //} - //} + // } + // } } } } @@ -161,7 +153,7 @@ func (inc SHARK) SharkShort(highs, lows floats.Slice, p float64, lookback int) f if highs.Index(b-1) > highs.Index(b) && highs.Index(b) < highs.Index(b+1) { B := highs.Index(b) if hB > B && B > lB { - //log.Infof("got point B:%f", B) + // log.Infof("got point B:%f", B) AB := math.Abs(A - B) lC := B - 1.618*AB hC := B - 1.13*AB @@ -169,24 +161,24 @@ func (inc SHARK) SharkShort(highs, lows floats.Slice, p float64, lookback int) f if lows.Index(c-1) < lows.Index(c) && lows.Index(c) > lows.Index(c+1) { C := lows.Index(c) if hC > C && C > lC { - //log.Infof("got point C:%f", C) + // log.Infof("got point C:%f", C) XC := math.Abs(X - C) lD := C + 0.886*XC hD := C + 1.13*XC - //for d := 1; d < c; d++ { - //if lows.Index(d-1) > lows.Index(d) && lows.Index(d) < lows.Index(d+1) { - D := p //lows.Index(d) + // for d := 1; d < c; d++ { + // if lows.Index(d-1) > lows.Index(d) && lows.Index(d) < lows.Index(d+1) { + D := p // lows.Index(d) if hD > D && D > lD { BC := math.Abs(B - C) lD2 := C + 1.618*BC hD2 := C + 2.24*BC if hD2 > D && D > lD2 { - //log.Infof("got point D:%f", D) + // log.Infof("got point D:%f", D) score++ } } - //} - //} + // } + // } } } } diff --git a/pkg/strategy/irr/neg_return_rate.go b/pkg/strategy/irr/neg_return_rate.go index 72fe420feb..f16adecbef 100644 --- a/pkg/strategy/irr/neg_return_rate.go +++ b/pkg/strategy/irr/neg_return_rate.go @@ -53,20 +53,12 @@ func (inc *NRR) Update(openPrice, closePrice float64) { inc.ReturnValues.Push(irr) } -func (inc *NRR) Last(int) float64 { - if len(inc.Values) == 0 { - return 0 - } - - return inc.Values[len(inc.Values)-1] +func (inc *NRR) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *NRR) Index(i int) float64 { - if i >= len(inc.Values) { - return 0 - } - - return inc.Values[len(inc.Values)-1-i] + return inc.Last(i) } func (inc *NRR) Length() int { diff --git a/pkg/strategy/supertrend/linreg.go b/pkg/strategy/supertrend/linreg.go index 65813aa7f9..cc790b0b0f 100644 --- a/pkg/strategy/supertrend/linreg.go +++ b/pkg/strategy/supertrend/linreg.go @@ -19,20 +19,13 @@ type LinReg struct { } // Last slope of linear regression baseline -func (lr *LinReg) Last(int) float64 { - if lr.Values.Length() == 0 { - return 0.0 - } - return lr.Values.Last(0) +func (lr *LinReg) Last(i int) float64 { + return lr.Values.Last(i) } // Index returns the slope of specified index func (lr *LinReg) Index(i int) float64 { - if i >= lr.Values.Length() { - return 0.0 - } - - return lr.Values.Index(i) + return lr.Last(i) } // Length of the slope values diff --git a/pkg/types/filter.go b/pkg/types/filter.go index bdb9e5de07..3c1a2f8587 100644 --- a/pkg/types/filter.go +++ b/pkg/types/filter.go @@ -12,7 +12,7 @@ func (f *FilterResult) Last(j int) float64 { return 0 } if len(f.c) > j { - return f.a.Index(f.c[j]) + return f.a.Last(f.c[j]) } l := f.a.Length() k := len(f.c) @@ -21,7 +21,7 @@ func (f *FilterResult) Last(j int) float64 { i = f.c[k-1] + 1 } for ; i < l; i++ { - tmp := f.a.Index(i) + tmp := f.a.Last(i) if f.b(i, tmp) { f.c = append(f.c, i) if j == k { diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index d8fe3870c6..4dd396b055 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -26,7 +26,7 @@ func (a *AbsResult) Last(i int) float64 { } func (a *AbsResult) Index(i int) float64 { - return math.Abs(a.a.Index(i)) + return a.Last(i) } func (a *AbsResult) Length() int { @@ -49,7 +49,7 @@ func LinearRegression(a Series, lookback int) (alpha float64, beta float64) { var weights []float64 for i := 0; i < lookback; i++ { x[i] = float64(i) - y[i] = a.Index(i) + y[i] = a.Last(i) } alpha, beta = stat.LinearRegression(x, y, weights, false) return @@ -83,8 +83,8 @@ func NextCross(a Series, b Series, lookback int) (int, float64, bool) { var weights []float64 for i := 0; i < lookback; i++ { x[i] = float64(i) - y1[i] = a.Index(i) - y2[i] = b.Index(i) + y1[i] = a.Last(i) + y2[i] = b.Last(i) } alpha1, beta1 := stat.LinearRegression(x, y1, weights, false) alpha2, beta2 := stat.LinearRegression(x, y2, weights, false) @@ -113,9 +113,9 @@ func (c *CrossResult) Last() bool { return false } if c.isOver { - return c.a.Last(0)-c.b.Last(0) > 0 && c.a.Index(1)-c.b.Index(1) < 0 + return c.a.Last(0)-c.b.Last(0) > 0 && c.a.Last(1)-c.b.Last(1) < 0 } else { - return c.a.Last(0)-c.b.Last(0) < 0 && c.a.Index(1)-c.b.Index(1) > 0 + return c.a.Last(0)-c.b.Last(0) < 0 && c.a.Last(1)-c.b.Last(1) > 0 } } @@ -124,9 +124,9 @@ func (c *CrossResult) Index(i int) bool { return false } if c.isOver { - return c.a.Index(i)-c.b.Index(i) > 0 && c.a.Index(i+1)-c.b.Index(i+1) < 0 + return c.a.Last(i)-c.b.Last(i) > 0 && c.a.Last(i+1)-c.b.Last(i+1) < 0 } else { - return c.a.Index(i)-c.b.Index(i) < 0 && c.a.Index(i+1)-c.b.Index(i+1) > 0 + return c.a.Last(i)-c.b.Last(i) < 0 && c.a.Last(i+1)-c.b.Last(i+1) > 0 } } @@ -161,7 +161,7 @@ func Highest(a Series, lookback int) float64 { } highest := a.Last(0) for i := 1; i < lookback; i++ { - current := a.Index(i) + current := a.Last(i) if highest < current { highest = current } @@ -175,7 +175,7 @@ func Lowest(a Series, lookback int) float64 { } lowest := a.Last(0) for i := 1; i < lookback; i++ { - current := a.Index(i) + current := a.Last(i) if lowest > current { lowest = current } @@ -247,7 +247,7 @@ func Sub(a interface{}, b interface{}) SeriesExtend { } func (a *MinusSeriesResult) Last(i int) float64 { - return a.a.Index(i) - a.b.Index(i) + return a.a.Last(i) - a.b.Last(i) } func (a *MinusSeriesResult) Index(i int) float64 { @@ -303,7 +303,7 @@ type DivSeriesResult struct { } func (a *DivSeriesResult) Last(i int) float64 { - return a.a.Index(i) / a.b.Index(i) + return a.a.Last(i) / a.b.Last(i) } func (a *DivSeriesResult) Index(i int) float64 { @@ -334,7 +334,7 @@ type MulSeriesResult struct { } func (a *MulSeriesResult) Last(i int) float64 { - return a.a.Index(i) * a.b.Index(i) + return a.a.Last(i) * a.b.Last(i) } func (a *MulSeriesResult) Index(i int) float64 { @@ -427,19 +427,19 @@ func Dot(a interface{}, b interface{}, limit ...int) float64 { } else if isaf && !isbf { sum := 0. for i := 0; i < l; i++ { - sum += aaf * bbs.Index(i) + sum += aaf * bbs.Last(i) } return sum } else if !isaf && isbf { sum := 0. for i := 0; i < l; i++ { - sum += aas.Index(i) * bbf + sum += aas.Last(i) * bbf } return sum } else { sum := 0. for i := 0; i < l; i++ { - sum += aas.Index(i) * bbs.Index(i) + sum += aas.Last(i) * bbs.Index(i) } return sum } @@ -458,7 +458,7 @@ func Array(a Series, limit ...int) (result []float64) { } result = make([]float64, l) for i := 0; i < l; i++ { - result[i] = a.Index(i) + result[i] = a.Last(i) } return } @@ -475,7 +475,7 @@ func Reverse(a Series, limit ...int) (result floats.Slice) { } result = make([]float64, l) for i := 0; i < l; i++ { - result[l-i-1] = a.Index(i) + result[l-i-1] = a.Last(i) } return } @@ -489,7 +489,7 @@ func (c *ChangeResult) Last(i int) float64 { if i+c.offset >= c.a.Length() { return 0 } - return c.a.Index(i) - c.a.Index(i+c.offset) + return c.a.Last(i) - c.a.Last(i+c.offset) } func (c *ChangeResult) Index(i int) float64 { @@ -524,7 +524,7 @@ func (c *PercentageChangeResult) Last(i int) float64 { if i+c.offset >= c.a.Length() { return 0 } - return c.a.Index(i)/c.a.Index(i+c.offset) - 1 + return c.a.Last(i)/c.a.Last(i+c.offset) - 1 } func (c *PercentageChangeResult) Index(i int) float64 { @@ -565,7 +565,7 @@ func Stdev(a Series, params ...int) float64 { avg := Mean(a, length) s := .0 for i := 0; i < length; i++ { - diff := a.Index(i) - avg + diff := a.Last(i) - avg s += diff * diff } if length-ddof == 0 { @@ -588,7 +588,7 @@ func Kendall(a, b Series, length int) float64 { concordant, discordant := 0, 0 for i := 0; i < length; i++ { for j := i + 1; j < length; j++ { - value := (aRanks.Index(i) - aRanks.Index(j)) * (bRanks.Index(i) - bRanks.Index(j)) + value := (aRanks.Last(i) - aRanks.Last(j)) * (bRanks.Last(i) - bRanks.Last(j)) if value > 0 { concordant++ } else { @@ -606,10 +606,10 @@ func Rank(a Series, length int) SeriesExtend { rank := make([]float64, length) mapper := make([]float64, length+1) for i := length - 1; i >= 0; i-- { - ii := a.Index(i) + ii := a.Last(i) counter := 0. for j := 0; j < length; j++ { - if a.Index(j) <= ii { + if a.Last(j) <= ii { counter += 1. } } @@ -633,8 +633,8 @@ func Pearson(a, b Series, length int) float64 { x := make([]float64, length) y := make([]float64, length) for i := 0; i < length; i++ { - x[i] = a.Index(i) - y[i] = b.Index(i) + x[i] = a.Last(i) + y[i] = b.Last(i) } return stat.Correlation(x, y, nil) } @@ -690,7 +690,7 @@ func Covariance(a Series, b Series, length int) float64 { meanb := Mean(b, length) sum := 0.0 for i := 0; i < length; i++ { - sum += (a.Index(i) - meana) * (b.Index(i) - meanb) + sum += (a.Last(i) - meana) * (b.Last(i) - meanb) } sum /= float64(length) return sum @@ -711,7 +711,7 @@ func Skew(a Series, length int) float64 { sum2 := 0.0 sum3 := 0.0 for i := 0; i < length; i++ { - diff := a.Index(i) - mean + diff := a.Last(i) - mean sum2 += diff * diff sum3 += diff * diff * diff } @@ -735,7 +735,7 @@ func (inc *ShiftResult) Last(i int) float64 { return 0 } - return inc.a.Index(inc.offset + i) + return inc.a.Last(inc.offset + i) } func (inc *ShiftResult) Index(i int) float64 { @@ -811,11 +811,11 @@ func Softmax(a Series, window int) SeriesExtend { s := 0.0 max := Highest(a, window) for i := 0; i < window; i++ { - s += math.Exp(a.Index(i) - max) + s += math.Exp(a.Last(i) - max) } out := NewQueue(window) for i := window - 1; i >= 0; i-- { - out.Update(math.Exp(a.Index(i)-max) / s) + out.Update(math.Exp(a.Last(i)-max) / s) } return out } @@ -825,7 +825,7 @@ func Softmax(a Series, window int) SeriesExtend { // - sum(v * ln(v)) func Entropy(a Series, window int) (e float64) { for i := 0; i < window; i++ { - v := a.Index(i) + v := a.Last(i) if v != 0 { e -= v * math.Log(v) } @@ -836,9 +836,9 @@ func Entropy(a Series, window int) (e float64) { // CrossEntropy computes the cross-entropy between the two distributions func CrossEntropy(a, b Series, window int) (e float64) { for i := 0; i < window; i++ { - v := a.Index(i) + v := a.Last(i) if v != 0 { - e -= v * math.Log(b.Index(i)) + e -= v * math.Log(b.Last(i)) } } return e @@ -893,7 +893,7 @@ func LogisticRegression(x []Series, y Series, lookback, iterations int, learning xx := make([][]float64, lookback) for i := 0; i < lookback; i++ { for j := 0; j < features; j++ { - xx[i] = append(xx[i], x[j].Index(lookback-i-1)) + xx[i] = append(xx[i], x[j].Last(lookback-i-1)) } } yy := Reverse(y, lookback) @@ -1002,7 +1002,7 @@ func (canvas *Canvas) Plot(tag string, a Series, endTime Time, length int, inter if a.Length() == 0 { return } - oldest := a.Index(a.Length() - 1) + oldest := a.Last(a.Length() - 1) interval := canvas.Interval if len(intervals) > 0 { interval = intervals[0] @@ -1026,7 +1026,7 @@ func (canvas *Canvas) PlotRaw(tag string, a Series, length int) { if a.Length() == 0 { return } - oldest := a.Index(a.Length() - 1) + oldest := a.Last(a.Length() - 1) canvas.Series = append(canvas.Series, chart.ContinuousSeries{ Name: tag, XValues: x, diff --git a/pkg/types/indicator_test.go b/pkg/types/indicator_test.go index 19c265ea81..da328c6661 100644 --- a/pkg/types/indicator_test.go +++ b/pkg/types/indicator_test.go @@ -24,7 +24,7 @@ func TestQueue(t *testing.T) { func TestFloat(t *testing.T) { var a Series = Sub(3., 2.) assert.Equal(t, a.Last(0), 1.) - assert.Equal(t, a.Index(100), 1.) + assert.Equal(t, a.Last(100), 1.) } func TestNextCross(t *testing.T) { @@ -67,8 +67,8 @@ func TestCorr(t *testing.T) { corr := Correlation(&a, &b, 4, Pearson) assert.InDelta(t, corr, -0.8510644, 0.001) out := Rank(&a, 4) - assert.Equal(t, out.Index(0), 2.5) - assert.Equal(t, out.Index(1), 4.0) + assert.Equal(t, out.Last(0), 2.5) + assert.Equal(t, out.Last(1), 4.0) corr = Correlation(&a, &b, 4, Spearman) assert.InDelta(t, corr, -0.94868, 0.001) } @@ -119,7 +119,7 @@ func TestSoftmax(t *testing.T) { out := Softmax(&a, a.Length()) r := floats.Slice{0.8360188027814407, 0.11314284146556013, 0.05083835575299916} for i := 0; i < out.Length(); i++ { - assert.InDelta(t, r.Index(i), out.Index(i), 0.001) + assert.InDelta(t, r.Last(i), out.Last(i), 0.001) } } @@ -128,7 +128,7 @@ func TestSigmoid(t *testing.T) { out := Sigmoid(&a) r := floats.Slice{0.9525741268224334, 0.7310585786300049, 0.8909031788043871} for i := 0; i < out.Length(); i++ { - assert.InDelta(t, r.Index(i), out.Index(i), 0.001, "i=%d", i) + assert.InDelta(t, r.Last(i), out.Last(i), 0.001, "i=%d", i) } } @@ -143,7 +143,6 @@ func TestAdd(t *testing.T) { var b NumberSeries = 2.0 out := Add(&a, &b) assert.Equal(t, out.Last(0), 5.0) - assert.Equal(t, out.Index(0), 5.0) assert.Equal(t, out.Length(), math.MaxInt32) } @@ -153,7 +152,7 @@ func TestDiv(t *testing.T) { out := Div(&a, &b) assert.Equal(t, 1.0, out.Last(0)) assert.Equal(t, 3, out.Length()) - assert.Equal(t, 0.5, out.Index(1)) + assert.Equal(t, 0.5, out.Last(1)) } func TestMul(t *testing.T) { @@ -162,7 +161,7 @@ func TestMul(t *testing.T) { out := Mul(&a, &b) assert.Equal(t, out.Last(0), 4.0) assert.Equal(t, out.Length(), 3) - assert.Equal(t, out.Index(1), 2.0) + assert.Equal(t, out.Last(1), 2.0) } func TestArray(t *testing.T) { diff --git a/pkg/types/kline.go b/pkg/types/kline.go index fcc358176b..8df06d297a 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -597,26 +597,11 @@ type KLineSeries struct { kv KValueType } -func (k *KLineSeries) Last(int) float64 { - length := len(*k.lines) - switch k.kv { - case kOpUnknown: - panic("kline series operator unknown") - case kOpenValue: - return (*k.lines)[length-1].GetOpen().Float64() - case kCloseValue: - return (*k.lines)[length-1].GetClose().Float64() - case kLowValue: - return (*k.lines)[length-1].GetLow().Float64() - case kHighValue: - return (*k.lines)[length-1].GetHigh().Float64() - case kVolumeValue: - return (*k.lines)[length-1].Volume.Float64() - } - return 0 +func (k *KLineSeries) Index(i int) float64 { + return k.Last(i) } -func (k *KLineSeries) Index(i int) float64 { +func (k *KLineSeries) Last(i int) float64 { length := len(*k.lines) if length == 0 || length-i-1 < 0 { return 0 diff --git a/pkg/types/omega.go b/pkg/types/omega.go index a89649e4f1..0556bb0906 100644 --- a/pkg/types/omega.go +++ b/pkg/types/omega.go @@ -17,7 +17,7 @@ func Omega(returns Series, returnThresholds ...float64) float64 { win := 0.0 loss := 0.0 for i := 0; i < length; i++ { - out := threshold - returns.Index(i) + out := threshold - returns.Last(i) if out > 0 { win += out } else { diff --git a/pkg/types/pca.go b/pkg/types/pca.go index 9ecc00a6c2..d11a3c15c3 100644 --- a/pkg/types/pca.go +++ b/pkg/types/pca.go @@ -2,6 +2,7 @@ package types import ( "fmt" + "gonum.org/v1/gonum/mat" ) @@ -21,7 +22,7 @@ func (pca *PCA) Fit(x []SeriesExtend, lookback int) error { for i, xx := range x { mean := xx.Mean(lookback) for j := 0; j < lookback; j++ { - vec[i+j*i] = xx.Index(j) - mean + vec[i+j*i] = xx.Last(j) - mean } } pca.svd = &mat.SVD{} @@ -40,7 +41,7 @@ func (pca *PCA) Transform(x []SeriesExtend, lookback int, features int) (result vec := make([]float64, lookback*len(x)) for i, xx := range x { for j := 0; j < lookback; j++ { - vec[i+j*i] = xx.Index(j) + vec[i+j*i] = xx.Last(j) } } newX := mat.NewDense(lookback, len(x), vec) diff --git a/pkg/types/queue.go b/pkg/types/queue.go index 5205f802f8..ce68877ec0 100644 --- a/pkg/types/queue.go +++ b/pkg/types/queue.go @@ -21,6 +21,7 @@ func (inc *Queue) Last(i int) float64 { if i < 0 || len(inc.arr)-i-1 < 0 { return 0 } + return inc.arr[len(inc.arr)-1-i] } diff --git a/pkg/types/series.go b/pkg/types/series.go index 1c4289ee28..f97472b220 100644 --- a/pkg/types/series.go +++ b/pkg/types/series.go @@ -103,7 +103,7 @@ func Sum(a Series, limit ...int) (sum float64) { l = limit[0] } for i := 0; i < l; i++ { - sum += a.Index(i) + sum += a.Last(i) } return sum } diff --git a/pkg/types/seriesbase_imp.go b/pkg/types/seriesbase_imp.go index 1e52ce5449..fee51ddc49 100644 --- a/pkg/types/seriesbase_imp.go +++ b/pkg/types/seriesbase_imp.go @@ -12,17 +12,14 @@ type SeriesBase struct { } func (s *SeriesBase) Index(i int) float64 { - if s.Series == nil { - return 0 - } - return s.Series.Last(i) + return s.Last(i) } -func (s *SeriesBase) Last(int) float64 { +func (s *SeriesBase) Last(i int) float64 { if s.Series == nil { return 0 } - return s.Series.Last(0) + return s.Series.Last(i) } func (s *SeriesBase) Length() int { diff --git a/pkg/types/sortino.go b/pkg/types/sortino.go index 32acdb3f47..362b94d606 100644 --- a/pkg/types/sortino.go +++ b/pkg/types/sortino.go @@ -6,9 +6,11 @@ import ( // Sortino: Calcluates the sotino ratio of access returns // -// ROI_excess E[ROI] - ROI_risk_free +// ROI_excess E[ROI] - ROI_risk_free +// // sortino = ---------- = ----------------------- -// risk sqrt(E[ROI_drawdown^2]) +// +// risk sqrt(E[ROI_drawdown^2]) // // @param returns (Series): Series of profit/loss percentage every specific interval // @param riskFreeReturns (float): risk-free return rate of year @@ -29,7 +31,7 @@ func Sortino(returns Series, riskFreeReturns float64, periods int, annualize boo } var sum = 0. for i := 0; i < num; i++ { - exRet := returns.Index(i) - avgRiskFreeReturns + exRet := returns.Last(i) - avgRiskFreeReturns if exRet < 0 { sum += exRet * exRet } From 9e6cb0858ef01380a6c401260501cd6d769a5acf Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 07:58:58 +0800 Subject: [PATCH 0923/1392] indicator: simplify source, calculate binding --- pkg/indicator/float64updater.go | 34 ++++++++++++++++++++++++++++++++- pkg/indicator/types.go | 5 +++++ pkg/indicator/v2_ewma.go | 16 ++-------------- pkg/indicator/v2_rma.go | 18 +++-------------- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/pkg/indicator/float64updater.go b/pkg/indicator/float64updater.go index 6f4e69ddb9..8d2d9e3b46 100644 --- a/pkg/indicator/float64updater.go +++ b/pkg/indicator/float64updater.go @@ -28,9 +28,41 @@ func (f *Float64Series) Last(i int) float64 { } func (f *Float64Series) Index(i int) float64 { - return f.slice.Last(i) + return f.Last(i) } func (f *Float64Series) Length() int { return len(f.slice) } + +func (f *Float64Series) PushAndEmit(x float64) { + f.slice.Push(x) + f.EmitUpdate(x) +} + +// Bind binds the source event to the target (Float64Calculator) +// A Float64Calculator should be able to calculate the float64 result from a single float64 argument input +func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) { + var c func(x float64) + + // optimize the truncation check + trc, canTruncate := target.(Float64Truncator) + if canTruncate { + c = func(x float64) { + y := target.Calculate(x) + target.PushAndEmit(y) + trc.Truncate() + } + } else { + c = func(x float64) { + y := target.Calculate(x) + target.PushAndEmit(y) + } + } + + if sub, ok := source.(Float64Subscription); ok { + sub.AddSubscriber(c) + } else { + source.OnUpdate(c) + } +} diff --git a/pkg/indicator/types.go b/pkg/indicator/types.go index 78dcaabd3e..7e750b9da2 100644 --- a/pkg/indicator/types.go +++ b/pkg/indicator/types.go @@ -4,6 +4,7 @@ import "github.com/c9s/bbgo/pkg/types" type Float64Calculator interface { Calculate(x float64) float64 + PushAndEmit(x float64) } type Float64Source interface { @@ -15,3 +16,7 @@ type Float64Subscription interface { types.Series AddSubscriber(f func(v float64)) } + +type Float64Truncator interface { + Truncate() +} diff --git a/pkg/indicator/v2_ewma.go b/pkg/indicator/v2_ewma.go index afd0752ad7..1be654f85f 100644 --- a/pkg/indicator/v2_ewma.go +++ b/pkg/indicator/v2_ewma.go @@ -13,23 +13,11 @@ func EWMA2(source Float64Source, window int) *EWMAStream { window: window, multiplier: 2.0 / float64(1+window), } - - if sub, ok := source.(Float64Subscription); ok { - sub.AddSubscriber(s.calculateAndPush) - } else { - source.OnUpdate(s.calculateAndPush) - } - + s.Bind(source, s) return s } -func (s *EWMAStream) calculateAndPush(v float64) { - v2 := s.calculate(v) - s.slice.Push(v2) - s.EmitUpdate(v2) -} - -func (s *EWMAStream) calculate(v float64) float64 { +func (s *EWMAStream) Calculate(v float64) float64 { last := s.slice.Last(0) m := s.multiplier return (1.0-m)*last + m*v diff --git a/pkg/indicator/v2_rma.go b/pkg/indicator/v2_rma.go index f268aec527..17650d9d93 100644 --- a/pkg/indicator/v2_rma.go +++ b/pkg/indicator/v2_rma.go @@ -19,23 +19,11 @@ func RMA2(source Float64Source, window int, adjust bool) *RMAStream { Adjust: adjust, } - if sub, ok := source.(Float64Subscription); ok { - sub.AddSubscriber(s.calculateAndPush) - } else { - source.OnUpdate(s.calculateAndPush) - } - + s.Bind(source, s) return s } -func (s *RMAStream) calculateAndPush(v float64) { - v2 := s.calculate(v) - s.slice.Push(v2) - s.EmitUpdate(v2) - s.truncate() -} - -func (s *RMAStream) calculate(x float64) float64 { +func (s *RMAStream) Calculate(x float64) float64 { lambda := 1 / float64(s.window) tmp := 0.0 if s.counter == 0 { @@ -62,7 +50,7 @@ func (s *RMAStream) calculate(x float64) float64 { return tmp } -func (s *RMAStream) truncate() { +func (s *RMAStream) Truncate() { if len(s.slice) > MaxNumOfRMA { s.slice = s.slice[MaxNumOfRMATruncateSize-1:] } From 47e869a9f7e40fac9cf2e0c90f3b0c1ffb8a2506 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 08:11:19 +0800 Subject: [PATCH 0924/1392] floats: add Truncate method support to floats slice --- pkg/datatype/floats/slice.go | 14 +++++++++----- pkg/datatype/floats/slice_test.go | 8 ++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index fe9314666b..8fa850d134 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -191,14 +191,18 @@ func (s Slice) Last(i int) float64 { return s[length-1-i] } +func (s Slice) Truncate(size int) Slice { + if len(s) < size { + return s + } + + return s[len(s)-size:] +} + // Index fetches the element from the end of the slice // WARNING: it does not start from 0!!! func (s Slice) Index(i int) float64 { - length := len(s) - if i < 0 || length-1-i < 0 { - return 0.0 - } - return s[length-1-i] + return s.Last(i) } func (s Slice) Length() int { diff --git a/pkg/datatype/floats/slice_test.go b/pkg/datatype/floats/slice_test.go index d5fdc000c6..d72caebdbd 100644 --- a/pkg/datatype/floats/slice_test.go +++ b/pkg/datatype/floats/slice_test.go @@ -15,6 +15,14 @@ func TestSub(t *testing.T) { assert.Equal(t, 5, c.Length()) } +func TestTruncate(t *testing.T) { + a := New(1, 2, 3, 4, 5) + for i := 5; i > 0; i-- { + a = a.Truncate(i) + assert.Equal(t, i, a.Length()) + } +} + func TestAdd(t *testing.T) { a := New(1, 2, 3, 4, 5) b := New(1, 2, 3, 4, 5) From ee8bbe3418412c52df3254d39086c37c73787e0d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 08:11:30 +0800 Subject: [PATCH 0925/1392] indicator: add v2 sma --- pkg/indicator/sma.go | 19 ------------------- pkg/indicator/v2_rsi.go | 16 ++-------------- pkg/indicator/v2_sma.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 33 deletions(-) create mode 100644 pkg/indicator/v2_sma.go diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index cc3bb1bc70..53c9f468c6 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -1,7 +1,6 @@ package indicator import ( - "fmt" "time" "github.com/c9s/bbgo/pkg/datatype/floats" @@ -82,21 +81,3 @@ func (inc *SMA) LoadK(allKLines []types.KLine) { inc.PushK(k) } } - -func calculateSMA(kLines []types.KLine, window int, priceF KLineValueMapper) (float64, error) { - length := len(kLines) - if length == 0 || length < window { - return 0.0, fmt.Errorf("insufficient elements for calculating SMA with window = %d", window) - } - if length != window { - return 0.0, fmt.Errorf("too much klines passed in, requires only %d klines", window) - } - - sum := 0.0 - for _, k := range kLines { - sum += priceF(k) - } - - avg := sum / float64(window) - return avg, nil -} diff --git a/pkg/indicator/v2_rsi.go b/pkg/indicator/v2_rsi.go index 583e68bafe..81e439320b 100644 --- a/pkg/indicator/v2_rsi.go +++ b/pkg/indicator/v2_rsi.go @@ -17,17 +17,11 @@ func RSI2(source Float64Source, window int) *RSIStream { Float64Series: NewFloat64Series(), window: window, } - - if sub, ok := source.(Float64Subscription); ok { - sub.AddSubscriber(s.calculateAndPush) - } else { - source.OnUpdate(s.calculateAndPush) - } - + s.Bind(source, s) return s } -func (s *RSIStream) calculate(_ float64) float64 { +func (s *RSIStream) Calculate(_ float64) float64 { var gainSum, lossSum float64 var sourceLen = s.source.Length() var limit = min(s.window, sourceLen) @@ -48,9 +42,3 @@ func (s *RSIStream) calculate(_ float64) float64 { rsi := 100.0 - (100.0 / (1.0 + rs)) return rsi } - -func (s *RSIStream) calculateAndPush(x float64) { - rsi := s.calculate(x) - s.slice.Push(rsi) - s.EmitUpdate(rsi) -} diff --git a/pkg/indicator/v2_sma.go b/pkg/indicator/v2_sma.go new file mode 100644 index 0000000000..7d6ec97f91 --- /dev/null +++ b/pkg/indicator/v2_sma.go @@ -0,0 +1,29 @@ +package indicator + +import "github.com/c9s/bbgo/pkg/types" + +type SMAStream struct { + Float64Series + window int + rawValues *types.Queue +} + +func SMA2(source Float64Source, window int) *SMAStream { + s := &SMAStream{ + Float64Series: NewFloat64Series(), + window: window, + rawValues: types.NewQueue(window), + } + s.Bind(source, s) + return s +} + +func (s *SMAStream) Calculate(v float64) float64 { + s.rawValues.Update(v) + sma := s.rawValues.Mean(s.window) + return sma +} + +func (s *SMAStream) Truncate() { + s.slice.Truncate(MaxNumOfSMA) +} From 01ef6c2628306bcbce15aff6c5ff2f7e534a0945 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 08:28:49 +0800 Subject: [PATCH 0926/1392] indicator: add v2 MACD --- pkg/indicator/macd.go | 10 ++---- pkg/indicator/v2_macd.go | 29 ++++++++++++++++++ pkg/indicator/v2_macd_test.go | 57 +++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 pkg/indicator/v2_macd.go create mode 100644 pkg/indicator/v2_macd_test.go diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index 2535e96814..6947eb7313 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -75,12 +75,8 @@ func (inc *MACDLegacy) Update(x float64) { inc.EmitUpdate(macd, signal, histogram) } -func (inc *MACDLegacy) Last(int) float64 { - if len(inc.Values) == 0 { - return 0.0 - } - - return inc.Values[len(inc.Values)-1] +func (inc *MACDLegacy) Last(i int) float64 { + return inc.Values.Last(i) } func (inc *MACDLegacy) Length() int { @@ -111,7 +107,7 @@ func (inc *MACDValues) Last(i int) float64 { } func (inc *MACDValues) Index(i int) float64 { - return inc.Values.Last(i) + return inc.Last(i) } func (inc *MACDValues) Length() int { diff --git a/pkg/indicator/v2_macd.go b/pkg/indicator/v2_macd.go new file mode 100644 index 0000000000..2e52534ac0 --- /dev/null +++ b/pkg/indicator/v2_macd.go @@ -0,0 +1,29 @@ +package indicator + +type MACDStream struct { + *SubtractStream + + shortWindow, longWindow, signalWindow int + + fastEWMA, slowEWMA, signal *EWMAStream + histogram *SubtractStream +} + +func MACD2(source Float64Source, shortWindow, longWindow, signalWindow int) *MACDStream { + // bind and calculate these first + fastEWMA := EWMA2(source, shortWindow) + slowEWMA := EWMA2(source, longWindow) + macd := Subtract(fastEWMA, slowEWMA) + signal := EWMA2(macd, signalWindow) + histogram := Subtract(macd, signal) + return &MACDStream{ + SubtractStream: macd, + shortWindow: shortWindow, + longWindow: longWindow, + signalWindow: signalWindow, + fastEWMA: fastEWMA, + slowEWMA: slowEWMA, + signal: signal, + histogram: histogram, + } +} diff --git a/pkg/indicator/v2_macd_test.go b/pkg/indicator/v2_macd_test.go new file mode 100644 index 0000000000..74617089a7 --- /dev/null +++ b/pkg/indicator/v2_macd_test.go @@ -0,0 +1,57 @@ +package indicator + +import ( + "encoding/json" + "math" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +/* +python: + +import pandas as pd +s = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]) +slow = s.ewm(span=26, adjust=False).mean() +fast = s.ewm(span=12, adjust=False).mean() +print(fast - slow) +*/ + +func Test_MACD2(t *testing.T) { + var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []fixedpoint.Value + err := json.Unmarshal(randomPrices, &input) + assert.NoError(t, err) + + tests := []struct { + name string + kLines []types.KLine + want float64 + }{ + { + name: "random_case", + kLines: buildKLines(input), + want: 0.7967670223776384, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prices := &PriceStream{} + macd := MACD2(prices, 12, 26, 9) + for _, k := range tt.kLines { + prices.EmitUpdate(k.Close.Float64()) + } + + got := macd.Last(0) + diff := math.Trunc((got-tt.want)*100) / 100 + if diff != 0 { + t.Errorf("MACD2() = %v, want %v", got, tt.want) + } + }) + } +} From 0da0b1086af6ad0f94a3ec4f3796ef23eac75782 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 08:33:14 +0800 Subject: [PATCH 0927/1392] indicator: fix SMA truncate call --- pkg/indicator/v2_sma.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/indicator/v2_sma.go b/pkg/indicator/v2_sma.go index 7d6ec97f91..641cdb3d29 100644 --- a/pkg/indicator/v2_sma.go +++ b/pkg/indicator/v2_sma.go @@ -25,5 +25,5 @@ func (s *SMAStream) Calculate(v float64) float64 { } func (s *SMAStream) Truncate() { - s.slice.Truncate(MaxNumOfSMA) + s.slice = s.slice.Truncate(MaxNumOfSMA) } From e320e5d2493aa234762f145204f6c1d07e39688f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 08:56:17 +0800 Subject: [PATCH 0928/1392] indicator: remove unused low value indicator --- pkg/indicator/low.go | 37 ---------------------------------- pkg/indicator/low_callbacks.go | 15 -------------- 2 files changed, 52 deletions(-) delete mode 100644 pkg/indicator/low.go delete mode 100644 pkg/indicator/low_callbacks.go diff --git a/pkg/indicator/low.go b/pkg/indicator/low.go deleted file mode 100644 index 6f6d9468b4..0000000000 --- a/pkg/indicator/low.go +++ /dev/null @@ -1,37 +0,0 @@ -package indicator - -import ( - "time" - - "github.com/c9s/bbgo/pkg/datatype/floats" - "github.com/c9s/bbgo/pkg/types" -) - -//go:generate callbackgen -type Low -type Low struct { - types.IntervalWindow - types.SeriesBase - - Values floats.Slice - EndTime time.Time - - updateCallbacks []func(value float64) -} - -func (inc *Low) Update(value float64) { - if len(inc.Values) == 0 { - inc.SeriesBase.Series = inc - } - - inc.Values.Push(value) -} - -func (inc *Low) PushK(k types.KLine) { - if k.EndTime.Before(inc.EndTime) { - return - } - - inc.Update(k.Low.Float64()) - inc.EndTime = k.EndTime.Time() - inc.EmitUpdate(inc.Last(0)) -} diff --git a/pkg/indicator/low_callbacks.go b/pkg/indicator/low_callbacks.go deleted file mode 100644 index bd261b79ae..0000000000 --- a/pkg/indicator/low_callbacks.go +++ /dev/null @@ -1,15 +0,0 @@ -// Code generated by "callbackgen -type Low"; DO NOT EDIT. - -package indicator - -import () - -func (inc *Low) OnUpdate(cb func(value float64)) { - inc.updateCallbacks = append(inc.updateCallbacks, cb) -} - -func (inc *Low) EmitUpdate(value float64) { - for _, cb := range inc.updateCallbacks { - cb(value) - } -} From 9c43c75361509d31808e49320a6c866549aa69e8 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 11:43:22 +0800 Subject: [PATCH 0929/1392] floats: fix floats.Slice truncate --- pkg/datatype/floats/slice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index 8fa850d134..1d610a4f53 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -192,7 +192,7 @@ func (s Slice) Last(i int) float64 { } func (s Slice) Truncate(size int) Slice { - if len(s) < size { + if size < 0 || len(s) <= size { return s } From 23a49a8fd2fa002bee08fbf35e9e1f8ef2259e80 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 11:43:37 +0800 Subject: [PATCH 0930/1392] indicator: fix klines stream emitter --- pkg/indicator/klinestream.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/indicator/klinestream.go b/pkg/indicator/klinestream.go index 13c43737e9..1838584bf1 100644 --- a/pkg/indicator/klinestream.go +++ b/pkg/indicator/klinestream.go @@ -11,29 +11,43 @@ type KLineStream struct { kLines []types.KLine } +func (s *KLineStream) Length() int { + return len(s.kLines) +} + +func (s *KLineStream) Last(i int) *types.KLine { + l := len(s.kLines) + if i < 0 || l-1-i < 0 { + return nil + } + + return &s.kLines[l-1-i] +} + // AddSubscriber adds the subscriber function and push historical data to the subscriber func (s *KLineStream) AddSubscriber(f func(k types.KLine)) { + s.OnUpdate(f) + if len(s.kLines) > 0 { // push historical klines to the subscriber for _, k := range s.kLines { f(k) } } - s.OnUpdate(f) } // KLines creates a KLine stream that pushes the klines to the subscribers -func KLines(source types.Stream) *KLineStream { +func KLines(source types.Stream, symbol string, interval types.Interval) *KLineStream { s := &KLineStream{} - source.OnKLineClosed(func(k types.KLine) { + source.OnKLineClosed(types.KLineWith(symbol, interval, func(k types.KLine) { s.kLines = append(s.kLines, k) + s.EmitUpdate(k) if len(s.kLines) > MaxNumOfKLines { s.kLines = s.kLines[len(s.kLines)-1-MaxNumOfKLines:] } - s.EmitUpdate(k) - }) + })) return s } From f349f3620c51c17d47c78c02109c2b49c64af882 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 12:13:22 +0800 Subject: [PATCH 0931/1392] autoborrow: add SlackAttachment support to the binance balance update event --- pkg/exchange/binance/parse.go | 200 +++++++++++++++------------- pkg/strategy/autoborrow/strategy.go | 2 +- 2 files changed, 112 insertions(+), 90 deletions(-) diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index eb3bbd6eca..499fca9481 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -7,6 +7,7 @@ import ( "time" "github.com/adshao/go-binance/v2/futures" + "github.com/slack-go/slack" "github.com/adshao/go-binance/v2" "github.com/valyala/fastjson" @@ -21,43 +22,42 @@ type EventBase struct { } /* - executionReport -{ - "e": "executionReport", // Event type - "E": 1499405658658, // Event time - "s": "ETHBTC", // Symbol - "c": "mUvoqJxFIILMdfAW5iGSOW", // Client order ID - "S": "BUY", // Side - "o": "LIMIT", // Order type - "f": "GTC", // Time in force - "q": "1.00000000", // Order quantity - "p": "0.10264410", // Order price - "P": "0.00000000", // Stop price - "F": "0.00000000", // Iceberg quantity - "g": -1, // OrderListId - "C": null, // Original client order ID; This is the ID of the order being canceled - "x": "NEW", // Current execution type - "X": "NEW", // Current order status - "r": "NONE", // Order reject reason; will be an error code. - "i": 4293153, // Order ID - "l": "0.00000000", // Last executed quantity - "z": "0.00000000", // Cumulative filled quantity - "L": "0.00000000", // Last executed price - "n": "0", // Commission amount - "N": null, // Commission asset - "T": 1499405658657, // Transaction time - "t": -1, // Trade ID - "I": 8641984, // Ignore - "w": true, // Is the order on the book? - "m": false, // Is this trade the maker side? - "M": false, // Ignore - "O": 1499405658657, // Order creation time - "Z": "0.00000000", // Cumulative quote asset transacted quantity - "Y": "0.00000000", // Last quote asset transacted quantity (i.e. lastPrice * lastQty) - "Q": "0.00000000" // Quote Order Quantity -} + { + "e": "executionReport", // Event type + "E": 1499405658658, // Event time + "s": "ETHBTC", // Symbol + "c": "mUvoqJxFIILMdfAW5iGSOW", // Client order ID + "S": "BUY", // Side + "o": "LIMIT", // Order type + "f": "GTC", // Time in force + "q": "1.00000000", // Order quantity + "p": "0.10264410", // Order price + "P": "0.00000000", // Stop price + "F": "0.00000000", // Iceberg quantity + "g": -1, // OrderListId + "C": null, // Original client order ID; This is the ID of the order being canceled + "x": "NEW", // Current execution type + "X": "NEW", // Current order status + "r": "NONE", // Order reject reason; will be an error code. + "i": 4293153, // Order ID + "l": "0.00000000", // Last executed quantity + "z": "0.00000000", // Cumulative filled quantity + "L": "0.00000000", // Last executed price + "n": "0", // Commission amount + "N": null, // Commission asset + "T": 1499405658657, // Transaction time + "t": -1, // Trade ID + "I": 8641984, // Ignore + "w": true, // Is the order on the book? + "m": false, // Is this trade the maker side? + "M": false, // Ignore + "O": 1499405658657, // Order creation time + "Z": "0.00000000", // Cumulative quote asset transacted quantity + "Y": "0.00000000", // Last quote asset transacted quantity (i.e. lastPrice * lastQty) + "Q": "0.00000000" // Quote Order Quantity + } */ type ExecutionReportEvent struct { EventBase @@ -167,69 +167,91 @@ func (e *ExecutionReportEvent) Trade() (*types.Trade, error) { /* balanceUpdate -{ - "e": "balanceUpdate", //KLineEvent Type - "E": 1573200697110, //KLineEvent Time - "a": "BTC", //Asset - "d": "100.00000000", //Balance Delta - "T": 1573200697068 //Clear Time -} + { + "e": "balanceUpdate", //KLineEvent Type + "E": 1573200697110, //KLineEvent Time + "a": "BTC", //Asset + "d": "100.00000000", //Balance Delta + "T": 1573200697068 //Clear Time + } */ type BalanceUpdateEvent struct { EventBase - Asset string `json:"a"` - Delta string `json:"d"` - ClearTime int64 `json:"T"` + Asset string `json:"a"` + Delta fixedpoint.Value `json:"d"` + ClearTime types.MillisecondTimestamp `json:"T"` } -/* +func (e *BalanceUpdateEvent) SlackAttachment() slack.Attachment { + return slack.Attachment{ + Title: "Binance Balance Update Event", + Color: "warning", + Fields: []slack.AttachmentField{ + { + Title: "Asset", + Value: e.Asset, + Short: true, + }, + { + Title: "Delta", + Value: e.Delta.String(), + Short: true, + }, + { + Title: "Time", + Value: e.ClearTime.String(), + Short: true, + }, + }, + } +} +/* outboundAccountInfo -{ - "e": "outboundAccountInfo", // KLineEvent type - "E": 1499405658849, // KLineEvent time - "m": 0, // Maker commission rate (bips) - "t": 0, // Taker commission rate (bips) - "b": 0, // Buyer commission rate (bips) - "s": 0, // Seller commission rate (bips) - "T": true, // Can trade? - "W": true, // Can withdraw? - "D": true, // Can deposit? - "u": 1499405658848, // Time of last account update - "B": [ // AccountBalances array - { - "a": "LTC", // Asset - "f": "17366.18538083", // Free amount - "l": "0.00000000" // Locked amount - }, - { - "a": "BTC", - "f": "10537.85314051", - "l": "2.19464093" - }, - { - "a": "ETH", - "f": "17902.35190619", - "l": "0.00000000" - }, - { - "a": "BNC", - "f": "1114503.29769312", - "l": "0.00000000" - }, - { - "a": "NEO", - "f": "0.00000000", - "l": "0.00000000" - } - ], - "P": [ // Account Permissions - "SPOT" - ] -} - + { + "e": "outboundAccountInfo", // KLineEvent type + "E": 1499405658849, // KLineEvent time + "m": 0, // Maker commission rate (bips) + "t": 0, // Taker commission rate (bips) + "b": 0, // Buyer commission rate (bips) + "s": 0, // Seller commission rate (bips) + "T": true, // Can trade? + "W": true, // Can withdraw? + "D": true, // Can deposit? + "u": 1499405658848, // Time of last account update + "B": [ // AccountBalances array + { + "a": "LTC", // Asset + "f": "17366.18538083", // Free amount + "l": "0.00000000" // Locked amount + }, + { + "a": "BTC", + "f": "10537.85314051", + "l": "2.19464093" + }, + { + "a": "ETH", + "f": "17902.35190619", + "l": "0.00000000" + }, + { + "a": "BNC", + "f": "1114503.29769312", + "l": "0.00000000" + }, + { + "a": "NEO", + "f": "0.00000000", + "l": "0.00000000" + } + ], + "P": [ // Account Permissions + "SPOT" + ] + } */ type Balance struct { Asset string `json:"a"` diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 31e625001f..c1885948af 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -359,7 +359,7 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE return } - delta := fixedpoint.MustNewFromString(event.Delta) + delta := event.Delta // ignore outflow if delta.Sign() < 0 { From 1dfb0cd1a146728b421c7ca587835970c450320e Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 12:13:51 +0800 Subject: [PATCH 0932/1392] autoborrow: notify balance delta event --- pkg/strategy/autoborrow/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index c1885948af..27980ff790 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -350,6 +350,8 @@ func (s *Strategy) handleBalanceUpdate(balances types.BalanceMap) { } func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateEvent) { + bbgo.Notify(event) + if s.MinMarginLevel.IsZero() { return } From fa0cb1e85f6859708c915702c6cfa3fbaf59355e Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 12:17:11 +0800 Subject: [PATCH 0933/1392] binance: document balanceUpdate event --- pkg/exchange/binance/parse.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index 499fca9481..b2990d9778 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -165,7 +165,12 @@ func (e *ExecutionReportEvent) Trade() (*types.Trade, error) { } /* -balanceUpdate +event: balanceUpdate + +Balance Update occurs during the following: + +Deposits or withdrawals from the account +Transfer of funds between accounts (e.g. Spot to Margin) { "e": "balanceUpdate", //KLineEvent Type @@ -174,6 +179,8 @@ balanceUpdate "d": "100.00000000", //Balance Delta "T": 1573200697068 //Clear Time } + +This event is only for Spot */ type BalanceUpdateEvent struct { EventBase From 95e1f10934be09247db7ad8085c7d72f9a0a1fb8 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 12:18:53 +0800 Subject: [PATCH 0934/1392] autoborrow: send notify when auto repay is skip --- pkg/strategy/autoborrow/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 27980ff790..0c50f646ae 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -358,6 +358,7 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE account := s.ExchangeSession.GetAccount() if account.MarginLevel.Compare(s.MinMarginLevel) > 0 { + bbgo.Notify("account margin level %f is greater than minimal margin level %f, skip", account.MarginLevel.Float64(), s.MinMarginLevel.Float64()) return } From b55fbd5c9689a2998242a68fdc0e30eaa4244dc8 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 12:27:39 +0800 Subject: [PATCH 0935/1392] autoborrow: check maxBorrowable --- pkg/strategy/autoborrow/strategy.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 0c50f646ae..ba4d0a3b91 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -247,9 +247,9 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { if !marginAsset.MaxTotalBorrow.IsZero() { // check if we over borrow - newBorrow := toBorrow.Add(b.Borrowed) - if newBorrow.Compare(marginAsset.MaxTotalBorrow) > 0 { - toBorrow = toBorrow.Sub(newBorrow.Sub(marginAsset.MaxTotalBorrow)) + newTotalBorrow := toBorrow.Add(b.Borrowed) + if newTotalBorrow.Compare(marginAsset.MaxTotalBorrow) > 0 { + toBorrow = toBorrow.Sub(newTotalBorrow.Sub(marginAsset.MaxTotalBorrow)) if toBorrow.Sign() < 0 { log.Warnf("margin asset %s is over borrowed, skip", marginAsset.Asset) continue @@ -257,6 +257,22 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { } } + maxBorrowable, err2 := s.marginBorrowRepay.QueryMarginAssetMaxBorrowable(ctx, marginAsset.Asset) + if err2 != nil { + log.WithError(err).Errorf("max borrowable query error") + continue + } + + if toBorrow.Compare(maxBorrowable) > 0 { + bbgo.Notify("Trying to borrow %f %s, which is greater than the max borrowable amount %f, will adjust borrow amount to %f", + toBorrow.Float64(), + marginAsset.Asset, + maxBorrowable.Float64(), + maxBorrowable.Float64()) + + toBorrow = fixedpoint.Min(maxBorrowable, toBorrow) + } + if toBorrow.IsZero() { continue } From 3e9458499c20cb4491791097b81253e279bc8cc1 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 12:39:30 +0800 Subject: [PATCH 0936/1392] indicator: fix tests --- pkg/indicator/v2_atr_test.go | 2 +- pkg/indicator/v2_test.go | 2 +- pkg/indicator/v2_tr_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/indicator/v2_atr_test.go b/pkg/indicator/v2_atr_test.go index bacbc78612..23b3cc9702 100644 --- a/pkg/indicator/v2_atr_test.go +++ b/pkg/indicator/v2_atr_test.go @@ -64,7 +64,7 @@ func Test_ATR2(t *testing.T) { t.Run(tt.name, func(t *testing.T) { stream := &types.StandardStream{} - kLines := KLines(stream) + kLines := KLines(stream, "", "") atr := ATR2(kLines, tt.window) for _, k := range tt.kLines { diff --git a/pkg/indicator/v2_test.go b/pkg/indicator/v2_test.go index a96812c1fc..717713d11b 100644 --- a/pkg/indicator/v2_test.go +++ b/pkg/indicator/v2_test.go @@ -11,7 +11,7 @@ import ( func Test_v2_Subtract(t *testing.T) { stream := &types.StandardStream{} - kLines := KLines(stream) + kLines := KLines(stream, "", "") closePrices := ClosePrices(kLines) fastEMA := EWMA2(closePrices, 10) slowEMA := EWMA2(closePrices, 25) diff --git a/pkg/indicator/v2_tr_test.go b/pkg/indicator/v2_tr_test.go index c6b46332ab..1067529c9a 100644 --- a/pkg/indicator/v2_tr_test.go +++ b/pkg/indicator/v2_tr_test.go @@ -64,7 +64,7 @@ func Test_TR_and_RMA(t *testing.T) { t.Run(tt.name, func(t *testing.T) { stream := &types.StandardStream{} - kLines := KLines(stream) + kLines := KLines(stream, "", "") atr := TR2(kLines) rma := RMA2(atr, tt.window, true) From 8ebf5723a7414314277e96c17ce702db412e75da Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 14:27:03 +0800 Subject: [PATCH 0937/1392] indicator: add v2 CMA indicator --- pkg/indicator/ad.go | 12 ------------ pkg/indicator/alma.go | 11 ----------- pkg/indicator/ca_callbacks.go | 4 ++-- pkg/indicator/cci.go | 12 ------------ pkg/indicator/cma.go | 15 ++------------- pkg/indicator/dmi.go | 12 ------------ pkg/indicator/v2_cma.go | 23 +++++++++++++++++++++++ 7 files changed, 27 insertions(+), 62 deletions(-) create mode 100644 pkg/indicator/v2_cma.go diff --git a/pkg/indicator/ad.go b/pkg/indicator/ad.go index 31536c8f5b..02d91acee9 100644 --- a/pkg/indicator/ad.go +++ b/pkg/indicator/ad.go @@ -64,15 +64,3 @@ func (inc *AD) CalculateAndUpdate(kLines []types.KLine) { inc.EmitUpdate(inc.Last(0)) inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } - -func (inc *AD) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - - inc.CalculateAndUpdate(window) -} - -func (inc *AD) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) -} diff --git a/pkg/indicator/alma.go b/pkg/indicator/alma.go index 7f3c806b62..85e33056bf 100644 --- a/pkg/indicator/alma.go +++ b/pkg/indicator/alma.go @@ -90,14 +90,3 @@ func (inc *ALMA) CalculateAndUpdate(allKLines []types.KLine) { inc.Update(allKLines[len(allKLines)-1].Close.Float64()) inc.EmitUpdate(inc.Last(0)) } - -func (inc *ALMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - inc.CalculateAndUpdate(window) -} - -func (inc *ALMA) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) -} diff --git a/pkg/indicator/ca_callbacks.go b/pkg/indicator/ca_callbacks.go index 4883dc6a62..d4b999952c 100644 --- a/pkg/indicator/ca_callbacks.go +++ b/pkg/indicator/ca_callbacks.go @@ -5,11 +5,11 @@ package indicator import () func (inc *CA) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) + inc.updateCallbacks = append(inc.updateCallbacks, cb) } func (inc *CA) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { + for _, cb := range inc.updateCallbacks { cb(value) } } diff --git a/pkg/indicator/cci.go b/pkg/indicator/cci.go index dd24b46c3b..f11f337138 100644 --- a/pkg/indicator/cci.go +++ b/pkg/indicator/cci.go @@ -98,15 +98,3 @@ func (inc *CCI) CalculateAndUpdate(allKLines []types.KLine) { inc.EmitUpdate(inc.Last(0)) } } - -func (inc *CCI) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - - inc.CalculateAndUpdate(window) -} - -func (inc *CCI) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) -} diff --git a/pkg/indicator/cma.go b/pkg/indicator/cma.go index 51043f8054..7a4117194e 100644 --- a/pkg/indicator/cma.go +++ b/pkg/indicator/cma.go @@ -14,13 +14,14 @@ type CA struct { Interval types.Interval Values floats.Slice length float64 - UpdateCallbacks []func(value float64) + updateCallbacks []func(value float64) } func (inc *CA) Update(x float64) { if len(inc.Values) == 0 { inc.SeriesBase.Series = inc } + newVal := (inc.Values.Last(0)*inc.length + x) / (inc.length + 1.) inc.length += 1 inc.Values.Push(newVal) @@ -54,15 +55,3 @@ func (inc *CA) CalculateAndUpdate(allKLines []types.KLine) { inc.EmitUpdate(inc.Last(0)) } } - -func (inc *CA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - - inc.CalculateAndUpdate(window) -} - -func (inc *CA) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) -} diff --git a/pkg/indicator/dmi.go b/pkg/indicator/dmi.go index 0bf270477a..7b98dbdd99 100644 --- a/pkg/indicator/dmi.go +++ b/pkg/indicator/dmi.go @@ -115,15 +115,3 @@ func (inc *DMI) CalculateAndUpdate(allKLines []types.KLine) { inc.EmitUpdate(inc.DIPlus.Last(0), inc.DIMinus.Last(0), inc.ADX.Last(0)) } } - -func (inc *DMI) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - - inc.CalculateAndUpdate(window) -} - -func (inc *DMI) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) -} diff --git a/pkg/indicator/v2_cma.go b/pkg/indicator/v2_cma.go new file mode 100644 index 0000000000..f3c485aff4 --- /dev/null +++ b/pkg/indicator/v2_cma.go @@ -0,0 +1,23 @@ +package indicator + +type CMAStream struct { + Float64Series +} + +func CMA2(source Float64Source) *CMAStream { + s := &CMAStream{ + Float64Series: NewFloat64Series(), + } + s.Bind(source, s) + return s +} + +func (s *CMAStream) Calculate(x float64) float64 { + l := float64(s.slice.Length()) + cma := (s.slice.Last(0)*l + x) / (l + 1.) + return cma +} + +func (s *CMAStream) Truncate() { + s.slice.Truncate(MaxNumOfEWMA) +} From 1535572b4392b47d548ad634b02de83e5ecbf8f3 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 14:30:16 +0800 Subject: [PATCH 0938/1392] indicator: add multiply operator --- pkg/indicator/v2_multiply.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 pkg/indicator/v2_multiply.go diff --git a/pkg/indicator/v2_multiply.go b/pkg/indicator/v2_multiply.go new file mode 100644 index 0000000000..3599e7f6be --- /dev/null +++ b/pkg/indicator/v2_multiply.go @@ -0,0 +1,19 @@ +package indicator + +type MultiplyStream struct { + Float64Series + multiplier float64 +} + +func Multiply(source Float64Source, multiplier float64) *MultiplyStream { + s := &MultiplyStream{ + Float64Series: NewFloat64Series(), + multiplier: multiplier, + } + s.Bind(source, s) + return s +} + +func (s *MultiplyStream) Calculate(v float64) float64 { + return v * s.multiplier +} From 4f07a44b6116c61496f1487108fb07d084808a4f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 14:43:29 +0800 Subject: [PATCH 0939/1392] indicator: add v2 stochastic oscillator --- pkg/indicator/v2_stoch.go | 61 +++++++++++++++++++ pkg/indicator/{subtract.go => v2_subtract.go} | 0 2 files changed, 61 insertions(+) create mode 100644 pkg/indicator/v2_stoch.go rename pkg/indicator/{subtract.go => v2_subtract.go} (100%) diff --git a/pkg/indicator/v2_stoch.go b/pkg/indicator/v2_stoch.go new file mode 100644 index 0000000000..a821d8cf0a --- /dev/null +++ b/pkg/indicator/v2_stoch.go @@ -0,0 +1,61 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// Stochastic Oscillator +// - https://www.investopedia.com/terms/s/stochasticoscillator.asp +// +// The Stochastic Oscillator is a technical analysis indicator that is used to identify potential overbought or oversold conditions +// in a security's price. It is calculated by taking the current closing price of the security and comparing it to the high and low prices +// over a specified period of time. This comparison is then plotted as a line on the price chart, with values above 80 indicating overbought +// conditions and values below 20 indicating oversold conditions. The Stochastic Oscillator can be used by traders to identify potential +// entry and exit points for trades, or to confirm other technical analysis signals. It is typically used in conjunction with other indicators +// to provide a more comprehensive view of the security's price. + +//go:generate callbackgen -type StochStream +type StochStream struct { + types.SeriesBase + + K, D floats.Slice + + window int + dPeriod int + + highPrices, lowPrices *PriceStream + + updateCallbacks []func(k, d float64) +} + +// Stochastic Oscillator +func Stoch2(source KLineSubscription, window, dPeriod int) *StochStream { + highPrices := HighPrices(source) + lowPrices := LowPrices(source) + + s := &StochStream{ + window: window, + dPeriod: dPeriod, + highPrices: highPrices, + lowPrices: lowPrices, + } + + source.AddSubscriber(func(kLine types.KLine) { + lowest := s.lowPrices.slice.Tail(s.window).Min() + highest := s.highPrices.slice.Tail(s.window).Max() + + var k float64 = 50.0 + var d float64 = 0.0 + + if highest != lowest { + k = 100.0 * (kLine.Close.Float64() - lowest) / (highest - lowest) + } + + d = s.K.Tail(s.dPeriod).Mean() + s.K.Push(k) + s.D.Push(d) + s.EmitUpdate(k, d) + }) + return s +} diff --git a/pkg/indicator/subtract.go b/pkg/indicator/v2_subtract.go similarity index 100% rename from pkg/indicator/subtract.go rename to pkg/indicator/v2_subtract.go From 66d99ce6ae1441abb5c7078f6dee2385cbf3268b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 14:52:09 +0800 Subject: [PATCH 0940/1392] indicator: add stoch test --- pkg/indicator/stochstream_callbacks.go | 15 +++++ pkg/indicator/v2_stoch.go | 3 +- pkg/indicator/v2_stoch_test.go | 80 ++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 pkg/indicator/stochstream_callbacks.go create mode 100644 pkg/indicator/v2_stoch_test.go diff --git a/pkg/indicator/stochstream_callbacks.go b/pkg/indicator/stochstream_callbacks.go new file mode 100644 index 0000000000..539b13992a --- /dev/null +++ b/pkg/indicator/stochstream_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type StochStream"; DO NOT EDIT. + +package indicator + +import () + +func (S *StochStream) OnUpdate(cb func(k float64, d float64)) { + S.updateCallbacks = append(S.updateCallbacks, cb) +} + +func (S *StochStream) EmitUpdate(k float64, d float64) { + for _, cb := range S.updateCallbacks { + cb(k, d) + } +} diff --git a/pkg/indicator/v2_stoch.go b/pkg/indicator/v2_stoch.go index a821d8cf0a..462e4b851b 100644 --- a/pkg/indicator/v2_stoch.go +++ b/pkg/indicator/v2_stoch.go @@ -52,8 +52,9 @@ func Stoch2(source KLineSubscription, window, dPeriod int) *StochStream { k = 100.0 * (kLine.Close.Float64() - lowest) / (highest - lowest) } - d = s.K.Tail(s.dPeriod).Mean() s.K.Push(k) + + d = s.K.Tail(s.dPeriod).Mean() s.D.Push(d) s.EmitUpdate(k, d) }) diff --git a/pkg/indicator/v2_stoch_test.go b/pkg/indicator/v2_stoch_test.go new file mode 100644 index 0000000000..fa5f05b098 --- /dev/null +++ b/pkg/indicator/v2_stoch_test.go @@ -0,0 +1,80 @@ +package indicator + +import ( + "encoding/json" + "math" + "testing" + "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +/* +python +import pandas as pd +import pandas_ta as ta + +klines = ... +df = pd.DataFrame(klines, columns=['open', 'high', 'low', 'close', 'volume']) + + print(df.ta.stoch(df['high'], df['low'], df['close'], k=14, d=3, smooth_k=1)) +*/ +func TestSTOCH2_update(t *testing.T) { + open := []byte(`[8273.0, 8280.0, 8280.0, 8275.0, 8281.0, 8277.0, 8279.0, 8280.0, 8284.0, 8286.0, 8283.0, 8283.0, 8284.0, 8286.0, 8285.0, 8287.0, 8289.0, 8282.0, 8286.0, 8279.0, 8275.0, 8276.0, 8276.0, 8281.0, 8269.0, 8256.0, 8258.0, 8252.0, 8241.0, 8232.0, 8218.0, 8221.0, 8216.0, 8210.0, 8212.0, 8201.0, 8197.0, 8200.0, 8193.0, 8181.0, 8185.0, 8190.0, 8184.0, 8185.0, 8163.0, 8153.0, 8162.0, 8165.0, 8162.0, 8157.0, 8159.0, 8141.0, 8140.0, 8141.0, 8130.0, 8144.0, 8141.0, 8148.0, 8145.0, 8134.0, 8123.0, 8127.0, 8130.0, 8125.0, 8122.0, 8105.0, 8096.0, 8103.0, 8102.0, 8110.0, 8104.0, 8109.0, 8103.0, 8111.0, 8112.0, 8109.0, 8092.0, 8100.0, 8101.0, 8100.0, 8096.0, 8095.0, 8094.0, 8101.0, 8095.0, 8069.0, 8067.0, 8070.0, 8069.0, 8066.0, 8047.0, 8046.0, 8042.0, 8039.0, 8049.0, 8055.0, 8063.0, 8061.0, 8056.0, 8057.0, 8056.0, 8057.0, 8057.0, 8054.0, 8056.0, 8056.0, 8065.0, 8065.0, 8070.0, 8065.0, 8064.0, 8063.0, 8060.0, 8065.0, 8068.0, 8068.0, 8069.0, 8073.0, 8073.0, 8084.0, 8084.0, 8076.0, 8074.0, 8074.0, 8074.0, 8078.0, 8080.0, 8082.0, 8085.0, 8083.0, 8087.0, 8087.0, 8083.0, 8083.0, 8082.0, 8074.0, 8074.0, 8071.0, 8071.0, 8072.0, 8075.0, 8075.0, 8076.0, 8073.0, 8071.0, 8070.0, 8075.0, 8078.0, 8077.0, 8075.0, 8073.0, 8079.0, 8084.0, 8082.0, 8085.0, 8085.0, 8085.0, 8101.0, 8106.0, 8113.0, 8109.0, 8104.0, 8105.0, 8105.0, 8107.0, 8106.0, 8104.0, 8106.0, 8106.0, 8110.0, 8107.0, 8110.0, 8111.0, 8104.0, 8098.0, 8098.0, 8098.0, 8098.0, 8094.0, 8097.0, 8096.0, 8099.0, 8098.0, 8099.0, 8098.0, 8095.0, 8096.0, 8086.0, 8088.0, 8093.0, 8092.0, 8096.0, 8100.0, 8104.0, 8104.0, 8108.0, 8107.0, 8103.0, 8104.0, 8110.0, 8105.0, 8102.0, 8104.0, 8096.0, 8099.0, 8103.0, 8102.0, 8108.0, 8107.0, 8107.0, 8104.0, 8095.0, 8091.0, 8092.0, 8090.0, 8093.0, 8093.0, 8094.0, 8095.0, 8096.0, 8088.0, 8090.0, 8079.0, 8077.0, 8079.0, 8081.0, 8083.0, 8084.0, 8084.0, 8087.0, 8091.0, 8089.0, 8089.0, 8091.0, 8087.0, 8093.0, 8090.0, 8090.0, 8095.0, 8093.0, 8088.0, 8087.0, 8090.0, 8089.0, 8087.0, 8084.0, 8087.0, 8084.0, 8080.0, 8078.0, 8077.0, 8077.0, 8076.0, 8072.0, 8072.0, 8075.0, 8076.0, 8074.0, 8077.0, 8081.0, 8080.0, 8076.0, 8075.0, 8077.0, 8080.0, 8077.0, 8076.0, 8076.0, 8070.0, 8071.0, 8070.0, 8073.0, 8069.0, 8069.0, 8068.0, 8072.0, 8078.0, 8077.0, 8079.0, 8081.0, 8076.0, 8076.0, 8077.0, 8077.0, 8078.0, 8075.0, 8066.0, 8064.0, 8064.0, 8062.0, 8062.0, 8065.0, 8062.0, 8063.0, 8074.0, 8070.0, 8069.0, 8068.0, 8074.0, 8075.0]`) + high := []byte(`[8279.0, 8282.0, 8280.0, 8280.0, 8284.0, 8284.0, 8280.0, 8282.0, 8284.0, 8289.0, 8288.0, 8285.0, 8284.0, 8287.0, 8286.0, 8294.0, 8290.0, 8292.0, 8289.0, 8288.0, 8278.0, 8279.0, 8279.0, 8284.0, 8282.0, 8270.0, 8261.0, 8260.0, 8252.0, 8244.0, 8233.0, 8227.0, 8222.0, 8217.0, 8217.0, 8211.0, 8202.0, 8203.0, 8203.0, 8196.0, 8186.0, 8193.0, 8194.0, 8187.0, 8185.0, 8168.0, 8165.0, 8169.0, 8166.0, 8163.0, 8162.0, 8159.0, 8143.0, 8148.0, 8143.0, 8146.0, 8152.0, 8149.0, 8152.0, 8147.0, 8138.0, 8128.0, 8134.0, 8131.0, 8133.0, 8123.0, 8106.0, 8105.0, 8104.0, 8113.0, 8112.0, 8112.0, 8111.0, 8114.0, 8115.0, 8114.0, 8110.0, 8101.0, 8107.0, 8103.0, 8100.0, 8101.0, 8100.0, 8102.0, 8101.0, 8100.0, 8070.0, 8076.0, 8072.0, 8072.0, 8069.0, 8050.0, 8048.0, 8044.0, 8049.0, 8055.0, 8063.0, 8070.0, 8067.0, 8061.0, 8059.0, 8060.0, 8063.0, 8058.0, 8061.0, 8061.0, 8068.0, 8066.0, 8071.0, 8073.0, 8068.0, 8066.0, 8066.0, 8065.0, 8070.0, 8072.0, 8072.0, 8075.0, 8078.0, 8084.0, 8085.0, 8084.0, 8077.0, 8076.0, 8075.0, 8079.0, 8081.0, 8083.0, 8088.0, 8086.0, 8088.0, 8088.0, 8092.0, 8086.0, 8086.0, 8083.0, 8075.0, 8074.0, 8073.0, 8073.0, 8077.0, 8077.0, 8078.0, 8077.0, 8076.0, 8073.0, 8075.0, 8079.0, 8079.0, 8078.0, 8074.0, 8080.0, 8086.0, 8086.0, 8085.0, 8085.0, 8087.0, 8102.0, 8109.0, 8113.0, 8114.0, 8110.0, 8105.0, 8106.0, 8109.0, 8114.0, 8107.0, 8106.0, 8106.0, 8110.0, 8111.0, 8110.0, 8112.0, 8112.0, 8109.0, 8102.0, 8098.0, 8099.0, 8098.0, 8097.0, 8099.0, 8099.0, 8099.0, 8102.0, 8099.0, 8099.0, 8096.0, 8097.0, 8091.0, 8094.0, 8094.0, 8096.0, 8102.0, 8106.0, 8109.0, 8109.0, 8110.0, 8108.0, 8106.0, 8110.0, 8122.0, 8105.0, 8105.0, 8104.0, 8103.0, 8104.0, 8103.0, 8110.0, 8110.0, 8107.0, 8109.0, 8105.0, 8097.0, 8095.0, 8093.0, 8094.0, 8097.0, 8096.0, 8096.0, 8096.0, 8097.0, 8092.0, 8090.0, 8081.0, 8081.0, 8083.0, 8087.0, 8085.0, 8085.0, 8087.0, 8092.0, 8094.0, 8090.0, 8093.0, 8092.0, 8094.0, 8093.0, 8091.0, 8095.0, 8095.0, 8092.0, 8089.0, 8090.0, 8090.0, 8091.0, 8088.0, 8089.0, 8089.0, 8085.0, 8081.0, 8080.0, 8078.0, 8078.0, 8076.0, 8073.0, 8077.0, 8078.0, 8077.0, 8077.0, 8083.0, 8082.0, 8082.0, 8077.0, 8079.0, 8082.0, 8080.0, 8077.0, 8078.0, 8076.0, 8073.0, 8074.0, 8073.0, 8073.0, 8070.0, 8070.0, 8072.0, 8079.0, 8078.0, 8079.0, 8081.0, 8083.0, 8077.0, 8078.0, 8080.0, 8079.0, 8080.0, 8077.0, 8069.0, 8071.0, 8066.0, 8064.0, 8066.0, 8066.0, 8063.0, 8074.0, 8075.0, 8071.0, 8070.0, 8075.0, 8075.0]`) + low := []byte(`[8260.0, 8272.0, 8275.0, 8274.0, 8275.0, 8277.0, 8276.0, 8278.0, 8277.0, 8283.0, 8282.0, 8283.0, 8283.0, 8283.0, 8283.0, 8279.0, 8281.0, 8282.0, 8277.0, 8276.0, 8273.0, 8275.0, 8274.0, 8275.0, 8266.0, 8256.0, 8255.0, 8250.0, 8239.0, 8230.0, 8214.0, 8218.0, 8216.0, 8208.0, 8209.0, 8201.0, 8190.0, 8195.0, 8193.0, 8181.0, 8175.0, 8183.0, 8182.0, 8181.0, 8159.0, 8152.0, 8150.0, 8160.0, 8161.0, 8153.0, 8153.0, 8137.0, 8135.0, 8139.0, 8130.0, 8130.0, 8140.0, 8137.0, 8145.0, 8134.0, 8123.0, 8116.0, 8122.0, 8124.0, 8122.0, 8105.0, 8096.0, 8096.0, 8097.0, 8100.0, 8100.0, 8104.0, 8101.0, 8103.0, 8109.0, 8108.0, 8089.0, 8092.0, 8097.0, 8098.0, 8094.0, 8092.0, 8087.0, 8094.0, 8094.0, 8069.0, 8058.0, 8065.0, 8066.0, 8065.0, 8046.0, 8041.0, 8036.0, 8038.0, 8039.0, 8047.0, 8053.0, 8058.0, 8056.0, 8056.0, 8053.0, 8052.0, 8054.0, 8051.0, 8053.0, 8056.0, 8055.0, 8063.0, 8064.0, 8063.0, 8062.0, 8061.0, 8059.0, 8059.0, 8063.0, 8066.0, 8067.0, 8068.0, 8071.0, 8071.0, 8079.0, 8074.0, 8073.0, 8074.0, 8073.0, 8073.0, 8076.0, 8079.0, 8080.0, 8083.0, 8083.0, 8085.0, 8082.0, 8082.0, 8081.0, 8072.0, 8072.0, 8068.0, 8070.0, 8070.0, 8072.0, 8074.0, 8075.0, 8073.0, 8071.0, 8070.0, 8067.0, 8074.0, 8076.0, 8072.0, 8070.0, 8072.0, 8079.0, 8081.0, 8082.0, 8082.0, 8084.0, 8083.0, 8097.0, 8103.0, 8107.0, 8104.0, 8103.0, 8104.0, 8103.0, 8105.0, 8103.0, 8102.0, 8102.0, 8103.0, 8106.0, 8107.0, 8108.0, 8102.0, 8098.0, 8096.0, 8095.0, 8096.0, 8093.0, 8094.0, 8094.0, 8096.0, 8097.0, 8097.0, 8096.0, 8094.0, 8094.0, 8086.0, 8086.0, 8087.0, 8090.0, 8091.0, 8095.0, 8099.0, 8104.0, 8102.0, 8106.0, 8101.0, 8103.0, 8104.0, 8104.0, 8101.0, 8102.0, 8096.0, 8096.0, 8098.0, 8100.0, 8102.0, 8106.0, 8103.0, 8103.0, 8094.0, 8090.0, 8090.0, 8089.0, 8088.0, 8090.0, 8093.0, 8094.0, 8094.0, 8088.0, 8087.0, 8079.0, 8075.0, 8076.0, 8077.0, 8081.0, 8083.0, 8083.0, 8084.0, 8087.0, 8089.0, 8088.0, 8088.0, 8086.0, 8087.0, 8090.0, 8088.0, 8090.0, 8091.0, 8087.0, 8087.0, 8086.0, 8088.0, 8087.0, 8082.0, 8083.0, 8083.0, 8078.0, 8077.0, 8077.0, 8072.0, 8074.0, 8071.0, 8070.0, 8072.0, 8073.0, 8073.0, 8072.0, 8076.0, 8079.0, 8075.0, 8075.0, 8075.0, 8076.0, 8076.0, 8074.0, 8076.0, 8069.0, 8068.0, 8069.0, 8069.0, 8065.0, 8067.0, 8067.0, 8067.0, 8073.0, 8075.0, 8076.0, 8077.0, 8075.0, 8072.0, 8074.0, 8075.0, 8074.0, 8072.0, 8066.0, 8063.0, 8062.0, 8058.0, 8060.0, 8059.0, 8060.0, 8059.0, 8062.0, 8067.0, 8068.0, 8067.0, 8068.0, 8071.0]`) + cloze := []byte(`[8262.0, 8273.0, 8279.0, 8279.0, 8275.0, 8282.0, 8278.0, 8279.0, 8281.0, 8285.0, 8287.0, 8284.0, 8283.0, 8283.0, 8285.0, 8286.0, 8287.0, 8290.0, 8283.0, 8287.0, 8278.0, 8275.0, 8276.0, 8275.0, 8281.0, 8270.0, 8257.0, 8258.0, 8252.0, 8243.0, 8231.0, 8219.0, 8220.0, 8216.0, 8210.0, 8211.0, 8201.0, 8197.0, 8201.0, 8193.0, 8183.0, 8184.0, 8191.0, 8184.0, 8185.0, 8161.0, 8154.0, 8163.0, 8164.0, 8162.0, 8156.0, 8158.0, 8141.0, 8139.0, 8142.0, 8130.0, 8145.0, 8140.0, 8149.0, 8146.0, 8136.0, 8123.0, 8126.0, 8130.0, 8125.0, 8122.0, 8106.0, 8096.0, 8103.0, 8102.0, 8111.0, 8105.0, 8111.0, 8103.0, 8112.0, 8113.0, 8109.0, 8093.0, 8101.0, 8101.0, 8100.0, 8095.0, 8096.0, 8095.0, 8100.0, 8095.0, 8069.0, 8068.0, 8072.0, 8068.0, 8067.0, 8046.0, 8045.0, 8043.0, 8040.0, 8049.0, 8055.0, 8062.0, 8062.0, 8058.0, 8056.0, 8055.0, 8058.0, 8057.0, 8054.0, 8056.0, 8057.0, 8066.0, 8065.0, 8069.0, 8064.0, 8063.0, 8064.0, 8059.0, 8065.0, 8069.0, 8068.0, 8069.0, 8072.0, 8074.0, 8084.0, 8084.0, 8076.0, 8074.0, 8074.0, 8075.0, 8077.0, 8080.0, 8082.0, 8086.0, 8084.0, 8087.0, 8087.0, 8083.0, 8083.0, 8082.0, 8074.0, 8073.0, 8072.0, 8071.0, 8072.0, 8075.0, 8076.0, 8076.0, 8074.0, 8071.0, 8071.0, 8075.0, 8079.0, 8077.0, 8074.0, 8072.0, 8079.0, 8084.0, 8082.0, 8085.0, 8086.0, 8084.0, 8102.0, 8107.0, 8113.0, 8109.0, 8104.0, 8104.0, 8105.0, 8108.0, 8106.0, 8104.0, 8106.0, 8105.0, 8110.0, 8107.0, 8109.0, 8112.0, 8104.0, 8099.0, 8097.0, 8097.0, 8098.0, 8095.0, 8096.0, 8097.0, 8099.0, 8098.0, 8099.0, 8099.0, 8095.0, 8097.0, 8086.0, 8088.0, 8093.0, 8092.0, 8096.0, 8101.0, 8105.0, 8105.0, 8109.0, 8107.0, 8103.0, 8104.0, 8109.0, 8105.0, 8102.0, 8104.0, 8097.0, 8100.0, 8103.0, 8103.0, 8109.0, 8107.0, 8106.0, 8104.0, 8096.0, 8090.0, 8092.0, 8089.0, 8093.0, 8093.0, 8094.0, 8095.0, 8096.0, 8088.0, 8089.0, 8079.0, 8077.0, 8079.0, 8082.0, 8083.0, 8084.0, 8084.0, 8087.0, 8091.0, 8088.0, 8088.0, 8091.0, 8087.0, 8092.0, 8090.0, 8091.0, 8095.0, 8092.0, 8088.0, 8087.0, 8090.0, 8089.0, 8087.0, 8084.0, 8088.0, 8084.0, 8079.0, 8078.0, 8078.0, 8076.0, 8075.0, 8071.0, 8072.0, 8074.0, 8077.0, 8074.0, 8077.0, 8081.0, 8080.0, 8076.0, 8076.0, 8078.0, 8079.0, 8076.0, 8076.0, 8076.0, 8070.0, 8072.0, 8069.0, 8072.0, 8070.0, 8069.0, 8069.0, 8073.0, 8078.0, 8077.0, 8079.0, 8080.0, 8076.0, 8076.0, 8076.0, 8077.0, 8078.0, 8075.0, 8067.0, 8064.0, 8064.0, 8062.0, 8062.0, 8065.0, 8062.0, 8063.0, 8074.0, 8070.0, 8069.0, 8068.0, 8074.0]`) + + buildKLines := func(open, high, low, cloze []fixedpoint.Value) (kLines []types.KLine) { + for i := range high { + kLines = append(kLines, types.KLine{Open: open[i], High: high[i], Low: low[i], Close: cloze[i], EndTime: types.Time(time.Now())}) + } + return kLines + } + var o, h, l, c []fixedpoint.Value + _ = json.Unmarshal(open, &o) + _ = json.Unmarshal(high, &h) + _ = json.Unmarshal(low, &l) + _ = json.Unmarshal(cloze, &c) + + tests := []struct { + name string + kLines []types.KLine + window int + wantK float64 + wantD float64 + }{ + { + name: "TXF1-1min_2016/1/4", + kLines: buildKLines(o, h, l, c), + window: 14, + wantK: 84.210526, + wantD: 59.888357, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stream := &types.StandardStream{} + kLines := KLines(stream, "", "") + kd := Stoch2(kLines, tt.window, DPeriod) + + for _, k := range tt.kLines { + stream.EmitKLineClosed(k) + } + + gotK := kd.K.Last(0) + diff_k := math.Trunc((gotK-tt.wantK)*100) / 100 + if diff_k != 0 { + t.Errorf("%%K() = %v, want %v", gotK, tt.wantK) + } + + gotD := kd.D.Last(0) + diff_d := math.Trunc((gotD-tt.wantD)*100) / 100 + if diff_d != 0 { + t.Errorf("%%D() = %v, want %v", gotD, tt.wantD) + } + }) + } +} From b0abc1bf551f9d5eec6d362bbfefadc7c2856835 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 14:54:25 +0800 Subject: [PATCH 0941/1392] indicator: drop ssf unused func --- pkg/indicator/ssf.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pkg/indicator/ssf.go b/pkg/indicator/ssf.go index 3a0f71bb35..689c3b19db 100644 --- a/pkg/indicator/ssf.go +++ b/pkg/indicator/ssf.go @@ -80,9 +80,6 @@ func (inc *SSF) Index(i int) float64 { } func (inc *SSF) Length() int { - if inc.Values == nil { - return 0 - } return inc.Values.Length() } @@ -104,14 +101,3 @@ func (inc *SSF) CalculateAndUpdate(allKLines []types.KLine) { inc.EmitUpdate(inc.Last(0)) } } - -func (inc *SSF) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - inc.CalculateAndUpdate(window) -} - -func (inc *SSF) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) -} From 3d4b88fa7dca90e94d6e0223cac947f904b90074 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 14:59:02 +0800 Subject: [PATCH 0942/1392] indicator: drop unused stddev code --- pkg/indicator/stddev.go | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/pkg/indicator/stddev.go b/pkg/indicator/stddev.go index 7e06c448c4..7abb4a84c2 100644 --- a/pkg/indicator/stddev.go +++ b/pkg/indicator/stddev.go @@ -54,32 +54,3 @@ func (inc *StdDev) PushK(k types.KLine) { inc.Update(k.Close.Float64()) inc.EndTime = k.EndTime.Time() } - -func (inc *StdDev) CalculateAndUpdate(allKLines []types.KLine) { - var last = allKLines[len(allKLines)-1] - - if inc.rawValues == nil { - for _, k := range allKLines { - if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { - continue - } - inc.PushK(k) - } - } else { - inc.PushK(last) - } - - inc.EmitUpdate(inc.Values.Last(0)) -} - -func (inc *StdDev) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - - inc.CalculateAndUpdate(window) -} - -func (inc *StdDev) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) -} From b141ae3ece1d4c8938a014a51ec289a54fdfe749 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 15:19:12 +0800 Subject: [PATCH 0943/1392] indicator: add stddev v2 --- pkg/indicator/v2_stddev.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 pkg/indicator/v2_stddev.go diff --git a/pkg/indicator/v2_stddev.go b/pkg/indicator/v2_stddev.go new file mode 100644 index 0000000000..28a5a4d070 --- /dev/null +++ b/pkg/indicator/v2_stddev.go @@ -0,0 +1,28 @@ +package indicator + +import "github.com/c9s/bbgo/pkg/types" + +type StdDevStream struct { + Float64Series + + rawValues *types.Queue + + window int + multiplier float64 +} + +func StdDev2(source Float64Source, window int) *StdDevStream { + s := &StdDevStream{ + Float64Series: NewFloat64Series(), + rawValues: types.NewQueue(window), + window: window, + } + s.Bind(source, s) + return s +} + +func (s *StdDevStream) Calculate(x float64) float64 { + s.rawValues.Update(x) + var std = s.rawValues.Stdev() + return std +} From 0b01750528bf4fe4737ed96ffd0cb4970be177c5 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 15:56:47 +0800 Subject: [PATCH 0944/1392] indicator: drop unused code --- pkg/indicator/tma.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/pkg/indicator/tma.go b/pkg/indicator/tma.go index fd1c416dba..36e3ad15f8 100644 --- a/pkg/indicator/tma.go +++ b/pkg/indicator/tma.go @@ -48,28 +48,3 @@ var _ types.SeriesExtend = &TMA{} func (inc *TMA) PushK(k types.KLine) { inc.Update(k.Close.Float64()) } - -func (inc *TMA) CalculateAndUpdate(allKLines []types.KLine) { - if inc.s1 == nil { - for _, k := range allKLines { - inc.PushK(k) - inc.EmitUpdate(inc.Last(0)) - } - } else { - k := allKLines[len(allKLines)-1] - inc.PushK(k) - inc.EmitUpdate(inc.Last(0)) - } -} - -func (inc *TMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - - inc.CalculateAndUpdate(window) -} - -func (inc *TMA) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) -} From 8a8edc7bb64bc17f6035e4c50cb792c57e6eef05 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 16:52:02 +0800 Subject: [PATCH 0945/1392] indicator: rename price.go to v2_price.go --- pkg/indicator/{price.go => v2_price.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg/indicator/{price.go => v2_price.go} (100%) diff --git a/pkg/indicator/price.go b/pkg/indicator/v2_price.go similarity index 100% rename from pkg/indicator/price.go rename to pkg/indicator/v2_price.go From 9a486388faebcdf94c3248971516e03413a93c37 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 17:17:14 +0800 Subject: [PATCH 0946/1392] indicator: add v2 pivot low indicator --- pkg/indicator/float64updater.go | 14 +++++++++----- pkg/indicator/v2_pivotlow.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 pkg/indicator/v2_pivotlow.go diff --git a/pkg/indicator/float64updater.go b/pkg/indicator/float64updater.go index 8d2d9e3b46..13adfa6fae 100644 --- a/pkg/indicator/float64updater.go +++ b/pkg/indicator/float64updater.go @@ -40,6 +40,14 @@ func (f *Float64Series) PushAndEmit(x float64) { f.EmitUpdate(x) } +func (f *Float64Series) Subscribe(source Float64Source, c func(x float64)) { + if sub, ok := source.(Float64Subscription); ok { + sub.AddSubscriber(c) + } else { + source.OnUpdate(c) + } +} + // Bind binds the source event to the target (Float64Calculator) // A Float64Calculator should be able to calculate the float64 result from a single float64 argument input func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) { @@ -60,9 +68,5 @@ func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) { } } - if sub, ok := source.(Float64Subscription); ok { - sub.AddSubscriber(c) - } else { - source.OnUpdate(c) - } + f.Subscribe(source, c) } diff --git a/pkg/indicator/v2_pivotlow.go b/pkg/indicator/v2_pivotlow.go new file mode 100644 index 0000000000..a20e00f19c --- /dev/null +++ b/pkg/indicator/v2_pivotlow.go @@ -0,0 +1,29 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" +) + +type PivotLowStream struct { + Float64Series + + rawValues floats.Slice + + window, rightWindow int +} + +func PivotLow2(source Float64Source, window, rightWindow int) *PivotLowStream { + s := &PivotLowStream{ + Float64Series: NewFloat64Series(), + window: window, + rightWindow: rightWindow, + } + + s.Subscribe(source, func(x float64) { + s.rawValues.Push(x) + if low, ok := calculatePivotLow(s.rawValues, s.window, s.rightWindow); ok { + s.PushAndEmit(low) + } + }) + return s +} From 15d1caef314d76ea51420b668c88ddc250f8a59d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Jun 2023 17:31:12 +0800 Subject: [PATCH 0947/1392] indicator: add pivothigh v2 indicator --- pkg/indicator/v2_pivothigh.go | 27 +++++++++++++++++++++++++++ pkg/indicator/v2_pivotlow.go | 4 +--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 pkg/indicator/v2_pivothigh.go diff --git a/pkg/indicator/v2_pivothigh.go b/pkg/indicator/v2_pivothigh.go new file mode 100644 index 0000000000..eb0352a731 --- /dev/null +++ b/pkg/indicator/v2_pivothigh.go @@ -0,0 +1,27 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" +) + +type PivotHighStream struct { + Float64Series + rawValues floats.Slice + window, rightWindow int +} + +func PivotHigh2(source Float64Source, window, rightWindow int) *PivotHighStream { + s := &PivotHighStream{ + Float64Series: NewFloat64Series(), + window: window, + rightWindow: rightWindow, + } + + s.Subscribe(source, func(x float64) { + s.rawValues.Push(x) + if low, ok := calculatePivotHigh(s.rawValues, s.window, s.rightWindow); ok { + s.PushAndEmit(low) + } + }) + return s +} diff --git a/pkg/indicator/v2_pivotlow.go b/pkg/indicator/v2_pivotlow.go index a20e00f19c..47d76308ff 100644 --- a/pkg/indicator/v2_pivotlow.go +++ b/pkg/indicator/v2_pivotlow.go @@ -6,9 +6,7 @@ import ( type PivotLowStream struct { Float64Series - - rawValues floats.Slice - + rawValues floats.Slice window, rightWindow int } From aae7fd310ebed16e94992b0c7def97eb66e63b05 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 2 Jun 2023 18:56:28 +0800 Subject: [PATCH 0948/1392] indicator: add ATRP indicator --- pkg/indicator/v2_atrp.go | 19 +++++++++++++++++++ pkg/indicator/v2_price.go | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 pkg/indicator/v2_atrp.go diff --git a/pkg/indicator/v2_atrp.go b/pkg/indicator/v2_atrp.go new file mode 100644 index 0000000000..6261c1cb37 --- /dev/null +++ b/pkg/indicator/v2_atrp.go @@ -0,0 +1,19 @@ +package indicator + +type ATRPStream struct { + Float64Series +} + +func ATRP2(source KLineSubscription, window int) *ATRPStream { + s := &ATRPStream{} + tr := TR2(source) + atr := RMA2(tr, window, true) + atr.OnUpdate(func(x float64) { + // x is the last rma + k := source.Last(0) + cloze := k.Close.Float64() + atrp := x / cloze + s.PushAndEmit(atrp) + }) + return s +} diff --git a/pkg/indicator/v2_price.go b/pkg/indicator/v2_price.go index e80736715e..d95976ddaa 100644 --- a/pkg/indicator/v2_price.go +++ b/pkg/indicator/v2_price.go @@ -6,6 +6,8 @@ import ( type KLineSubscription interface { AddSubscriber(f func(k types.KLine)) + Length() int + Last(i int) *types.KLine } type PriceStream struct { From 97e7b93997128ec33b08a613f3a95f5226bc3360 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Jun 2023 14:08:20 +0800 Subject: [PATCH 0949/1392] indicator: rewrite Multiply to make it consistent with Subtract --- pkg/indicator/float64updater.go | 4 ++++ pkg/indicator/v2_macd.go | 12 +++++------ pkg/indicator/v2_multiply.go | 35 +++++++++++++++++++++++++++------ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/pkg/indicator/float64updater.go b/pkg/indicator/float64updater.go index 13adfa6fae..7afdef8aac 100644 --- a/pkg/indicator/float64updater.go +++ b/pkg/indicator/float64updater.go @@ -35,6 +35,10 @@ func (f *Float64Series) Length() int { return len(f.slice) } +func (f *Float64Series) Slice() floats.Slice { + return f.slice +} + func (f *Float64Series) PushAndEmit(x float64) { f.slice.Push(x) f.EmitUpdate(x) diff --git a/pkg/indicator/v2_macd.go b/pkg/indicator/v2_macd.go index 2e52534ac0..de62cd4964 100644 --- a/pkg/indicator/v2_macd.go +++ b/pkg/indicator/v2_macd.go @@ -5,8 +5,8 @@ type MACDStream struct { shortWindow, longWindow, signalWindow int - fastEWMA, slowEWMA, signal *EWMAStream - histogram *SubtractStream + FastEWMA, SlowEWMA, Signal *EWMAStream + Histogram *SubtractStream } func MACD2(source Float64Source, shortWindow, longWindow, signalWindow int) *MACDStream { @@ -21,9 +21,9 @@ func MACD2(source Float64Source, shortWindow, longWindow, signalWindow int) *MAC shortWindow: shortWindow, longWindow: longWindow, signalWindow: signalWindow, - fastEWMA: fastEWMA, - slowEWMA: slowEWMA, - signal: signal, - histogram: histogram, + FastEWMA: fastEWMA, + SlowEWMA: slowEWMA, + Signal: signal, + Histogram: histogram, } } diff --git a/pkg/indicator/v2_multiply.go b/pkg/indicator/v2_multiply.go index 3599e7f6be..24f647005f 100644 --- a/pkg/indicator/v2_multiply.go +++ b/pkg/indicator/v2_multiply.go @@ -1,19 +1,42 @@ package indicator +import "github.com/c9s/bbgo/pkg/datatype/floats" + type MultiplyStream struct { Float64Series - multiplier float64 + a, b floats.Slice } -func Multiply(source Float64Source, multiplier float64) *MultiplyStream { +func Multiply(a, b Float64Source) *MultiplyStream { s := &MultiplyStream{ Float64Series: NewFloat64Series(), - multiplier: multiplier, } - s.Bind(source, s) + + a.OnUpdate(func(v float64) { + s.a.Push(v) + s.calculate() + }) + b.OnUpdate(func(v float64) { + s.b.Push(v) + s.calculate() + }) + return s } -func (s *MultiplyStream) Calculate(v float64) float64 { - return v * s.multiplier +func (s *MultiplyStream) calculate() { + if s.a.Length() != s.b.Length() { + return + } + + if s.a.Length() > s.slice.Length() { + var numNewElems = s.a.Length() - s.slice.Length() + var tailA = s.a.Tail(numNewElems) + var tailB = s.b.Tail(numNewElems) + var tailC = tailA.Mul(tailB) + for _, f := range tailC { + s.slice.Push(f) + s.EmitUpdate(f) + } + } } From 24003139f4e3b3cfb9c76d81bd0a6e3986b25264 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Jun 2023 14:09:02 +0800 Subject: [PATCH 0950/1392] types: fix return value var --- pkg/types/seriesbase_imp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/types/seriesbase_imp.go b/pkg/types/seriesbase_imp.go index fee51ddc49..037b45f217 100644 --- a/pkg/types/seriesbase_imp.go +++ b/pkg/types/seriesbase_imp.go @@ -85,11 +85,11 @@ func (s *SeriesBase) Dot(b interface{}, limit ...int) float64 { return Dot(s, b, limit...) } -func (s *SeriesBase) Array(limit ...int) (result []float64) { +func (s *SeriesBase) Array(limit ...int) []float64 { return Array(s, limit...) } -func (s *SeriesBase) Reverse(limit ...int) (result floats.Slice) { +func (s *SeriesBase) Reverse(limit ...int) floats.Slice { return Reverse(s, limit...) } From 7f3f2c1217b5214fbecbccb6b3c781cddfabc9d5 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Jun 2023 14:18:47 +0800 Subject: [PATCH 0951/1392] types: move cross result to a single file --- pkg/types/cross.go | 56 ++++++++++++++++++++++++++++++++++++++++++ pkg/types/indicator.go | 55 ----------------------------------------- 2 files changed, 56 insertions(+), 55 deletions(-) create mode 100644 pkg/types/cross.go diff --git a/pkg/types/cross.go b/pkg/types/cross.go new file mode 100644 index 0000000000..5504b035ba --- /dev/null +++ b/pkg/types/cross.go @@ -0,0 +1,56 @@ +package types + +// The result structure that maps to the crossing result of `CrossOver` and `CrossUnder` +// Accessible through BoolSeries interface +type CrossResult struct { + a Series + b Series + isOver bool +} + +func (c *CrossResult) Last() bool { + if c.Length() == 0 { + return false + } + if c.isOver { + return c.a.Last(0)-c.b.Last(0) > 0 && c.a.Last(1)-c.b.Last(1) < 0 + } else { + return c.a.Last(0)-c.b.Last(0) < 0 && c.a.Last(1)-c.b.Last(1) > 0 + } +} + +func (c *CrossResult) Index(i int) bool { + if i >= c.Length() { + return false + } + if c.isOver { + return c.a.Last(i)-c.b.Last(i) > 0 && c.a.Last(i+1)-c.b.Last(i+1) < 0 + } else { + return c.a.Last(i)-c.b.Last(i) < 0 && c.a.Last(i+1)-c.b.Last(i+1) > 0 + } +} + +func (c *CrossResult) Length() int { + la := c.a.Length() + lb := c.b.Length() + if la > lb { + return lb + } + return la +} + +// a series cross above b series. +// If in current KLine, a is higher than b, and in previous KLine, a is lower than b, then return true. +// Otherwise return false. +// If accessing index <= length, will always return false +func CrossOver(a Series, b Series) BoolSeries { + return &CrossResult{a, b, true} +} + +// a series cross under b series. +// If in current KLine, a is lower than b, and in previous KLine, a is higher than b, then return true. +// Otherwise return false. +// If accessing index <= length, will always return false +func CrossUnder(a Series, b Series) BoolSeries { + return &CrossResult{a, b, false} +} diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index 4dd396b055..593f6bec82 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -100,61 +100,6 @@ func NextCross(a Series, b Series, lookback int) (int, float64, bool) { return int(math.Ceil(-indexf)), alpha1 + beta1*indexf, true } -// The result structure that maps to the crossing result of `CrossOver` and `CrossUnder` -// Accessible through BoolSeries interface -type CrossResult struct { - a Series - b Series - isOver bool -} - -func (c *CrossResult) Last() bool { - if c.Length() == 0 { - return false - } - if c.isOver { - return c.a.Last(0)-c.b.Last(0) > 0 && c.a.Last(1)-c.b.Last(1) < 0 - } else { - return c.a.Last(0)-c.b.Last(0) < 0 && c.a.Last(1)-c.b.Last(1) > 0 - } -} - -func (c *CrossResult) Index(i int) bool { - if i >= c.Length() { - return false - } - if c.isOver { - return c.a.Last(i)-c.b.Last(i) > 0 && c.a.Last(i+1)-c.b.Last(i+1) < 0 - } else { - return c.a.Last(i)-c.b.Last(i) < 0 && c.a.Last(i+1)-c.b.Last(i+1) > 0 - } -} - -func (c *CrossResult) Length() int { - la := c.a.Length() - lb := c.b.Length() - if la > lb { - return lb - } - return la -} - -// a series cross above b series. -// If in current KLine, a is higher than b, and in previous KLine, a is lower than b, then return true. -// Otherwise return false. -// If accessing index <= length, will always return false -func CrossOver(a Series, b Series) BoolSeries { - return &CrossResult{a, b, true} -} - -// a series cross under b series. -// If in current KLine, a is lower than b, and in previous KLine, a is higher than b, then return true. -// Otherwise return false. -// If accessing index <= length, will always return false -func CrossUnder(a Series, b Series) BoolSeries { - return &CrossResult{a, b, false} -} - func Highest(a Series, lookback int) float64 { if lookback > a.Length() { lookback = a.Length() From ca78a3379a1518c9fddb0cb1fceb14c716e84848 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 4 Jun 2023 14:47:29 +0800 Subject: [PATCH 0952/1392] indicator: add cross stream --- pkg/indicator/float64series.go | 71 +++++++++++++++++++++++++++++++++ pkg/indicator/float64updater.go | 70 -------------------------------- pkg/indicator/v2_cross.go | 59 +++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 70 deletions(-) create mode 100644 pkg/indicator/float64series.go create mode 100644 pkg/indicator/v2_cross.go diff --git a/pkg/indicator/float64series.go b/pkg/indicator/float64series.go new file mode 100644 index 0000000000..821c176666 --- /dev/null +++ b/pkg/indicator/float64series.go @@ -0,0 +1,71 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +type Float64Series struct { + types.SeriesBase + Float64Updater + slice floats.Slice +} + +func NewFloat64Series(v ...float64) Float64Series { + s := Float64Series{} + s.slice = v + s.SeriesBase.Series = s.slice + return s +} + +func (f *Float64Series) Last(i int) float64 { + return f.slice.Last(i) +} + +func (f *Float64Series) Index(i int) float64 { + return f.Last(i) +} + +func (f *Float64Series) Length() int { + return len(f.slice) +} + +func (f *Float64Series) Slice() floats.Slice { + return f.slice +} + +func (f *Float64Series) PushAndEmit(x float64) { + f.slice.Push(x) + f.EmitUpdate(x) +} + +func (f *Float64Series) Subscribe(source Float64Source, c func(x float64)) { + if sub, ok := source.(Float64Subscription); ok { + sub.AddSubscriber(c) + } else { + source.OnUpdate(c) + } +} + +// Bind binds the source event to the target (Float64Calculator) +// A Float64Calculator should be able to calculate the float64 result from a single float64 argument input +func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) { + var c func(x float64) + + // optimize the truncation check + trc, canTruncate := target.(Float64Truncator) + if canTruncate { + c = func(x float64) { + y := target.Calculate(x) + target.PushAndEmit(y) + trc.Truncate() + } + } else { + c = func(x float64) { + y := target.Calculate(x) + target.PushAndEmit(y) + } + } + + f.Subscribe(source, c) +} diff --git a/pkg/indicator/float64updater.go b/pkg/indicator/float64updater.go index 7afdef8aac..a9743538eb 100644 --- a/pkg/indicator/float64updater.go +++ b/pkg/indicator/float64updater.go @@ -1,76 +1,6 @@ package indicator -import ( - "github.com/c9s/bbgo/pkg/datatype/floats" - "github.com/c9s/bbgo/pkg/types" -) - //go:generate callbackgen -type Float64Updater type Float64Updater struct { updateCallbacks []func(v float64) } - -type Float64Series struct { - types.SeriesBase - Float64Updater - slice floats.Slice -} - -func NewFloat64Series(v ...float64) Float64Series { - s := Float64Series{} - s.slice = v - s.SeriesBase.Series = s.slice - return s -} - -func (f *Float64Series) Last(i int) float64 { - return f.slice.Last(i) -} - -func (f *Float64Series) Index(i int) float64 { - return f.Last(i) -} - -func (f *Float64Series) Length() int { - return len(f.slice) -} - -func (f *Float64Series) Slice() floats.Slice { - return f.slice -} - -func (f *Float64Series) PushAndEmit(x float64) { - f.slice.Push(x) - f.EmitUpdate(x) -} - -func (f *Float64Series) Subscribe(source Float64Source, c func(x float64)) { - if sub, ok := source.(Float64Subscription); ok { - sub.AddSubscriber(c) - } else { - source.OnUpdate(c) - } -} - -// Bind binds the source event to the target (Float64Calculator) -// A Float64Calculator should be able to calculate the float64 result from a single float64 argument input -func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) { - var c func(x float64) - - // optimize the truncation check - trc, canTruncate := target.(Float64Truncator) - if canTruncate { - c = func(x float64) { - y := target.Calculate(x) - target.PushAndEmit(y) - trc.Truncate() - } - } else { - c = func(x float64) { - y := target.Calculate(x) - target.PushAndEmit(y) - } - } - - f.Subscribe(source, c) -} diff --git a/pkg/indicator/v2_cross.go b/pkg/indicator/v2_cross.go new file mode 100644 index 0000000000..084130fdb7 --- /dev/null +++ b/pkg/indicator/v2_cross.go @@ -0,0 +1,59 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" +) + +type CrossType float64 + +const ( + CrossOver CrossType = 1.0 + CrossUnder CrossType = -1.0 +) + +// CrossStream subscribes 2 upstreams, and calculate the cross signal +type CrossStream struct { + Float64Series + + a, b floats.Slice +} + +// Cross creates the CrossStream object: +// +// cross := Cross(fastEWMA, slowEWMA) +func Cross(a, b Float64Source) *CrossStream { + s := &CrossStream{ + Float64Series: NewFloat64Series(), + } + a.OnUpdate(func(v float64) { + s.a.Push(v) + s.calculate() + }) + b.OnUpdate(func(v float64) { + s.b.Push(v) + s.calculate() + }) + return s +} + +func (s *CrossStream) calculate() { + if s.a.Length() != s.b.Length() { + return + } + + current := s.a.Last(0) - s.b.Last(0) + previous := s.a.Last(1) - s.b.Last(1) + + if previous == 0.0 { + return + } + + // cross over or cross under + if current*previous < 0 { + if current > 0 { + s.PushAndEmit(float64(CrossOver)) + } else { + s.PushAndEmit(float64(CrossUnder)) + } + } +} From b90564be9084f0305dde1ee3a4c56b3013c1ae51 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Jun 2023 15:50:07 +0800 Subject: [PATCH 0953/1392] bbgo: fix order executor error message and add price check --- pkg/bbgo/order_executor_general.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 8faaaaa1f9..80bdba8a5d 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -268,7 +268,7 @@ func (e *GeneralOrderExecutor) reduceQuantityAndSubmitOrder(ctx context.Context, submitOrder.Quantity = q if e.position.Market.IsDustQuantity(submitOrder.Quantity, price) { - return nil, types.NewZeroAssetError(fmt.Errorf("dust quantity")) + return nil, types.NewZeroAssetError(fmt.Errorf("dust quantity, quantity = %f, price = %f", submitOrder.Quantity.Float64(), price.Float64())) } createdOrder, err2 := e.SubmitOrders(ctx, submitOrder) @@ -334,10 +334,15 @@ func (e *GeneralOrderExecutor) NewOrderFromOpenPosition(ctx context.Context, opt return nil, err } + if price.IsZero() { + return nil, errors.New("unable to calculate quantity: zero price given") + } + quantity = quoteQuantity.Div(price) } + if e.position.Market.IsDustQuantity(quantity, price) { - log.Warnf("dust quantity: %v", quantity) + log.Errorf("can not submit order: dust quantity, quantity = %f, price = %f", quantity.Float64(), price.Float64()) return nil, nil } @@ -389,9 +394,11 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos if err != nil { return nil, err } + if submitOrder == nil { return nil, nil } + price := options.Price side := "long" @@ -399,7 +406,7 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos side = "short" } - Notify("Opening %s %s position with quantity %v at price %v", e.position.Symbol, side, submitOrder.Quantity, price) + Notify("Opening %s %s position with quantity %f at price %f", e.position.Symbol, side, submitOrder.Quantity.Float64(), price.Float64()) createdOrder, err := e.SubmitOrders(ctx, *submitOrder) if err == nil { From 9f5ef21dda86cad40c79897bb5b1cdb03ea99aef Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Jun 2023 16:57:38 +0800 Subject: [PATCH 0954/1392] types: Add TradeWith helper --- pkg/types/kline.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 8df06d297a..a17e1e94ea 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -627,6 +627,16 @@ func (k *KLineSeries) Length() int { var _ Series = &KLineSeries{} +func TradeWith(symbol string, f func(trade Trade)) func(trade Trade) { + return func(trade Trade) { + if symbol != "" && trade.Symbol != symbol { + return + } + + f(trade) + } +} + func KLineWith(symbol string, interval Interval, callback KLineCallback) KLineCallback { return func(k KLine) { if k.Symbol != symbol || (k.Interval != "" && k.Interval != interval) { From aa281b164e05243978d3b16be260c3f47e7ec62a Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 6 Jun 2023 16:58:01 +0800 Subject: [PATCH 0955/1392] bbgo: improve tradingStop message --- pkg/bbgo/exit_trailing_stop.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/bbgo/exit_trailing_stop.go b/pkg/bbgo/exit_trailing_stop.go index 4de0ad66b0..672f2606df 100644 --- a/pkg/bbgo/exit_trailing_stop.go +++ b/pkg/bbgo/exit_trailing_stop.go @@ -59,15 +59,11 @@ func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrd })) if !IsBackTesting && enableMarketTradeStop { - session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { - if trade.Symbol != position.Symbol { - return - } - + session.MarketDataStream.OnMarketTrade(types.TradeWith(position.Symbol, func(trade types.Trade) { if err := s.checkStopPrice(trade.Price, position); err != nil { log.WithError(err).Errorf("error") } - }) + })) } } @@ -78,8 +74,10 @@ func (s *TrailingStop2) getRatio(price fixedpoint.Value, position *types.Positio // for short position, it's: // (avg_cost - price) / avg_cost return position.AverageCost.Sub(price).Div(position.AverageCost), nil + case types.SideTypeSell: return price.Sub(position.AverageCost).Div(position.AverageCost), nil + default: if position.IsLong() { return price.Sub(position.AverageCost).Div(position.AverageCost), nil @@ -174,12 +172,15 @@ func (s *TrailingStop2) triggerStop(price fixedpoint.Value) error { s.activated = false s.latestHigh = fixedpoint.Zero }() - Notify("[TrailingStop] %s %s stop loss triggered. price: %f callback rate: %f", s.Symbol, s, price.Float64(), s.CallbackRate.Float64()) + + Notify("[TrailingStop] %s %s tailingStop is triggered. price: %f callbackRate: %s", s.Symbol, s.ActivationRatio.Percentage(), price.Float64(), s.CallbackRate.Percentage()) ctx := context.Background() p := fixedpoint.One if !s.ClosePosition.IsZero() { p = s.ClosePosition } - return s.orderExecutor.ClosePosition(ctx, p, "trailingStop") + tagName := fmt.Sprintf("trailingStop:activation=%s,callback=%s", s.ActivationRatio.Percentage(), s.CallbackRate.Percentage()) + + return s.orderExecutor.ClosePosition(ctx, p, tagName) } From f6a300a7c46d2ca4e03de42f1b6fda7ee75b2277 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Jun 2023 16:27:36 +0800 Subject: [PATCH 0956/1392] schedule: add useLimitOrder option --- pkg/strategy/schedule/strategy.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/schedule/strategy.go b/pkg/strategy/schedule/strategy.go index 5137ae01a7..1d6ccffddd 100644 --- a/pkg/strategy/schedule/strategy.go +++ b/pkg/strategy/schedule/strategy.go @@ -34,6 +34,8 @@ type Strategy struct { // Side is the order side type, which can be buy or sell Side types.SideType `json:"side,omitempty"` + UseLimitOrder bool `json:"useLimitOrder"` + bbgo.QuantityOrAmount MaxBaseBalance fixedpoint.Value `json:"maxBaseBalance"` @@ -197,19 +199,29 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se quoteQuantity = quantity.Mul(closePrice) } + // truncate quantity by its step size + quantity = s.Market.TruncateQuantity(quantity) + if s.Market.IsDustQuantity(quantity, closePrice) { - log.Warnf("%s: quantity %f is too small", s.Symbol, quantity.Float64()) + log.Warnf("%s: quantity %f is too small, skip order", s.Symbol, quantity.Float64()) return } - bbgo.Notify("Submitting scheduled %s order with quantity %s at price %s", s.Symbol, quantity.String(), closePrice.String()) - _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + submitOrder := types.SubmitOrder{ Symbol: s.Symbol, Side: side, Type: types.OrderTypeMarket, Quantity: quantity, Market: s.Market, - }) + } + + if s.UseLimitOrder { + submitOrder.Type = types.OrderTypeLimit + submitOrder.Price = closePrice + } + + bbgo.Notify("Submitting scheduled %s order with quantity %s at price %s", s.Symbol, quantity.String(), closePrice.String()) + _, err := s.orderExecutor.SubmitOrders(ctx, submitOrder) if err != nil { bbgo.Notify("Can not place scheduled %s order: submit error %s", s.Symbol, err.Error()) log.WithError(err).Errorf("can not place scheduled %s order error", s.Symbol) From e0e27e75bb392b3d21be65f49d574567b51db556 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Jun 2023 16:30:54 +0800 Subject: [PATCH 0957/1392] schedule: graceful cancel orders before the next submission --- pkg/strategy/schedule/strategy.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/schedule/strategy.go b/pkg/strategy/schedule/strategy.go index 1d6ccffddd..aaa91d35ca 100644 --- a/pkg/strategy/schedule/strategy.go +++ b/pkg/strategy/schedule/strategy.go @@ -183,7 +183,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if quoteBalance.Available.Compare(quoteQuantity) < 0 { - bbgo.Notify("Can not place scheduled %s order: quote balance %s is not enough: %v < %v", s.Symbol, s.Market.QuoteCurrency, quoteBalance.Available, quoteQuantity) log.Errorf("can not place scheduled %s order: quote balance %s is not enough: %v < %v", s.Symbol, s.Market.QuoteCurrency, quoteBalance.Available, quoteQuantity) return } @@ -220,6 +219,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se submitOrder.Price = closePrice } + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cancel order error") + } + bbgo.Notify("Submitting scheduled %s order with quantity %s at price %s", s.Symbol, quantity.String(), closePrice.String()) _, err := s.orderExecutor.SubmitOrders(ctx, submitOrder) if err != nil { From 0f141c7f79521a750baaee914794f453c10805ba Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Jun 2023 16:36:38 +0800 Subject: [PATCH 0958/1392] schedule: add MinBaseBalance config --- pkg/strategy/schedule/strategy.go | 45 ++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/schedule/strategy.go b/pkg/strategy/schedule/strategy.go index aaa91d35ca..17d3b31bee 100644 --- a/pkg/strategy/schedule/strategy.go +++ b/pkg/strategy/schedule/strategy.go @@ -38,6 +38,7 @@ type Strategy struct { bbgo.QuantityOrAmount + MinBaseBalance fixedpoint.Value `json:"minBaseBalance"` MaxBaseBalance fixedpoint.Value `json:"maxBaseBalance"` BelowMovingAverage *bbgo.MovingAverageSettings `json:"belowMovingAverage,omitempty"` @@ -163,23 +164,35 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // calculate quote quantity for balance checking quoteQuantity := quantity.Mul(closePrice) + quoteBalance, ok := session.GetAccount().Balance(s.Market.QuoteCurrency) + if !ok { + log.Errorf("can not place scheduled %s order, quote balance %s is empty", s.Symbol, s.Market.QuoteCurrency) + return + } + + baseBalance, ok := session.GetAccount().Balance(s.Market.BaseCurrency) + if !ok { + log.Errorf("can not place scheduled %s order, base balance %s is empty", s.Symbol, s.Market.BaseCurrency) + return + } + + totalBase := baseBalance.Total() + // execute orders switch side { case types.SideTypeBuy: + if !s.MaxBaseBalance.IsZero() { - if baseBalance, ok := session.GetAccount().Balance(s.Market.BaseCurrency); ok { - total := baseBalance.Total() - if total.Add(quantity).Compare(s.MaxBaseBalance) >= 0 { - quantity = s.MaxBaseBalance.Sub(total) - quoteQuantity = quantity.Mul(closePrice) - } + if totalBase.Add(quantity).Compare(s.MaxBaseBalance) >= 0 { + quantity = s.MaxBaseBalance.Sub(totalBase) + quoteQuantity = quantity.Mul(closePrice) } } - quoteBalance, ok := session.GetAccount().Balance(s.Market.QuoteCurrency) - if !ok { - log.Errorf("can not place scheduled %s order, quote balance %s is empty", s.Symbol, s.Market.QuoteCurrency) - return + // if min base balance is defined + if !s.MinBaseBalance.IsZero() && s.MinBaseBalance.Compare(totalBase) > 0 { + quantity = fixedpoint.Max(quantity, s.MinBaseBalance.Sub(totalBase)) + quantity = fixedpoint.Max(quantity, s.Market.MinQuantity) } if quoteBalance.Available.Compare(quoteQuantity) < 0 { @@ -188,13 +201,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } case types.SideTypeSell: - baseBalance, ok := session.GetAccount().Balance(s.Market.BaseCurrency) - if !ok { - log.Errorf("can not place scheduled %s order, base balance %s is empty", s.Symbol, s.Market.BaseCurrency) - return + quantity = fixedpoint.Min(quantity, baseBalance.Available) + + // skip sell if we hit the minBaseBalance line + if !s.MinBaseBalance.IsZero() { + if totalBase.Sub(quantity).Compare(s.MinBaseBalance) < 0 { + return + } } - quantity = fixedpoint.Min(quantity, baseBalance.Available) quoteQuantity = quantity.Mul(closePrice) } From bd335a0335fecb0686592d1ebb6ced8cf427eb64 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Jun 2023 16:39:37 +0800 Subject: [PATCH 0959/1392] bbgo: fix trailing stop order tag --- pkg/bbgo/exit_trailing_stop_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/exit_trailing_stop_test.go b/pkg/bbgo/exit_trailing_stop_test.go index 42bd37e88d..b8ddeac3f3 100644 --- a/pkg/bbgo/exit_trailing_stop_test.go +++ b/pkg/bbgo/exit_trailing_stop_test.go @@ -41,7 +41,7 @@ func TestTrailingStop_ShortPosition(t *testing.T) { Type: types.OrderTypeMarket, Market: market, Quantity: fixedpoint.NewFromFloat(1.0), - Tag: "trailingStop", + Tag: "trailingStop:activation=1%,callback=1%", MarginSideEffect: types.SideEffectTypeAutoRepay, }) @@ -119,7 +119,7 @@ func TestTrailingStop_LongPosition(t *testing.T) { Type: types.OrderTypeMarket, Market: market, Quantity: fixedpoint.NewFromFloat(1.0), - Tag: "trailingStop", + Tag: "trailingStop:activation=1%,callback=1%", MarginSideEffect: types.SideEffectTypeAutoRepay, }) From c25ac65eb009005eb01c4c8b5193f3859f062894 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 7 Jun 2023 16:45:46 +0800 Subject: [PATCH 0960/1392] bbgo: improve hhllstop message --- pkg/bbgo/exit_hh_ll_stop.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index af8ec89bc0..f737bae561 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -3,6 +3,7 @@ package bbgo import ( "context" "fmt" + log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -82,14 +83,14 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close r := fixedpoint.One.Add(s.ActivationRatio) if closePrice.Compare(position.AverageCost.Mul(r)) >= 0 { s.activated = true - Notify("[hhllStop] Stop of %s activated for long position, activation ratio %s:", s.Symbol, s.ActivationRatio.Percentage()) + Notify("[hhllStop] %s stop is activated for long position, activation ratio %s:", s.Symbol, s.ActivationRatio.Percentage()) } } else if position.IsShort() { r := fixedpoint.One.Sub(s.ActivationRatio) // for short position, if the close price is less than the activation price then this is a profit position. if closePrice.Compare(position.AverageCost.Mul(r)) <= 0 { s.activated = true - Notify("[hhllStop] Stop of %s activated for short position, activation ratio %s:", s.Symbol, s.ActivationRatio.Percentage()) + Notify("[hhllStop] %s stop is activated for short position, activation ratio %s:", s.Symbol, s.ActivationRatio.Percentage()) } } } @@ -99,15 +100,18 @@ func (s *HigherHighLowerLowStop) updateHighLowNumber(kline types.KLine) { s.klines.Truncate(s.Window - 1) if s.klines.Len() >= s.Window-1 { - if s.klines.GetHigh().Compare(kline.GetHigh()) < 0 { + high := kline.GetHigh() + low := kline.GetLow() + if s.klines.GetHigh().Compare(high) < 0 { s.highLows = append(s.highLows, types.DirectionUp) - log.Debugf("[hhllStop] new higher high for %s", s.Symbol) - } else if s.klines.GetLow().Compare(kline.GetLow()) > 0 { + Notify("[hhllStop] detected %s new higher high %f", s.Symbol, high.Float64()) + } else if s.klines.GetLow().Compare(low) > 0 { s.highLows = append(s.highLows, types.DirectionDown) - log.Debugf("[hhllStop] new lower low for %s", s.Symbol) + Notify("[hhllStop] detected %s new lower low %f", s.Symbol, low.Float64()) } else { s.highLows = append(s.highLows, types.DirectionNone) } + // Truncate highLows if len(s.highLows) > s.HighLowWindow { end := len(s.highLows) @@ -118,6 +122,7 @@ func (s *HigherHighLowerLowStop) updateHighLowNumber(kline types.KLine) { kn := s.highLows[start:] s.highLows = kn } + } else { s.highLows = append(s.highLows, types.DirectionNone) } @@ -183,13 +188,17 @@ func (s *HigherHighLowerLowStop) Bind(session *ExchangeSession, orderExecutor *G // Close position & reset if s.shouldStop(position) { + defer func() { + s.activated = false + }() + err := s.orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "hhllStop") if err != nil { Notify("[hhllStop] Stop of %s triggered but failed to close %s position:", s.Symbol, err) - } else { - s.activated = false - Notify("[hhllStop] Stop of %s triggered and position closed", s.Symbol) + return } + + Notify("[hhllStop] Stop of %s triggered and position closed", s.Symbol) } })) From c9ee4e52ccd1d88f649e71b40d7e7212d5ab4ec2 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 8 Jun 2023 15:54:32 +0800 Subject: [PATCH 0961/1392] xalign: add xalign strategy --- config/xalign.yaml | 41 +++++ pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/xalign/strategy.go | 268 ++++++++++++++++++++++++++++++++ pkg/strategy/xnav/strategy.go | 5 +- pkg/types/balance.go | 6 +- 5 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 config/xalign.yaml create mode 100644 pkg/strategy/xalign/strategy.go diff --git a/config/xalign.yaml b/config/xalign.yaml new file mode 100644 index 0000000000..2be28cf825 --- /dev/null +++ b/config/xalign.yaml @@ -0,0 +1,41 @@ +--- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + + switches: + trade: true + orderUpdate: true + submitOrder: true + +sessions: + max: + exchange: max + envVarPrefix: max + + binance: + exchange: binance + envVarPrefix: binance + +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +crossExchangeStrategies: + +- xalign: + interval: 1m + sessions: + - max + - binance + quoteCurrencies: + buy: [USDC, TWD] + sell: [USDT] + expectedBalances: + BTC: 0.0440 + diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 50e1ec8bbd..0a76cfb4e9 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -36,6 +36,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/techsignal" _ "github.com/c9s/bbgo/pkg/strategy/trendtrader" _ "github.com/c9s/bbgo/pkg/strategy/wall" + _ "github.com/c9s/bbgo/pkg/strategy/xalign" _ "github.com/c9s/bbgo/pkg/strategy/xbalance" _ "github.com/c9s/bbgo/pkg/strategy/xfunding" _ "github.com/c9s/bbgo/pkg/strategy/xgap" diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go new file mode 100644 index 0000000000..5c25d1db2e --- /dev/null +++ b/pkg/strategy/xalign/strategy.go @@ -0,0 +1,268 @@ +package xalign + +import ( + "context" + "errors" + "fmt" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "xalign" + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type QuoteCurrencyPreference struct { + Buy []string `json:"buy"` + Sell []string `json:"sell"` +} + +type Strategy struct { + *bbgo.Environment + Interval types.Interval `json:"interval"` + PreferredSessions []string `json:"sessions"` + PreferredQuoteCurrencies *QuoteCurrencyPreference `json:"quoteCurrencies"` + ExpectedBalances map[string]fixedpoint.Value `json:"expectedBalances"` + UseTakerOrder bool `json:"useTakerOrder"` + + orderBook map[string]*bbgo.ActiveOrderBook +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s", ID) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { + +} + +func (s *Strategy) Validate() error { + if s.PreferredQuoteCurrencies == nil { + return errors.New("quoteCurrencies is not defined") + } + + return nil +} + +func (s *Strategy) aggregateBalances(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) (totalBalances types.BalanceMap, sessionBalances map[string]types.BalanceMap) { + totalBalances = make(types.BalanceMap) + sessionBalances = make(map[string]types.BalanceMap) + + // iterate the sessions and record them + for sessionName, session := range sessions { + // update the account balances and the margin information + if _, err := session.UpdateAccount(ctx); err != nil { + log.WithError(err).Errorf("can not update account") + return + } + + account := session.GetAccount() + balances := account.Balances() + + sessionBalances[sessionName] = balances + totalBalances = totalBalances.Add(balances) + } + + return totalBalances, sessionBalances +} + +func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[string]*bbgo.ExchangeSession, currency string, changeQuantity fixedpoint.Value) (*bbgo.ExchangeSession, *types.SubmitOrder) { + for _, sessionName := range s.PreferredSessions { + session := sessions[sessionName] + + var taker bool = s.UseTakerOrder + var side types.SideType + var quoteCurrencies []string + if changeQuantity.Sign() > 0 { + quoteCurrencies = s.PreferredQuoteCurrencies.Buy + side = types.SideTypeBuy + } else { + quoteCurrencies = s.PreferredQuoteCurrencies.Sell + side = types.SideTypeSell + } + + for _, quoteCurrency := range quoteCurrencies { + symbol := currency + quoteCurrency + market, ok := session.Market(symbol) + if !ok { + continue + } + + ticker, err := session.Exchange.QueryTicker(ctx, symbol) + if err != nil { + log.WithError(err).Errorf("unable to query ticker on %s", symbol) + continue + } + + // changeQuantity > 0 = buy + // changeQuantity < 0 = sell + q := changeQuantity.Abs() + + switch side { + + case types.SideTypeBuy: + quoteBalance, ok := session.Account.Balance(quoteCurrency) + if !ok { + continue + } + + price := ticker.Sell + if taker { + price = ticker.Sell + } else if ticker.Buy.Add(market.TickSize).Compare(ticker.Sell) < 0 { + price = ticker.Buy.Add(market.TickSize) + } else { + price = ticker.Buy + } + + requiredQuoteAmount := q.Div(price) + if requiredQuoteAmount.Compare(quoteBalance.Available) < 0 { + log.Warnf("required quote amount %f < quote balance %v", requiredQuoteAmount.Float64(), quoteBalance) + continue + } + + q = market.AdjustQuantityByMinNotional(q, price) + + return session, &types.SubmitOrder{ + Symbol: symbol, + Side: side, + Type: types.OrderTypeLimit, + Quantity: q, + Price: price, + Market: market, + TimeInForce: "GTC", + } + + case types.SideTypeSell: + baseBalance, ok := session.Account.Balance(currency) + if !ok { + continue + } + + if q.Compare(baseBalance.Available) > 0 { + log.Warnf("required base amount %f < available base balance %v", q.Float64(), baseBalance) + continue + } + + price := ticker.Buy + if taker { + price = ticker.Buy + } else if ticker.Sell.Add(market.TickSize.Neg()).Compare(ticker.Buy) < 0 { + price = ticker.Sell.Add(market.TickSize.Neg()) + } else { + price = ticker.Sell + } + + if market.IsDustQuantity(q, price) { + log.Infof("%s dust quantity: %f", currency, q.Float64()) + return nil, nil + } + + return session, &types.SubmitOrder{ + Symbol: symbol, + Side: side, + Type: types.OrderTypeLimit, + Quantity: q, + Price: price, + Market: market, + TimeInForce: "GTC", + } + } + + } + } + + return nil, nil +} + +func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + _ = instanceID + + s.orderBook = make(map[string]*bbgo.ActiveOrderBook) + + for _, sessionName := range s.PreferredSessions { + session, ok := sessions[sessionName] + if !ok { + return fmt.Errorf("incorrect preferred session name: %s is not defined", sessionName) + } + + orderBook := bbgo.NewActiveOrderBook("") + orderBook.BindStream(session.UserDataStream) + s.orderBook[sessionName] = orderBook + } + + go func() { + s.align(ctx, sessions) + + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + select { + + case <-ctx.Done(): + return + + case <-ticker.C: + s.align(ctx, sessions) + } + } + }() + + return nil +} + +func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) { + totalBalances, sessionBalances := s.aggregateBalances(ctx, sessions) + _ = sessionBalances + + for sessionName, session := range sessions { + if err := s.orderBook[sessionName].GracefulCancel(ctx, session.Exchange); err != nil { + log.WithError(err).Errorf("can not cancel order") + } + } + + for currency, expectedBalance := range s.ExpectedBalances { + q := s.calculateRefillQuantity(totalBalances, currency, expectedBalance) + + selectedSession, submitOrder := s.selectSessionForCurrency(ctx, sessions, currency, q) + if selectedSession != nil && submitOrder != nil { + + log.Infof("placing order on %s: %#v", selectedSession.Name, submitOrder) + + createdOrder, err := selectedSession.Exchange.SubmitOrder(ctx, *submitOrder) + if err != nil { + log.WithError(err).Errorf("can not place order") + return + } + + if createdOrder != nil { + s.orderBook[selectedSession.Name].Add(*createdOrder) + } + } + } +} + +func (s *Strategy) calculateRefillQuantity(totalBalances types.BalanceMap, currency string, expectedBalance fixedpoint.Value) fixedpoint.Value { + if b, ok := totalBalances[currency]; ok { + netBalance := b.Net() + return expectedBalance.Sub(netBalance) + } + return expectedBalance +} diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index b0514d9467..b4b33cab70 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -19,8 +19,6 @@ import ( const ID = "xnav" -const stateKey = "state-v1" - var log = logrus.WithField("strategy", ID) func init() { @@ -82,6 +80,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] priceTime := time.Now() // iterate the sessions and record them + quoteCurrency := "USDT" for sessionName, session := range sessions { // update the account balances and the margin information if _, err := session.UpdateAccount(ctx); err != nil { @@ -91,7 +90,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] account := session.GetAccount() balances := account.Balances() - if err := session.UpdatePrices(ctx, balances.Currencies(), "USDT"); err != nil { + if err := session.UpdatePrices(ctx, balances.Currencies(), quoteCurrency); err != nil { log.WithError(err).Error("price update failed") return } diff --git a/pkg/types/balance.go b/pkg/types/balance.go index e0c0255f73..dc83f65b0d 100644 --- a/pkg/types/balance.go +++ b/pkg/types/balance.go @@ -71,15 +71,15 @@ func (b Balance) String() (o string) { o = fmt.Sprintf("%s: %s", b.Currency, b.Net().String()) if b.Locked.Sign() > 0 { - o += fmt.Sprintf(" (locked %v)", b.Locked) + o += fmt.Sprintf(" (locked %f)", b.Locked.Float64()) } if b.Borrowed.Sign() > 0 { - o += fmt.Sprintf(" (borrowed: %v)", b.Borrowed) + o += fmt.Sprintf(" (borrowed: %f)", b.Borrowed.Float64()) } if b.Interest.Sign() > 0 { - o += fmt.Sprintf(" (interest: %v)", b.Interest) + o += fmt.Sprintf(" (interest: %f)", b.Interest.Float64()) } return o From db43c872276a04b9cf107febc5c1a8a5ab2f2045 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 8 Jun 2023 16:00:43 +0800 Subject: [PATCH 0962/1392] xalign: load interval from config --- pkg/strategy/xalign/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 5c25d1db2e..11511dd980 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -210,7 +210,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se go func() { s.align(ctx, sessions) - ticker := time.NewTicker(time.Minute) + ticker := time.NewTicker(s.Interval.Duration()) defer ticker.Stop() for { From cbb9cc7722cc426cfe54b0734052d8bdc855c436 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 8 Jun 2023 16:04:43 +0800 Subject: [PATCH 0963/1392] xalign: add doc comment --- config/xalign.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/xalign.yaml b/config/xalign.yaml index 2be28cf825..ce68ccb20c 100644 --- a/config/xalign.yaml +++ b/config/xalign.yaml @@ -33,6 +33,9 @@ crossExchangeStrategies: sessions: - max - binance + + ## quoteCurrencies config specifies which quote currency should be used for BUY order or SELL order. + ## when specifying [USDC,TWD] for "BUY", then it will consider BTCUSDT first then BTCTWD second. quoteCurrencies: buy: [USDC, TWD] sell: [USDT] From 7a6000a3163fb25cc0e4f2054b0bae62ab810042 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 8 Jun 2023 18:05:58 +0800 Subject: [PATCH 0964/1392] xalign: fix instanceID --- pkg/strategy/xalign/strategy.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 11511dd980..5c4457203a 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "time" log "github.com/sirupsen/logrus" @@ -40,7 +41,13 @@ func (s *Strategy) ID() string { } func (s *Strategy) InstanceID() string { - return fmt.Sprintf("%s", ID) + var cs []string + + for cur := range s.ExpectedBalances { + cs = append(cs, cur) + } + + return ID + strings.Join(s.PreferredSessions, "-") + strings.Join(cs, "-") } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { From 8baafdf329b619b60445ee96b4da537863056622 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 8 Jun 2023 23:15:26 +0800 Subject: [PATCH 0965/1392] xalign: add DryRun and fix quote amount calculation --- pkg/strategy/xalign/strategy.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 5c4457203a..a0721afcca 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -32,6 +32,7 @@ type Strategy struct { PreferredQuoteCurrencies *QuoteCurrencyPreference `json:"quoteCurrencies"` ExpectedBalances map[string]fixedpoint.Value `json:"expectedBalances"` UseTakerOrder bool `json:"useTakerOrder"` + DryRun bool `json:"dryRun"` orderBook map[string]*bbgo.ActiveOrderBook } @@ -92,7 +93,7 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st for _, sessionName := range s.PreferredSessions { session := sessions[sessionName] - var taker bool = s.UseTakerOrder + var taker = s.UseTakerOrder var side types.SideType var quoteCurrencies []string if changeQuantity.Sign() > 0 { @@ -137,7 +138,7 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st price = ticker.Buy } - requiredQuoteAmount := q.Div(price) + requiredQuoteAmount := q.Mul(price) if requiredQuoteAmount.Compare(quoteBalance.Available) < 0 { log.Warnf("required quote amount %f < quote balance %v", requiredQuoteAmount.Float64(), quoteBalance) continue @@ -250,9 +251,12 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange selectedSession, submitOrder := s.selectSessionForCurrency(ctx, sessions, currency, q) if selectedSession != nil && submitOrder != nil { - log.Infof("placing order on %s: %#v", selectedSession.Name, submitOrder) + if s.DryRun { + return + } + createdOrder, err := selectedSession.Exchange.SubmitOrder(ctx, *submitOrder) if err != nil { log.WithError(err).Errorf("can not place order") From f6f329319179bdc0beba3997c492c72eda5844c3 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Jun 2023 11:04:31 +0800 Subject: [PATCH 0966/1392] xalign: round up requiredQuoteAmount --- pkg/strategy/xalign/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index a0721afcca..3de3c32e20 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -139,6 +139,7 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st } requiredQuoteAmount := q.Mul(price) + requiredQuoteAmount = requiredQuoteAmount.Round(market.PricePrecision, fixedpoint.Up) if requiredQuoteAmount.Compare(quoteBalance.Available) < 0 { log.Warnf("required quote amount %f < quote balance %v", requiredQuoteAmount.Float64(), quoteBalance) continue From 93d35cc423c140f95fcf52306a115bae616a2760 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 27 Apr 2023 21:06:29 +0800 Subject: [PATCH 0967/1392] FEATURE: use TwinOrder to recover --- pkg/strategy/grid2/recover.go | 193 ++++++++++++++++++++++++++++--- pkg/strategy/grid2/twin_order.go | 113 ++++++++++++++++++ 2 files changed, 290 insertions(+), 16 deletions(-) create mode 100644 pkg/strategy/grid2/twin_order.go diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index fec69a2ef8..67f7111d40 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -116,32 +116,23 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") } - // 1. build pin-order map - pinOrdersOpen, err := s.buildPinOrderMap(grid.Pins, openOrdersOnGrid) + // 1. build twin-order map + twinOrdersOpen, err := s.buildTwinOrderMap(grid.Pins, openOrdersOnGrid) if err != nil { return errors.Wrapf(err, "failed to build pin order map with open orders") } - // 2. build the filled pin-order map by querying trades - pinOrdersFilled, err := s.buildFilledPinOrderMapFromTrades(ctx, historyService, pinOrdersOpen) + // 2. build the filled twin-order map by querying trades + twinOrdersFilled, err := s.buildFilledTwinOrderMapFromTrades(ctx, historyService, twinOrdersOpen) if err != nil { return errors.Wrapf(err, "failed to build filled pin order map") } - // 3. get the filled orders from pin-order map - filledOrders := pinOrdersFilled.AscendingOrders() - numFilledOrders := len(filledOrders) - if numFilledOrders == int(expectedNumOfOrders-numGridOpenOrders) { - // nums of filled order is the same as Size - 1 - num(open orders) - s.logger.Infof("nums of filled order is the same as Size - 1 - len(open orders) : %d = %d - 1 - %d", numFilledOrders, s.grid.Size, numGridOpenOrders) - } else if numFilledOrders == int(expectedNumOfOrders-numGridOpenOrders+1) { - filledOrders = filledOrders[1:] - } else { - return fmt.Errorf("not reasonable num of filled orders") - } + // 3. get the filled orders from twin-order map + filledOrders := twinOrdersFilled.AscendingOrders() // 4. verify the grid - if err := s.verifyFilledGrid(s.grid.Pins, pinOrdersOpen, filledOrders); err != nil { + if err := s.verifyFilledTwinGrid(s.grid.Pins, twinOrdersOpen, filledOrders); err != nil { return errors.Wrapf(err, "verify grid with error") } @@ -319,3 +310,173 @@ func addOrdersIntoPinOrderMap(pinOrders PinOrderMap, orders []types.Order) error return nil } + +func (s *Strategy) verifyFilledTwinGrid(pins []Pin, twinOrders TwinOrderMap, filledOrders []types.Order) error { + s.debugLog("verifying filled grid - pins: %+v", pins) + s.debugLog("verifying filled grid - open twin orders:\n%s", twinOrders.String()) + s.debugOrders("verifying filled grid - filled orders", filledOrders) + + if err := s.addOrdersIntoTwinOrderMap(twinOrders, filledOrders); err != nil { + return errors.Wrapf(err, "verifying filled grid error when add orders into twin order map") + } + + s.debugLog("verifying filled grid - filled twin orders:\n%+v", twinOrders.String()) + + for i, pin := range pins { + if i == 0 { + continue + } + + twin, exist := twinOrders[fixedpoint.Value(pin)] + if !exist { + return fmt.Errorf("there is no order at price (%+v)", pin) + } + + if !twin.Exist() { + return fmt.Errorf("all the price need a twin") + } + } + + return nil +} + +// buildTwinOrderMap build the pin-order map with grid and open orders. +// The keys of this map contains all required pins of this grid. +// If the Order of the pin is empty types.Order (OrderID == 0), it means there is no open orders at this pin. +func (s *Strategy) buildTwinOrderMap(pins []Pin, openOrders []types.Order) (TwinOrderMap, error) { + twinOrderMap := make(TwinOrderMap) + + for i, pin := range pins { + // twin order map only use sell price as key, so skip 0 + if i == 0 { + continue + } + + twinOrderMap[fixedpoint.Value(pin)] = TwinOrder{} + } + + for _, openOrder := range openOrders { + twinKey, err := findTwinOrderMapKey(s.grid, openOrder) + if err != nil { + return nil, errors.Wrapf(err, "failed to build twin order map") + } + + twinOrder, exist := twinOrderMap[twinKey] + if !exist { + return nil, fmt.Errorf("the price of the openOrder (id: %d) is not in pins", openOrder.OrderID) + } + + if twinOrder.Exist() { + return nil, fmt.Errorf("there are multiple order in a twin") + } + + twinOrder.SetOrder(openOrder) + twinOrderMap[twinKey] = twinOrder + } + + return twinOrderMap, nil +} + +// buildFilledTwinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map +// It will skip the orders on pins at which open orders are already +func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, twinOrdersOpen TwinOrderMap) (TwinOrderMap, error) { + twinOrdersFilled := make(TwinOrderMap) + + // existedOrders is used to avoid re-query the same orders + existedOrders := twinOrdersOpen.SyncOrderMap() + + var limit int64 = 1000 + // get the filled orders when bbgo is down in order from trades + // [NOTE] only retrieve from last 24 hours !!! + var fromTradeID uint64 = 0 + for { + trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ + LastTradeID: fromTradeID, + Limit: limit, + }) + + if err != nil { + return nil, errors.Wrapf(err, "failed to query trades to recover the grid with open orders") + } + + s.debugLog("QueryTrades return %d trades", len(trades)) + + for _, trade := range trades { + s.debugLog(trade.String()) + if existedOrders.Exists(trade.OrderID) { + // already queries, skip + continue + } + + order, err := s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ + OrderID: strconv.FormatUint(trade.OrderID, 10), + }) + + if err != nil { + return nil, errors.Wrapf(err, "failed to query order by trade") + } + + s.debugLog("%s (group_id: %d)", order.String(), order.GroupID) + + // avoid query this order again + existedOrders.Add(*order) + + // add 1 to avoid duplicate + fromTradeID = trade.ID + 1 + + twinOrderKey, err := findTwinOrderMapKey(s.grid, *order) + if err != nil { + return nil, errors.Wrapf(err, "failed to find grid order map's key when recover") + } + + twinOrderOpen, exist := twinOrdersOpen[twinOrderKey] + if !exist { + return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") + } + + if twinOrderOpen.Exist() { + continue + } + + if twinOrder, exist := twinOrdersFilled[twinOrderKey]; exist { + to := twinOrder.GetOrder() + if to.UpdateTime.Time().After(order.UpdateTime.Time()) { + s.logger.Infof("twinOrder's update time (%s) should not be after order's update time (%s)", to.UpdateTime, order.UpdateTime) + continue + } + } + + twinOrder := TwinOrder{} + twinOrder.SetOrder(*order) + twinOrdersFilled[twinOrderKey] = twinOrder + } + + // stop condition + if int64(len(trades)) < limit { + break + } + } + + return twinOrdersFilled, nil +} + +func (s *Strategy) addOrdersIntoTwinOrderMap(twinOrders TwinOrderMap, orders []types.Order) error { + for _, order := range orders { + k, err := findTwinOrderMapKey(s.grid, order) + if err != nil { + return errors.Wrap(err, "failed to add orders into twin order map") + } + + if v, exist := twinOrders[k]; !exist { + return fmt.Errorf("the price (%+v) is not in pins", k) + } else if v.Exist() { + return fmt.Errorf("there is already a twin order at this price (%+v)", k) + } else { + twin := TwinOrder{} + twin.SetOrder(order) + twinOrders[k] = twin + } + } + + return nil +} diff --git a/pkg/strategy/grid2/twin_order.go b/pkg/strategy/grid2/twin_order.go new file mode 100644 index 0000000000..12ffbaf95c --- /dev/null +++ b/pkg/strategy/grid2/twin_order.go @@ -0,0 +1,113 @@ +package grid2 + +import ( + "fmt" + "sort" + "strings" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// For grid trading, there are twin orders between a grid +// e.g. 100, 200, 300, 400, 500 +// BUY 100 and SELL 200 are a twin. +// BUY 200 and SELL 300 are a twin. +// Because they can't be placed on orderbook at the same time + +type TwinOrder struct { + BuyOrder types.Order + SellOrder types.Order +} + +func (t *TwinOrder) IsValid() bool { + // XOR + return (t.BuyOrder.OrderID == 0) != (t.SellOrder.OrderID == 0) +} + +func (t *TwinOrder) Exist() bool { + return t.BuyOrder.OrderID != 0 || t.SellOrder.OrderID != 0 +} + +func (t *TwinOrder) GetOrder() types.Order { + if t.BuyOrder.OrderID != 0 { + return t.BuyOrder + } + + return t.SellOrder +} + +func (t *TwinOrder) SetOrder(order types.Order) { + if order.Side == types.SideTypeBuy { + t.BuyOrder = order + t.SellOrder = types.Order{} + } else { + t.SellOrder = order + t.BuyOrder = types.Order{} + } +} + +type TwinOrderMap map[fixedpoint.Value]TwinOrder + +func findTwinOrderMapKey(grid *Grid, order types.Order) (fixedpoint.Value, error) { + if order.Side == types.SideTypeSell { + return order.Price, nil + } + + if order.Side == types.SideTypeBuy { + pin, ok := grid.NextHigherPin(order.Price) + if !ok { + return fixedpoint.Zero, fmt.Errorf("there is no next higher price for this order (%d, price: %s)", order.OrderID, order.Price) + } + + return fixedpoint.Value(pin), nil + } + + return fixedpoint.Zero, fmt.Errorf("unsupported side: %s of this order (%d)", order.Side, order.OrderID) +} + +func (m TwinOrderMap) AscendingOrders() []types.Order { + var orders []types.Order + for _, order := range m { + // skip empty order + if !order.Exist() { + continue + } + + orders = append(orders, order.GetOrder()) + } + + types.SortOrdersUpdateTimeAscending(orders) + + return orders +} + +func (m TwinOrderMap) SyncOrderMap() *types.SyncOrderMap { + orderMap := types.NewSyncOrderMap() + for _, twin := range m { + orderMap.Add(twin.GetOrder()) + } + + return orderMap +} + +func (m TwinOrderMap) String() string { + var sb strings.Builder + var pins []fixedpoint.Value + for pin, _ := range m { + pins = append(pins, pin) + } + + sort.Slice(pins, func(i, j int) bool { + return pins[j] < pins[i] + }) + + sb.WriteString("================== TWIN ORDER MAP ==================\n") + for _, pin := range pins { + twin := m[pin] + twinOrder := twin.GetOrder() + sb.WriteString(fmt.Sprintf("-> %8s) %s\n", pin, twinOrder.String())) + } + sb.WriteString("================== END OF PIN ORDER MAP ==================\n") + return sb.String() +} From 61892eb2df10965da2de1241e8d239206901162e Mon Sep 17 00:00:00 2001 From: chiahung Date: Fri, 28 Apr 2023 16:02:22 +0800 Subject: [PATCH 0968/1392] renaming --- pkg/strategy/grid2/recover.go | 11 ++++++++++- pkg/strategy/grid2/twin_order.go | 6 +++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 67f7111d40..0a69245cd3 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -313,8 +313,8 @@ func addOrdersIntoPinOrderMap(pinOrders PinOrderMap, orders []types.Order) error func (s *Strategy) verifyFilledTwinGrid(pins []Pin, twinOrders TwinOrderMap, filledOrders []types.Order) error { s.debugLog("verifying filled grid - pins: %+v", pins) - s.debugLog("verifying filled grid - open twin orders:\n%s", twinOrders.String()) s.debugOrders("verifying filled grid - filled orders", filledOrders) + s.debugLog("verifying filled grid - open twin orders:\n%s", twinOrders.String()) if err := s.addOrdersIntoTwinOrderMap(twinOrders, filledOrders); err != nil { return errors.Wrapf(err, "verifying filled grid error when add orders into twin order map") @@ -323,6 +323,7 @@ func (s *Strategy) verifyFilledTwinGrid(pins []Pin, twinOrders TwinOrderMap, fil s.debugLog("verifying filled grid - filled twin orders:\n%+v", twinOrders.String()) for i, pin := range pins { + // we use twinOrderMap to make sure there are no duplicated order at one grid, and we use the sell price as key so we skip the pins[0] which is only for buy price if i == 0 { continue } @@ -335,6 +336,10 @@ func (s *Strategy) verifyFilledTwinGrid(pins []Pin, twinOrders TwinOrderMap, fil if !twin.Exist() { return fmt.Errorf("all the price need a twin") } + + if !twin.IsValid() { + return fmt.Errorf("all the twins need to be valid") + } } return nil @@ -421,6 +426,10 @@ func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, histor // avoid query this order again existedOrders.Add(*order) + if order.GroupID != s.OrderGroupID { + continue + } + // add 1 to avoid duplicate fromTradeID = trade.ID + 1 diff --git a/pkg/strategy/grid2/twin_order.go b/pkg/strategy/grid2/twin_order.go index 12ffbaf95c..e05e2b4e0e 100644 --- a/pkg/strategy/grid2/twin_order.go +++ b/pkg/strategy/grid2/twin_order.go @@ -68,13 +68,13 @@ func findTwinOrderMapKey(grid *Grid, order types.Order) (fixedpoint.Value, error func (m TwinOrderMap) AscendingOrders() []types.Order { var orders []types.Order - for _, order := range m { + for _, twinOrder := range m { // skip empty order - if !order.Exist() { + if !twinOrder.Exist() { continue } - orders = append(orders, order.GetOrder()) + orders = append(orders, twinOrder.GetOrder()) } types.SortOrdersUpdateTimeAscending(orders) From f38cfb6ea36284510abf2e1b16f1de1a12bd8189 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 18 May 2023 17:47:26 +0800 Subject: [PATCH 0969/1392] REFACTOR: refactor for future test --- pkg/strategy/grid2/recover.go | 220 ++++------------------------ pkg/strategy/grid2/strategy_test.go | 3 +- 2 files changed, 33 insertions(+), 190 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 0a69245cd3..648d4bdbe2 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -13,6 +13,14 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +type QueryTradesService interface { + QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) +} + +type QueryOrderService interface { + QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) +} + func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { defer func() { s.updateGridNumOfOrdersMetricsWithLock() @@ -59,9 +67,20 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex } s.logger.Infof("start recovering") - if err := s.recoverWithOpenOrdersByScanningTrades(ctx, historyService, openOrders); err != nil { + filledOrders, err := s.getFilledOrdersByScanningTrades(ctx, historyService, s.orderQueryService, openOrders) + if err != nil { return errors.Wrap(err, "grid recover error") } + s.debugOrders("emit filled orders", filledOrders) + + // add open orders into avtive maker orders + s.addOrdersToActiveOrderBook(openOrders) + + // emit the filled orders + activeOrderBook := s.orderExecutor.ActiveMakerOrders() + for _, filledOrder := range filledOrders { + activeOrderBook.EmitFilled(filledOrder) + } // emit ready after recover s.EmitGridReady() @@ -96,11 +115,7 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex return nil } -func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrdersOnGrid []types.Order) error { - if s.orderQueryService == nil { - return fmt.Errorf("orderQueryService is nil, it can't get orders by trade") - } - +func (s *Strategy) getFilledOrdersByScanningTrades(ctx context.Context, queryTradesService QueryTradesService, queryOrderService QueryOrderService, openOrdersOnGrid []types.Order) ([]types.Order, error) { // set grid grid := s.newGrid() s.setGrid(grid) @@ -110,22 +125,21 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi s.debugLog("open orders nums: %d, expected nums: %d", numGridOpenOrders, expectedNumOfOrders) if expectedNumOfOrders == numGridOpenOrders { // no need to recover, only need to add open orders back to active order book - s.addOrdersToActiveOrderBook(openOrdersOnGrid) - return nil + return nil, nil } else if expectedNumOfOrders < numGridOpenOrders { - return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") + return nil, fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") } // 1. build twin-order map twinOrdersOpen, err := s.buildTwinOrderMap(grid.Pins, openOrdersOnGrid) if err != nil { - return errors.Wrapf(err, "failed to build pin order map with open orders") + return nil, errors.Wrapf(err, "failed to build pin order map with open orders") } // 2. build the filled twin-order map by querying trades - twinOrdersFilled, err := s.buildFilledTwinOrderMapFromTrades(ctx, historyService, twinOrdersOpen) + twinOrdersFilled, err := s.buildFilledTwinOrderMapFromTrades(ctx, queryTradesService, queryOrderService, twinOrdersOpen) if err != nil { - return errors.Wrapf(err, "failed to build filled pin order map") + return nil, errors.Wrapf(err, "failed to build filled pin order map") } // 3. get the filled orders from twin-order map @@ -133,182 +147,10 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi // 4. verify the grid if err := s.verifyFilledTwinGrid(s.grid.Pins, twinOrdersOpen, filledOrders); err != nil { - return errors.Wrapf(err, "verify grid with error") - } - - // 5. add open orders to active order book. - s.addOrdersToActiveOrderBook(openOrdersOnGrid) - - // 6. emit the filled orders - s.debugOrders("emit filled orders", filledOrders) - activeOrderBook := s.orderExecutor.ActiveMakerOrders() - for _, filledOrder := range filledOrders { - activeOrderBook.EmitFilled(filledOrder) - } - - return nil -} - -func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { - s.debugLog("verifying filled grid - pins: %+v", pins) - s.debugLog("verifying filled grid - open pin orders:\n%s", pinOrders.String()) - s.debugOrders("verifying filled grid - filled orders", filledOrders) - - if err := addOrdersIntoPinOrderMap(pinOrders, filledOrders); err != nil { - return errors.Wrapf(err, "verifying filled grid error when add orders into pin order map") - } - - s.debugLog("verifying filled grid - filled pin orders:\n%+v", pinOrders.String()) - - expectedSide := types.SideTypeBuy - for _, pin := range pins { - order, exist := pinOrders[fixedpoint.Value(pin)] - if !exist { - return fmt.Errorf("there is no order at price (%+v)", pin) - } - - // if there is order with OrderID = 0, means we hit the empty pin - // there must be only one empty pin in the grid - // all orders below this pin need to be bid orders, above this pin need to be ask orders - if order.OrderID == 0 { - if expectedSide == types.SideTypeBuy { - expectedSide = types.SideTypeSell - continue - } - - return fmt.Errorf("found more than one empty pins") - } - - if order.Side != expectedSide { - return fmt.Errorf("the side of order (%s) is wrong, expected: %s", order.Side, expectedSide) - } - } - - if expectedSide != types.SideTypeSell { - return fmt.Errorf("there is no empty pin in the grid") + return nil, errors.Wrapf(err, "verify grid with error") } - return nil -} - -// buildPinOrderMap build the pin-order map with grid and open orders. -// The keys of this map contains all required pins of this grid. -// If the Order of the pin is empty types.Order (OrderID == 0), it means there is no open orders at this pin. -func (s *Strategy) buildPinOrderMap(pins []Pin, openOrders []types.Order) (PinOrderMap, error) { - pinOrderMap := make(PinOrderMap) - - for _, pin := range pins { - pinOrderMap[fixedpoint.Value(pin)] = types.Order{} - } - - for _, openOrder := range openOrders { - pin := openOrder.Price - v, exist := pinOrderMap[pin] - if !exist { - return nil, fmt.Errorf("the price of the order (id: %d) is not in pins", openOrder.OrderID) - } - - if v.OrderID != 0 { - return nil, fmt.Errorf("there are duplicated open orders at the same pin") - } - - pinOrderMap[pin] = openOrder - } - - return pinOrderMap, nil -} - -// buildFilledPinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map -// It will skip the orders on pins at which open orders are already -func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, pinOrdersOpen PinOrderMap) (PinOrderMap, error) { - pinOrdersFilled := make(PinOrderMap) - - // existedOrders is used to avoid re-query the same orders - existedOrders := pinOrdersOpen.SyncOrderMap() - - var limit int64 = 1000 - // get the filled orders when bbgo is down in order from trades - // [NOTE] only retrieve from last 24 hours !!! - var fromTradeID uint64 = 0 - for { - trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ - LastTradeID: fromTradeID, - Limit: limit, - }) - - if err != nil { - return nil, errors.Wrapf(err, "failed to query trades to recover the grid with open orders") - } - - s.debugLog("QueryTrades return %d trades", len(trades)) - - for _, trade := range trades { - s.debugLog(trade.String()) - if existedOrders.Exists(trade.OrderID) { - // already queries, skip - continue - } - - order, err := s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ - OrderID: strconv.FormatUint(trade.OrderID, 10), - }) - - if err != nil { - return nil, errors.Wrapf(err, "failed to query order by trade") - } - - s.debugLog("%s (group_id: %d)", order.String(), order.GroupID) - - // avoid query this order again - existedOrders.Add(*order) - - // add 1 to avoid duplicate - fromTradeID = trade.ID + 1 - - // checked the trade's order is filled order - pin := order.Price - v, exist := pinOrdersOpen[pin] - if !exist { - return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") - } - - // skip open orders on grid - if v.OrderID != 0 { - continue - } - - // check the order's creation time - if pinOrder, exist := pinOrdersFilled[pin]; exist && pinOrder.CreationTime.Time().After(order.CreationTime.Time()) { - // do not replace the pin order if the order's creation time is not after pin order's creation time - // this situation should not happen actually, because the trades is already sorted. - s.logger.Infof("pinOrder's creation time (%s) should not be after order's creation time (%s)", pinOrder.CreationTime, order.CreationTime) - continue - } - pinOrdersFilled[pin] = *order - } - - // stop condition - if int64(len(trades)) < limit { - break - } - } - - return pinOrdersFilled, nil -} - -func addOrdersIntoPinOrderMap(pinOrders PinOrderMap, orders []types.Order) error { - for _, order := range orders { - price := order.Price - if o, exist := pinOrders[price]; !exist { - return fmt.Errorf("the price (%+v) is not in pins", price) - } else if o.OrderID != 0 { - return fmt.Errorf("there is already an order at this price (%+v)", price) - } else { - pinOrders[price] = order - } - } - - return nil + return filledOrders, nil } func (s *Strategy) verifyFilledTwinGrid(pins []Pin, twinOrders TwinOrderMap, filledOrders []types.Order) error { @@ -384,7 +226,7 @@ func (s *Strategy) buildTwinOrderMap(pins []Pin, openOrders []types.Order) (Twin // buildFilledTwinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map // It will skip the orders on pins at which open orders are already -func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, twinOrdersOpen TwinOrderMap) (TwinOrderMap, error) { +func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, queryTradesService QueryTradesService, queryOrderService QueryOrderService, twinOrdersOpen TwinOrderMap) (TwinOrderMap, error) { twinOrdersFilled := make(TwinOrderMap) // existedOrders is used to avoid re-query the same orders @@ -395,7 +237,7 @@ func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, histor // [NOTE] only retrieve from last 24 hours !!! var fromTradeID uint64 = 0 for { - trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ + trades, err := queryTradesService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ LastTradeID: fromTradeID, Limit: limit, }) @@ -413,7 +255,7 @@ func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, histor continue } - order, err := s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ + order, err := queryOrderService.QueryOrder(ctx, types.OrderQuery{ OrderID: strconv.FormatUint(trade.OrderID, 10), }) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 3cdcb79538..483734f011 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -6,7 +6,6 @@ import ( "context" "errors" "testing" - "time" "github.com/golang/mock/gomock" "github.com/sirupsen/logrus" @@ -1230,6 +1229,7 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { }) } +/* func Test_buildPinOrderMap(t *testing.T) { assert := assert.New(t) s := newTestStrategy() @@ -1585,3 +1585,4 @@ func Test_verifyFilledGrid(t *testing.T) { }) } +*/ From 2f050332eb5b20190be6fc12c239de8f5553273e Mon Sep 17 00:00:00 2001 From: chiahung Date: Fri, 19 May 2023 18:41:07 +0800 Subject: [PATCH 0970/1392] FEATURE: query trades until hard limit or finish filled --- pkg/strategy/grid2/recover.go | 76 +++++++++++++++++++++++++--------- pkg/strategy/grid2/strategy.go | 5 ++- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 648d4bdbe2..471f17461d 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -137,7 +137,8 @@ func (s *Strategy) getFilledOrdersByScanningTrades(ctx context.Context, queryTra } // 2. build the filled twin-order map by querying trades - twinOrdersFilled, err := s.buildFilledTwinOrderMapFromTrades(ctx, queryTradesService, queryOrderService, twinOrdersOpen) + expectedFilledNum := int(expectedNumOfOrders - numGridOpenOrders) + twinOrdersFilled, err := s.buildFilledTwinOrderMapFromTrades(ctx, queryTradesService, queryOrderService, twinOrdersOpen, expectedFilledNum) if err != nil { return nil, errors.Wrapf(err, "failed to build filled pin order map") } @@ -226,63 +227,100 @@ func (s *Strategy) buildTwinOrderMap(pins []Pin, openOrders []types.Order) (Twin // buildFilledTwinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map // It will skip the orders on pins at which open orders are already -func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, queryTradesService QueryTradesService, queryOrderService QueryOrderService, twinOrdersOpen TwinOrderMap) (TwinOrderMap, error) { +func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, queryTradesService QueryTradesService, queryOrderService QueryOrderService, twinOrdersOpen TwinOrderMap, expectedFillNum int) (TwinOrderMap, error) { twinOrdersFilled := make(TwinOrderMap) // existedOrders is used to avoid re-query the same orders existedOrders := twinOrdersOpen.SyncOrderMap() - var limit int64 = 1000 // get the filled orders when bbgo is down in order from trades - // [NOTE] only retrieve from last 24 hours !!! + until := time.Now() + // the first query only query the last 1 hour, because mostly shutdown and recovery happens within 1 hour + since := until.Add(-1 * time.Hour) + // hard limit for recover + recoverSinceLimit := time.Date(2023, time.March, 10, 0, 0, 0, 0, time.UTC) + + if s.RecoverWithin != 0 && until.Add(-1*s.RecoverWithin).After(recoverSinceLimit) { + recoverSinceLimit = until.Add(-1 * s.RecoverWithin) + } + + for { + if err := s.queryTradesToUpdateTwinOrdersMap(ctx, queryTradesService, queryOrderService, twinOrdersOpen, twinOrdersFilled, existedOrders, since, until); err != nil { + return nil, errors.Wrapf(err, "failed to query trades to update twin orders map") + } + + until = since + since = until.Add(-6 * time.Hour) + + if len(twinOrdersFilled) >= expectedFillNum { + s.logger.Infof("stop querying trades because twin orders filled (%d) >= expected filled nums (%d)", len(twinOrdersFilled), expectedFillNum) + break + } + + if s.GridProfitStats != nil && s.GridProfitStats.Since != nil && until.Before(*s.GridProfitStats.Since) { + s.logger.Infof("stop querying trades because the time range is out of the strategy's since (%s)", *s.GridProfitStats.Since) + break + } + + if until.Before(recoverSinceLimit) { + s.logger.Infof("stop querying trades because the time range is out of the limit (%s)", recoverSinceLimit) + break + } + } + + return twinOrdersFilled, nil +} + +func (s *Strategy) queryTradesToUpdateTwinOrdersMap(ctx context.Context, queryTradesService QueryTradesService, queryOrderService QueryOrderService, twinOrdersOpen, twinOrdersFilled TwinOrderMap, existedOrders *types.SyncOrderMap, since, until time.Time) error { var fromTradeID uint64 = 0 + var limit int64 = 1000 for { trades, err := queryTradesService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ + StartTime: &since, + EndTime: &until, LastTradeID: fromTradeID, Limit: limit, }) if err != nil { - return nil, errors.Wrapf(err, "failed to query trades to recover the grid with open orders") + return errors.Wrapf(err, "failed to query trades to recover the grid with open orders") } - s.debugLog("QueryTrades return %d trades", len(trades)) + s.debugLog("QueryTrades from %s <-> %s (from: %d) return %d trades", since, until, fromTradeID, len(trades)) for _, trade := range trades { + if trade.Time.After(until) { + return nil + } + s.debugLog(trade.String()) + if existedOrders.Exists(trade.OrderID) { // already queries, skip continue } - order, err := queryOrderService.QueryOrder(ctx, types.OrderQuery{ OrderID: strconv.FormatUint(trade.OrderID, 10), }) if err != nil { - return nil, errors.Wrapf(err, "failed to query order by trade") + return errors.Wrapf(err, "failed to query order by trade") } - s.debugLog("%s (group_id: %d)", order.String(), order.GroupID) - + s.debugLog(order.String()) // avoid query this order again existedOrders.Add(*order) - - if order.GroupID != s.OrderGroupID { - continue - } - // add 1 to avoid duplicate fromTradeID = trade.ID + 1 twinOrderKey, err := findTwinOrderMapKey(s.grid, *order) if err != nil { - return nil, errors.Wrapf(err, "failed to find grid order map's key when recover") + return errors.Wrapf(err, "failed to find grid order map's key when recover") } twinOrderOpen, exist := twinOrdersOpen[twinOrderKey] if !exist { - return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") + return fmt.Errorf("the price of the order with the same GroupID is not in pins") } if twinOrderOpen.Exist() { @@ -304,11 +342,9 @@ func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, queryT // stop condition if int64(len(trades)) < limit { - break + return nil } } - - return twinOrdersFilled, nil } func (s *Strategy) addOrdersIntoTwinOrderMap(twinOrders TwinOrderMap, orders []types.Order) error { diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index dc45dffd04..4a4e99a670 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -165,8 +165,9 @@ type Strategy struct { // it makes sure that your grid configuration is profitable. FeeRate fixedpoint.Value `json:"feeRate"` - SkipSpreadCheck bool `json:"skipSpreadCheck"` - RecoverGridByScanningTrades bool `json:"recoverGridByScanningTrades"` + SkipSpreadCheck bool `json:"skipSpreadCheck"` + RecoverGridByScanningTrades bool `json:"recoverGridByScanningTrades"` + RecoverWithin time.Duration `json:"recoverWithin"` EnableProfitFixer bool `json:"enableProfitFixer"` FixProfitSince *types.Time `json:"fixProfitSince"` From 18a7520fa7cddba240df80b21f7ad8b58a39731c Mon Sep 17 00:00:00 2001 From: chiahung Date: Mon, 22 May 2023 14:45:39 +0800 Subject: [PATCH 0971/1392] MINOR: add test for recovery --- pkg/strategy/grid2/recover_test.go | 284 ++++++++++++++++++ .../testcase1/closed_orders.csv | 1 + .../testcase1/open_orders.csv | 3 + .../grid2/recovery_testcase/testcase1/spec | 12 + .../recovery_testcase/testcase1/trades.csv | 1 + 5 files changed, 301 insertions(+) create mode 100644 pkg/strategy/grid2/recover_test.go create mode 100644 pkg/strategy/grid2/recovery_testcase/testcase1/closed_orders.csv create mode 100644 pkg/strategy/grid2/recovery_testcase/testcase1/open_orders.csv create mode 100644 pkg/strategy/grid2/recovery_testcase/testcase1/spec create mode 100644 pkg/strategy/grid2/recovery_testcase/testcase1/trades.csv diff --git a/pkg/strategy/grid2/recover_test.go b/pkg/strategy/grid2/recover_test.go new file mode 100644 index 0000000000..55419430b2 --- /dev/null +++ b/pkg/strategy/grid2/recover_test.go @@ -0,0 +1,284 @@ +package grid2 + +import ( + "context" + "encoding/csv" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "sort" + "strconv" + "testing" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +type TestData struct { + Market types.Market `json:"market" yaml:"market"` + Strategy Strategy `json:"strategy" yaml:"strategy"` + OpenOrders []types.Order `json:"openOrders" yaml:"openOrders"` + ClosedOrders []types.Order `json:"closedOrders" yaml:"closedOrders"` + Trades []types.Trade `json:"trades" yaml:"trades"` +} + +type TestDataService struct { + Orders map[string]types.Order + Trades []types.Trade +} + +func (t *TestDataService) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { + var i int = 0 + if options.LastTradeID != 0 { + for idx, trade := range t.Trades { + if trade.ID < options.LastTradeID { + continue + } + + i = idx + break + } + } + + var trades []types.Trade + l := len(t.Trades) + for ; i < l && len(trades) < int(options.Limit); i++ { + trades = append(trades, t.Trades[i]) + } + + return trades, nil +} + +func (t *TestDataService) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { + if len(q.OrderID) == 0 { + return nil, fmt.Errorf("order id should not be empty") + } + + order, exist := t.Orders[q.OrderID] + if !exist { + return nil, fmt.Errorf("order not found") + } + + return &order, nil +} + +func NewStrategy(t *TestData) *Strategy { + s := t.Strategy + s.Debug = true + s.Initialize() + s.Market = t.Market + s.Position = types.NewPositionFromMarket(t.Market) + s.orderExecutor = bbgo.NewGeneralOrderExecutor(&bbgo.ExchangeSession{}, t.Market.Symbol, ID, s.InstanceID(), s.Position) + return &s +} + +func NewTestDataService(t *TestData) *TestDataService { + var orders map[string]types.Order = make(map[string]types.Order) + for _, order := range t.OpenOrders { + orders[strconv.FormatUint(order.OrderID, 10)] = order + } + + for _, order := range t.ClosedOrders { + orders[strconv.FormatUint(order.OrderID, 10)] = order + } + + trades := t.Trades + sort.Slice(t.Trades, func(i, j int) bool { + return trades[i].ID < trades[j].ID + }) + + return &TestDataService{ + Orders: orders, + Trades: trades, + } +} + +func readSpec(fileName string) (*TestData, error) { + content, err := ioutil.ReadFile(fileName) + if err != nil { + return nil, err + } + + market := types.Market{} + if err := json.Unmarshal(content, &market); err != nil { + return nil, err + } + + strategy := Strategy{} + if err := json.Unmarshal(content, &strategy); err != nil { + return nil, err + } + + data := TestData{ + Market: market, + Strategy: strategy, + } + return &data, nil +} + +func readOrdersFromCSV(fileName string) ([]types.Order, error) { + csvFile, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer csvFile.Close() + csvReader := csv.NewReader(csvFile) + + keys, err := csvReader.Read() + if err != nil { + return nil, err + } + + var orders []types.Order + for { + row, err := csvReader.Read() + if err == io.EOF { + break + } + + if err != nil { + return nil, err + } + + if len(row) != len(keys) { + return nil, fmt.Errorf("length of row should be equal to length of keys") + } + + var m map[string]interface{} = make(map[string]interface{}) + for i, key := range keys { + if key == "orderID" { + x, err := strconv.ParseUint(row[i], 10, 64) + if err != nil { + return nil, err + } + m[key] = x + } else { + m[key] = row[i] + } + } + + b, err := json.Marshal(m) + if err != nil { + return nil, err + } + + order := types.Order{} + if err = json.Unmarshal(b, &order); err != nil { + return nil, err + } + + orders = append(orders, order) + } + + return orders, nil +} + +func readTradesFromCSV(fileName string) ([]types.Trade, error) { + csvFile, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer csvFile.Close() + csvReader := csv.NewReader(csvFile) + + keys, err := csvReader.Read() + if err != nil { + return nil, err + } + + var trades []types.Trade + for { + row, err := csvReader.Read() + if err == io.EOF { + break + } + + if err != nil { + return nil, err + } + + if len(row) != len(keys) { + return nil, fmt.Errorf("length of row should be equal to length of keys") + } + + var m map[string]interface{} = make(map[string]interface{}) + for i, key := range keys { + switch key { + case "id", "orderID": + x, err := strconv.ParseUint(row[i], 10, 64) + if err != nil { + return nil, err + } + m[key] = x + default: + m[key] = row[i] + } + } + + b, err := json.Marshal(m) + if err != nil { + return nil, err + } + + trade := types.Trade{} + if err = json.Unmarshal(b, &trade); err != nil { + return nil, err + } + + trades = append(trades, trade) + } + + return trades, nil +} + +func readTestDataFrom(fileDir string) (*TestData, error) { + data, err := readSpec(fmt.Sprintf("%s/spec", fileDir)) + if err != nil { + return nil, err + } + + openOrders, err := readOrdersFromCSV(fmt.Sprintf("%s/open_orders.csv", fileDir)) + if err != nil { + return nil, err + } + + closedOrders, err := readOrdersFromCSV(fmt.Sprintf("%s/closed_orders.csv", fileDir)) + if err != nil { + return nil, err + } + + trades, err := readTradesFromCSV(fmt.Sprintf("%s/trades.csv", fileDir)) + if err != nil { + return nil, err + } + + data.OpenOrders = openOrders + data.ClosedOrders = closedOrders + data.Trades = trades + return data, nil +} + +func TestRecoverByScanningTrades(t *testing.T) { + assert := assert.New(t) + + t.Run("test case 1", func(t *testing.T) { + fileDir := "recovery_testcase/testcase1/" + + data, err := readTestDataFrom(fileDir) + if !assert.NoError(err) { + return + } + + testService := NewTestDataService(data) + strategy := NewStrategy(data) + filledOrders, err := strategy.getFilledOrdersByScanningTrades(context.Background(), testService, testService, data.OpenOrders) + if !assert.NoError(err) { + return + } + + assert.Len(filledOrders, 0) + }) +} diff --git a/pkg/strategy/grid2/recovery_testcase/testcase1/closed_orders.csv b/pkg/strategy/grid2/recovery_testcase/testcase1/closed_orders.csv new file mode 100644 index 0000000000..86178721a2 --- /dev/null +++ b/pkg/strategy/grid2/recovery_testcase/testcase1/closed_orders.csv @@ -0,0 +1 @@ +orderID,price,quantity,remaining_quantity,side,creationTime,updateTime \ No newline at end of file diff --git a/pkg/strategy/grid2/recovery_testcase/testcase1/open_orders.csv b/pkg/strategy/grid2/recovery_testcase/testcase1/open_orders.csv new file mode 100644 index 0000000000..6748a09a38 --- /dev/null +++ b/pkg/strategy/grid2/recovery_testcase/testcase1/open_orders.csv @@ -0,0 +1,3 @@ +orderID,price,quantity,remaining_quantity,side,creationTime,updateTime +1,20000.0000000000000000,1.0000000000000000,1.0000000000000000,buy,2023-05-01T08:00:00.000Z,2023-05-01T08:00:00.000Z +2,25000.0000000000000000,1.0000000000000000,1.0000000000000000,buy,2023-05-01T08:00:00.000Z,2023-05-01T08:00:00.000Z \ No newline at end of file diff --git a/pkg/strategy/grid2/recovery_testcase/testcase1/spec b/pkg/strategy/grid2/recovery_testcase/testcase1/spec new file mode 100644 index 0000000000..c4baf09655 --- /dev/null +++ b/pkg/strategy/grid2/recovery_testcase/testcase1/spec @@ -0,0 +1,12 @@ +{ + "symbol":"BTCUSDT", + "quoteCurrency": "USDT", + "baseCurrency": "BTC", + "mode":"arithmetic", + "lowerPrice":"20000.0", + "upperPrice":"30000.0", + "gridNumber":3, + "quoteInvestment":"45000.0", + "tickSize":0.1, + "stepSize":0.00000001 +} \ No newline at end of file diff --git a/pkg/strategy/grid2/recovery_testcase/testcase1/trades.csv b/pkg/strategy/grid2/recovery_testcase/testcase1/trades.csv new file mode 100644 index 0000000000..ade45cd393 --- /dev/null +++ b/pkg/strategy/grid2/recovery_testcase/testcase1/trades.csv @@ -0,0 +1 @@ +id,price,quantity,side,fee,feeCurrency,orderID,created_at,tradedAt \ No newline at end of file From 49971a2e50179349f80383eee15684355e9bfaa8 Mon Sep 17 00:00:00 2001 From: chiahung Date: Wed, 24 May 2023 14:58:19 +0800 Subject: [PATCH 0972/1392] use existing interface --- pkg/strategy/grid2/recover.go | 18 +++++------------- pkg/strategy/grid2/recover_test.go | 11 +++++++++++ pkg/strategy/grid2/strategy.go | 2 +- pkg/strategy/grid2/twin_order.go | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 471f17461d..d0ce99f701 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -13,14 +13,6 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -type QueryTradesService interface { - QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) -} - -type QueryOrderService interface { - QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) -} - func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { defer func() { s.updateGridNumOfOrdersMetricsWithLock() @@ -115,7 +107,7 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex return nil } -func (s *Strategy) getFilledOrdersByScanningTrades(ctx context.Context, queryTradesService QueryTradesService, queryOrderService QueryOrderService, openOrdersOnGrid []types.Order) ([]types.Order, error) { +func (s *Strategy) getFilledOrdersByScanningTrades(ctx context.Context, queryTradesService types.ExchangeTradeHistoryService, queryOrderService types.ExchangeOrderQueryService, openOrdersOnGrid []types.Order) ([]types.Order, error) { // set grid grid := s.newGrid() s.setGrid(grid) @@ -227,7 +219,7 @@ func (s *Strategy) buildTwinOrderMap(pins []Pin, openOrders []types.Order) (Twin // buildFilledTwinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map // It will skip the orders on pins at which open orders are already -func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, queryTradesService QueryTradesService, queryOrderService QueryOrderService, twinOrdersOpen TwinOrderMap, expectedFillNum int) (TwinOrderMap, error) { +func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, queryTradesService types.ExchangeTradeHistoryService, queryOrderService types.ExchangeOrderQueryService, twinOrdersOpen TwinOrderMap, expectedFillNum int) (TwinOrderMap, error) { twinOrdersFilled := make(TwinOrderMap) // existedOrders is used to avoid re-query the same orders @@ -240,8 +232,8 @@ func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, queryT // hard limit for recover recoverSinceLimit := time.Date(2023, time.March, 10, 0, 0, 0, 0, time.UTC) - if s.RecoverWithin != 0 && until.Add(-1*s.RecoverWithin).After(recoverSinceLimit) { - recoverSinceLimit = until.Add(-1 * s.RecoverWithin) + if s.RecoverGridWithin != 0 && until.Add(-1*s.RecoverGridWithin).After(recoverSinceLimit) { + recoverSinceLimit = until.Add(-1 * s.RecoverGridWithin) } for { @@ -271,7 +263,7 @@ func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, queryT return twinOrdersFilled, nil } -func (s *Strategy) queryTradesToUpdateTwinOrdersMap(ctx context.Context, queryTradesService QueryTradesService, queryOrderService QueryOrderService, twinOrdersOpen, twinOrdersFilled TwinOrderMap, existedOrders *types.SyncOrderMap, since, until time.Time) error { +func (s *Strategy) queryTradesToUpdateTwinOrdersMap(ctx context.Context, queryTradesService types.ExchangeTradeHistoryService, queryOrderService types.ExchangeOrderQueryService, twinOrdersOpen, twinOrdersFilled TwinOrderMap, existedOrders *types.SyncOrderMap, since, until time.Time) error { var fromTradeID uint64 = 0 var limit int64 = 1000 for { diff --git a/pkg/strategy/grid2/recover_test.go b/pkg/strategy/grid2/recover_test.go index 55419430b2..997cac9e5f 100644 --- a/pkg/strategy/grid2/recover_test.go +++ b/pkg/strategy/grid2/recover_test.go @@ -11,6 +11,7 @@ import ( "sort" "strconv" "testing" + "time" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/types" @@ -65,6 +66,16 @@ func (t *TestDataService) QueryOrder(ctx context.Context, q types.OrderQuery) (* return &order, nil } +// dummy method for interface +func (t *TestDataService) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) { + return nil, nil +} + +// dummy method for interface +func (t *TestDataService) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([]types.Trade, error) { + return nil, nil +} + func NewStrategy(t *TestData) *Strategy { s := t.Strategy s.Debug = true diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 4a4e99a670..e007369cec 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -167,7 +167,7 @@ type Strategy struct { SkipSpreadCheck bool `json:"skipSpreadCheck"` RecoverGridByScanningTrades bool `json:"recoverGridByScanningTrades"` - RecoverWithin time.Duration `json:"recoverWithin"` + RecoverGridWithin time.Duration `json:"recoverGridWithin"` EnableProfitFixer bool `json:"enableProfitFixer"` FixProfitSince *types.Time `json:"fixProfitSince"` diff --git a/pkg/strategy/grid2/twin_order.go b/pkg/strategy/grid2/twin_order.go index e05e2b4e0e..adeeb52634 100644 --- a/pkg/strategy/grid2/twin_order.go +++ b/pkg/strategy/grid2/twin_order.go @@ -99,7 +99,7 @@ func (m TwinOrderMap) String() string { } sort.Slice(pins, func(i, j int) bool { - return pins[j] < pins[i] + return pins[j].Compare(pins[i]) < 0 }) sb.WriteString("================== TWIN ORDER MAP ==================\n") From 518c6938beddddb90415439ead6c1391c152c3ea Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 12:22:43 +0800 Subject: [PATCH 0973/1392] xalign: add more checks --- pkg/strategy/xalign/strategy.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 3de3c32e20..d09b010f0b 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -34,7 +34,7 @@ type Strategy struct { UseTakerOrder bool `json:"useTakerOrder"` DryRun bool `json:"dryRun"` - orderBook map[string]*bbgo.ActiveOrderBook + orderBooks map[string]*bbgo.ActiveOrderBook } func (s *Strategy) ID() string { @@ -203,7 +203,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se instanceID := s.InstanceID() _ = instanceID - s.orderBook = make(map[string]*bbgo.ActiveOrderBook) + s.orderBooks = make(map[string]*bbgo.ActiveOrderBook) for _, sessionName := range s.PreferredSessions { session, ok := sessions[sessionName] @@ -213,7 +213,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se orderBook := bbgo.NewActiveOrderBook("") orderBook.BindStream(session.UserDataStream) - s.orderBook[sessionName] = orderBook + s.orderBooks[sessionName] = orderBook } go func() { @@ -242,8 +242,15 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange _ = sessionBalances for sessionName, session := range sessions { - if err := s.orderBook[sessionName].GracefulCancel(ctx, session.Exchange); err != nil { - log.WithError(err).Errorf("can not cancel order") + ob, ok := s.orderBooks[sessionName] + if !ok { + log.Errorf("orderbook on session %s not found", sessionName) + return + } + if ok { + if err := ob.GracefulCancel(ctx, session.Exchange); err != nil { + log.WithError(err).Errorf("can not cancel order") + } } } @@ -265,7 +272,7 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange } if createdOrder != nil { - s.orderBook[selectedSession.Name].Add(*createdOrder) + s.orderBooks[selectedSession.Name].Add(*createdOrder) } } } From 909c8f5cc72c9b0729a383f648b17ab40e22d034 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 12:24:25 +0800 Subject: [PATCH 0974/1392] xalign: add more checks --- pkg/strategy/xalign/strategy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index d09b010f0b..8b198ca098 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -272,7 +272,11 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange } if createdOrder != nil { - s.orderBooks[selectedSession.Name].Add(*createdOrder) + if ob, ok := s.orderBooks[selectedSession.Name]; ok { + ob.Add(*createdOrder) + } else { + log.Errorf("orderbook %s not found", selectedSession.Name) + } } } } From 740cfe6d5c4be72b54ecee99136b6846780b15ad Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 12:27:38 +0800 Subject: [PATCH 0975/1392] xalign: fix session refs --- pkg/strategy/xalign/strategy.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 8b198ca098..1adc975a12 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -34,6 +34,7 @@ type Strategy struct { UseTakerOrder bool `json:"useTakerOrder"` DryRun bool `json:"dryRun"` + sessions map[string]*bbgo.ExchangeSession orderBooks map[string]*bbgo.ActiveOrderBook } @@ -203,6 +204,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se instanceID := s.InstanceID() _ = instanceID + s.sessions = make(map[string]*bbgo.ExchangeSession) s.orderBooks = make(map[string]*bbgo.ActiveOrderBook) for _, sessionName := range s.PreferredSessions { @@ -213,11 +215,13 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se orderBook := bbgo.NewActiveOrderBook("") orderBook.BindStream(session.UserDataStream) + + s.sessions[sessionName] = session s.orderBooks[sessionName] = orderBook } go func() { - s.align(ctx, sessions) + s.align(ctx, s.sessions) ticker := time.NewTicker(s.Interval.Duration()) defer ticker.Stop() @@ -229,7 +233,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se return case <-ticker.C: - s.align(ctx, sessions) + s.align(ctx, s.sessions) } } }() From fe5a6f4c364ffbcf55d071ad5da0be9d3323f086 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 12:42:07 +0800 Subject: [PATCH 0976/1392] xalign: fix quote amount check --- pkg/strategy/xalign/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 1adc975a12..89fbc6035e 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -141,8 +141,8 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st requiredQuoteAmount := q.Mul(price) requiredQuoteAmount = requiredQuoteAmount.Round(market.PricePrecision, fixedpoint.Up) - if requiredQuoteAmount.Compare(quoteBalance.Available) < 0 { - log.Warnf("required quote amount %f < quote balance %v", requiredQuoteAmount.Float64(), quoteBalance) + if requiredQuoteAmount.Compare(quoteBalance.Available) > 0 { + log.Warnf("required quote amount %f > quote balance %v, skip", requiredQuoteAmount.Float64(), quoteBalance) continue } @@ -165,7 +165,7 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st } if q.Compare(baseBalance.Available) > 0 { - log.Warnf("required base amount %f < available base balance %v", q.Float64(), baseBalance) + log.Warnf("required base amount %f < available base balance %v, skip", q.Float64(), baseBalance) continue } From 5a30bedc775bd9b27d4e1b7ace15fda1bd83db44 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 13:23:10 +0800 Subject: [PATCH 0977/1392] autoborrow: always repay first when it deposits --- pkg/strategy/autoborrow/strategy.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index ba4d0a3b91..1a05b31c3b 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -368,15 +368,7 @@ func (s *Strategy) handleBalanceUpdate(balances types.BalanceMap) { func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateEvent) { bbgo.Notify(event) - if s.MinMarginLevel.IsZero() { - return - } - account := s.ExchangeSession.GetAccount() - if account.MarginLevel.Compare(s.MinMarginLevel) > 0 { - bbgo.Notify("account margin level %f is greater than minimal margin level %f, skip", account.MarginLevel.Float64(), s.MinMarginLevel.Float64()) - return - } delta := event.Delta From dadf22e48f87d55d936de7545e238597e228d831 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 13:40:39 +0800 Subject: [PATCH 0978/1392] xalign: add more log --- pkg/strategy/xalign/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 89fbc6035e..0bb0143bc0 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -122,6 +122,8 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st // changeQuantity < 0 = sell q := changeQuantity.Abs() + log.Infof("%s changeQuantity: %f ticker: %+v market: %+v", symbol, changeQuantity.Float64(), ticker, market) + switch side { case types.SideTypeBuy: From 64dcef3429584024995f1fddae05a90ed3ddd2d5 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 13:44:31 +0800 Subject: [PATCH 0979/1392] xalign: fix tick size calculation --- pkg/strategy/xalign/strategy.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 0bb0143bc0..67aa9db1a3 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -118,6 +118,8 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st continue } + spread := ticker.Sell.Sub(ticker.Buy) + // changeQuantity > 0 = buy // changeQuantity < 0 = sell q := changeQuantity.Abs() @@ -135,8 +137,8 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st price := ticker.Sell if taker { price = ticker.Sell - } else if ticker.Buy.Add(market.TickSize).Compare(ticker.Sell) < 0 { - price = ticker.Buy.Add(market.TickSize) + } else if spread.Compare(market.TickSize) > 0 { + price = ticker.Sell.Sub(market.TickSize) } else { price = ticker.Buy } @@ -174,14 +176,14 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st price := ticker.Buy if taker { price = ticker.Buy - } else if ticker.Sell.Add(market.TickSize.Neg()).Compare(ticker.Buy) < 0 { - price = ticker.Sell.Add(market.TickSize.Neg()) + } else if spread.Compare(market.TickSize) > 0 { + price = ticker.Buy.Add(market.TickSize) } else { price = ticker.Sell } if market.IsDustQuantity(q, price) { - log.Infof("%s dust quantity: %f", currency, q.Float64()) + log.Infof("%s ignore dust quantity: %f", currency, q.Float64()) return nil, nil } From 358e873582e65dc4fb3674846d6325aea8fca266 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 13:47:01 +0800 Subject: [PATCH 0980/1392] xalign: add notification --- pkg/strategy/xalign/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 67aa9db1a3..284393c6c6 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -269,6 +269,8 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange if selectedSession != nil && submitOrder != nil { log.Infof("placing order on %s: %#v", selectedSession.Name, submitOrder) + bbgo.Notify("Aligning position", submitOrder) + if s.DryRun { return } From 599b18fc3c2fbacea96332980545f6294d025485 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 13:49:22 +0800 Subject: [PATCH 0981/1392] xalign: skip dust quantity --- pkg/strategy/xalign/strategy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 284393c6c6..e8cd52692f 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -124,6 +124,11 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st // changeQuantity < 0 = sell q := changeQuantity.Abs() + if q.Compare(market.MinQuantity) < 0 { + log.Infof("skip dust quantity: %f", q.Float64()) + continue + } + log.Infof("%s changeQuantity: %f ticker: %+v market: %+v", symbol, changeQuantity.Float64(), ticker, market) switch side { From 476378e742e6727cae4c0c2641c1a62682b877e5 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 13:53:51 +0800 Subject: [PATCH 0982/1392] xalign:add one more dust check --- pkg/strategy/xalign/strategy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index e8cd52692f..fa5e99f0bc 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -155,6 +155,11 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st continue } + if market.IsDustQuantity(q, price) { + log.Infof("%s ignore dust quantity: %f", currency, q.Float64()) + return nil, nil + } + q = market.AdjustQuantityByMinNotional(q, price) return session, &types.SubmitOrder{ From 6308ef5107b6ebf243497ea992219a922813e0cc Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 14:21:16 +0800 Subject: [PATCH 0983/1392] autoborrow: repay debt first --- pkg/strategy/autoborrow/strategy.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 1a05b31c3b..6fe224cd9d 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -381,11 +381,16 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE curMarginLevel := account.MarginLevel if b, ok := account.Balance(event.Asset); ok { - if b.Available.IsZero() || b.Borrowed.IsZero() { + if b.Available.IsZero() { return } - toRepay := fixedpoint.Min(b.Borrowed, b.Available) + debt := b.Debt() + if debt.IsZero() { + return + } + + toRepay := fixedpoint.Min(debt, b.Available) if toRepay.IsZero() { return } From 21a01bd2c0b28f91756862096eab234ba3a29003 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 17:08:02 +0800 Subject: [PATCH 0984/1392] go: update go-binance to v2.4.2 --- go.mod | 6 +++--- go.sum | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a902c8d055..8b64f30cc1 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ go 1.18 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Masterminds/squirrel v1.5.3 - github.com/adshao/go-binance/v2 v2.4.1 + github.com/adshao/go-binance/v2 v2.4.2 github.com/c-bata/goptuna v0.8.1 github.com/c9s/requestgen v1.3.4 github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b @@ -47,7 +47,7 @@ require ( github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.7.4 + github.com/stretchr/testify v1.8.1 github.com/valyala/fastjson v1.5.1 github.com/wcharczuk/go-chart/v2 v2.1.0 github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 @@ -67,7 +67,7 @@ require ( github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect github.com/VividCortex/ewma v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bitly/go-simplejson v0.5.0 // indirect + github.com/bitly/go-simplejson v0.5.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect diff --git a/go.sum b/go.sum index 6561eb306f..f9c5077c35 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,8 @@ github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdc github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/adshao/go-binance/v2 v2.4.1 h1:fOZ2tCbN7sgDZvvsawUMjhsOoe40X87JVE4DklIyyyc= github.com/adshao/go-binance/v2 v2.4.1/go.mod h1:6Qoh+CYcj8U43h4HgT6mqJnsGj4mWZKA/nsj8LN8ZTU= +github.com/adshao/go-binance/v2 v2.4.2 h1:NBNMUyXrci45v3sr0RkZosiBYSw1/yuqCrJNkyEM8U0= +github.com/adshao/go-binance/v2 v2.4.2/go.mod h1:41Up2dG4NfMXpCldrDPETEtiOq+pHoGsFZ73xGgaumo= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -72,6 +74,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= +github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= @@ -654,6 +658,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -665,6 +670,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= From 36fa565460abdd9ec53643e9cdcc55d981abf72e Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 17:08:28 +0800 Subject: [PATCH 0985/1392] types: add one more market tests --- pkg/types/market_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/types/market_test.go b/pkg/types/market_test.go index 493c3a21d9..b6fee9e8c9 100644 --- a/pkg/types/market_test.go +++ b/pkg/types/market_test.go @@ -215,11 +215,10 @@ func TestMarket_TruncateQuantity(t *testing.T) { } func TestMarket_AdjustQuantityByMinNotional(t *testing.T) { - market := Market{ Symbol: "ETHUSDT", StepSize: fixedpoint.NewFromFloat(0.0001), - MinQuantity: fixedpoint.NewFromFloat(0.0001), + MinQuantity: fixedpoint.NewFromFloat(0.00045), MinNotional: fixedpoint.NewFromFloat(10.0), VolumePrecision: 8, PricePrecision: 2, @@ -228,16 +227,17 @@ func TestMarket_AdjustQuantityByMinNotional(t *testing.T) { // Quantity:0.00573961 Price:1750.99 testCases := []struct { input string + price fixedpoint.Value expect string }{ - {"0.00573961", "0.0058"}, + {"0.00573961", number(1750.99), "0.0058"}, + {"0.0019", number(1757.38), "0.0057"}, } - price := fixedpoint.NewFromFloat(1750.99) for _, testCase := range testCases { q := fixedpoint.MustNewFromString(testCase.input) - q2 := market.AdjustQuantityByMinNotional(q, price) + q2 := market.AdjustQuantityByMinNotional(q, testCase.price) assert.Equalf(t, testCase.expect, q2.String(), "input: %s stepSize: %s", testCase.input, market.StepSize.String()) - assert.False(t, market.IsDustQuantity(q2, price)) + assert.False(t, market.IsDustQuantity(q2, testCase.price)) } } From 0a7c0632c4fe32c57688b547800dc2a5c3ce22ff Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 17:08:37 +0800 Subject: [PATCH 0986/1392] xalign: use %+v format for submit order --- pkg/strategy/xalign/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index fa5e99f0bc..fddfaba4a6 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -277,7 +277,7 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange selectedSession, submitOrder := s.selectSessionForCurrency(ctx, sessions, currency, q) if selectedSession != nil && submitOrder != nil { - log.Infof("placing order on %s: %#v", selectedSession.Name, submitOrder) + log.Infof("placing order on %s: %+v", selectedSession.Name, submitOrder) bbgo.Notify("Aligning position", submitOrder) From a126bc3bb60360af2a95381527558eccd1df590b Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 17:09:37 +0800 Subject: [PATCH 0987/1392] binance: add market info warning --- pkg/exchange/binance/convert.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go index d160e4f96b..be2383689a 100644 --- a/pkg/exchange/binance/convert.go +++ b/pkg/exchange/binance/convert.go @@ -45,6 +45,14 @@ func toGlobalMarket(symbol binance.Symbol) types.Market { market.TickSize = fixedpoint.MustNewFromString(f.TickSize) } + if market.MinNotional.IsZero() { + log.Warn("binance market %s minNotional is zero", market.Symbol) + } + + if market.MinQuantity.IsZero() { + log.Warn("binance market %s minQuantity is zero", market.Symbol) + } + return market } From 1855e52838135ab5ec31bfb2615c20a15bfb52bb Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 17:29:19 +0800 Subject: [PATCH 0988/1392] xalign: graceful cancel orders when shutting down --- pkg/strategy/xalign/strategy.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index fddfaba4a6..3e5d7593ca 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "sync" "time" log "github.com/sirupsen/logrus" @@ -234,6 +235,15 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.orderBooks[sessionName] = orderBook } + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + for n, session := range s.sessions { + if ob, ok := s.orderBooks[n]; ok { + _ = ob.GracefulCancel(ctx, session.Exchange) + } + } + }) + go func() { s.align(ctx, s.sessions) From 007f3c9531f3df57b083e672b0239dd6330370b7 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 23:17:24 +0800 Subject: [PATCH 0989/1392] autoborrow: add margin level check back --- pkg/strategy/autoborrow/strategy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 6fe224cd9d..59e352122c 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -380,6 +380,11 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE minMarginLevel := s.MinMarginLevel curMarginLevel := account.MarginLevel + // margin repay/borrow also trigger this update event + if curMarginLevel.Compare(minMarginLevel) > 0 { + return + } + if b, ok := account.Balance(event.Asset); ok { if b.Available.IsZero() { return From 45aaad1629076354a324f9998abdd673b0c91049 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 23:21:07 +0800 Subject: [PATCH 0990/1392] xalign: improve update message --- pkg/strategy/xalign/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 3e5d7593ca..5cd476a991 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -289,7 +289,7 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange if selectedSession != nil && submitOrder != nil { log.Infof("placing order on %s: %+v", selectedSession.Name, submitOrder) - bbgo.Notify("Aligning position", submitOrder) + bbgo.Notify("Aligning position on exchange session %s, delta: %f", selectedSession.Name, q.Float64(), submitOrder) if s.DryRun { return From 1fd52f78a9e25b2e9b4b5fd27d79c5757bf097ea Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 23:23:41 +0800 Subject: [PATCH 0991/1392] xalign: allocate and bind order store --- pkg/strategy/xalign/strategy.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 5cd476a991..aa30d7412f 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -37,6 +37,8 @@ type Strategy struct { sessions map[string]*bbgo.ExchangeSession orderBooks map[string]*bbgo.ActiveOrderBook + + orderStore *bbgo.OrderStore } func (s *Strategy) ID() string { @@ -222,12 +224,16 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.sessions = make(map[string]*bbgo.ExchangeSession) s.orderBooks = make(map[string]*bbgo.ActiveOrderBook) + s.orderStore = bbgo.NewOrderStore("") + for _, sessionName := range s.PreferredSessions { session, ok := sessions[sessionName] if !ok { return fmt.Errorf("incorrect preferred session name: %s is not defined", sessionName) } + s.orderStore.BindStream(session.UserDataStream) + orderBook := bbgo.NewActiveOrderBook("") orderBook.BindStream(session.UserDataStream) From ea3b1cc937628c32c283d50bedac08d4b43702ef Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:39:44 +0800 Subject: [PATCH 0992/1392] binance: fix logrus call --- pkg/exchange/binance/convert.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go index be2383689a..7586770d48 100644 --- a/pkg/exchange/binance/convert.go +++ b/pkg/exchange/binance/convert.go @@ -46,11 +46,11 @@ func toGlobalMarket(symbol binance.Symbol) types.Market { } if market.MinNotional.IsZero() { - log.Warn("binance market %s minNotional is zero", market.Symbol) + log.Warnf("binance market %s minNotional is zero", market.Symbol) } if market.MinQuantity.IsZero() { - log.Warn("binance market %s minQuantity is zero", market.Symbol) + log.Warnf("binance market %s minQuantity is zero", market.Symbol) } return market From 9d9f898f17a2c5fa63a1a11d14b4b88873d4145e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Jun 2023 13:18:08 +0800 Subject: [PATCH 0993/1392] indicator: use pointer for float64series --- pkg/indicator/float64series.go | 4 ++-- pkg/indicator/v2_atrp.go | 6 ++++-- pkg/indicator/v2_cma.go | 2 +- pkg/indicator/v2_cross.go | 2 +- pkg/indicator/v2_ewma.go | 2 +- pkg/indicator/v2_macd_test.go | 2 +- pkg/indicator/v2_multiply.go | 2 +- pkg/indicator/v2_pivothigh.go | 2 +- pkg/indicator/v2_pivotlow.go | 2 +- pkg/indicator/v2_price.go | 17 +++++++++-------- pkg/indicator/v2_rma.go | 2 +- pkg/indicator/v2_rsi.go | 2 +- pkg/indicator/v2_rsi_test.go | 2 +- pkg/indicator/v2_sma.go | 2 +- pkg/indicator/v2_stddev.go | 2 +- pkg/indicator/v2_subtract.go | 2 +- pkg/indicator/v2_tr.go | 2 +- 17 files changed, 29 insertions(+), 26 deletions(-) diff --git a/pkg/indicator/float64series.go b/pkg/indicator/float64series.go index 821c176666..c198e8e8d1 100644 --- a/pkg/indicator/float64series.go +++ b/pkg/indicator/float64series.go @@ -11,8 +11,8 @@ type Float64Series struct { slice floats.Slice } -func NewFloat64Series(v ...float64) Float64Series { - s := Float64Series{} +func NewFloat64Series(v ...float64) *Float64Series { + s := &Float64Series{} s.slice = v s.SeriesBase.Series = s.slice return s diff --git a/pkg/indicator/v2_atrp.go b/pkg/indicator/v2_atrp.go index 6261c1cb37..bb9b982d36 100644 --- a/pkg/indicator/v2_atrp.go +++ b/pkg/indicator/v2_atrp.go @@ -1,11 +1,13 @@ package indicator type ATRPStream struct { - Float64Series + *Float64Series } func ATRP2(source KLineSubscription, window int) *ATRPStream { - s := &ATRPStream{} + s := &ATRPStream{ + Float64Series: NewFloat64Series(), + } tr := TR2(source) atr := RMA2(tr, window, true) atr.OnUpdate(func(x float64) { diff --git a/pkg/indicator/v2_cma.go b/pkg/indicator/v2_cma.go index f3c485aff4..9bc2c89945 100644 --- a/pkg/indicator/v2_cma.go +++ b/pkg/indicator/v2_cma.go @@ -1,7 +1,7 @@ package indicator type CMAStream struct { - Float64Series + *Float64Series } func CMA2(source Float64Source) *CMAStream { diff --git a/pkg/indicator/v2_cross.go b/pkg/indicator/v2_cross.go index 084130fdb7..835e87c302 100644 --- a/pkg/indicator/v2_cross.go +++ b/pkg/indicator/v2_cross.go @@ -13,7 +13,7 @@ const ( // CrossStream subscribes 2 upstreams, and calculate the cross signal type CrossStream struct { - Float64Series + *Float64Series a, b floats.Slice } diff --git a/pkg/indicator/v2_ewma.go b/pkg/indicator/v2_ewma.go index 1be654f85f..45a15d7cab 100644 --- a/pkg/indicator/v2_ewma.go +++ b/pkg/indicator/v2_ewma.go @@ -1,7 +1,7 @@ package indicator type EWMAStream struct { - Float64Series + *Float64Series window int multiplier float64 diff --git a/pkg/indicator/v2_macd_test.go b/pkg/indicator/v2_macd_test.go index 74617089a7..0fb9b647a1 100644 --- a/pkg/indicator/v2_macd_test.go +++ b/pkg/indicator/v2_macd_test.go @@ -41,7 +41,7 @@ func Test_MACD2(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - prices := &PriceStream{} + prices := ClosePrices(nil) macd := MACD2(prices, 12, 26, 9) for _, k := range tt.kLines { prices.EmitUpdate(k.Close.Float64()) diff --git a/pkg/indicator/v2_multiply.go b/pkg/indicator/v2_multiply.go index 24f647005f..ee76bdc001 100644 --- a/pkg/indicator/v2_multiply.go +++ b/pkg/indicator/v2_multiply.go @@ -3,7 +3,7 @@ package indicator import "github.com/c9s/bbgo/pkg/datatype/floats" type MultiplyStream struct { - Float64Series + *Float64Series a, b floats.Slice } diff --git a/pkg/indicator/v2_pivothigh.go b/pkg/indicator/v2_pivothigh.go index eb0352a731..9e41438163 100644 --- a/pkg/indicator/v2_pivothigh.go +++ b/pkg/indicator/v2_pivothigh.go @@ -5,7 +5,7 @@ import ( ) type PivotHighStream struct { - Float64Series + *Float64Series rawValues floats.Slice window, rightWindow int } diff --git a/pkg/indicator/v2_pivotlow.go b/pkg/indicator/v2_pivotlow.go index 47d76308ff..1fa78e054c 100644 --- a/pkg/indicator/v2_pivotlow.go +++ b/pkg/indicator/v2_pivotlow.go @@ -5,7 +5,7 @@ import ( ) type PivotLowStream struct { - Float64Series + *Float64Series rawValues floats.Slice window, rightWindow int } diff --git a/pkg/indicator/v2_price.go b/pkg/indicator/v2_price.go index d95976ddaa..6e4c61844b 100644 --- a/pkg/indicator/v2_price.go +++ b/pkg/indicator/v2_price.go @@ -11,22 +11,23 @@ type KLineSubscription interface { } type PriceStream struct { - Float64Series + *Float64Series mapper KLineValueMapper } func Price(source KLineSubscription, mapper KLineValueMapper) *PriceStream { s := &PriceStream{ - mapper: mapper, + Float64Series: NewFloat64Series(), + mapper: mapper, } - s.SeriesBase.Series = s.slice - - source.AddSubscriber(func(k types.KLine) { - v := s.mapper(k) - s.PushAndEmit(v) - }) + if source != nil { + source.AddSubscriber(func(k types.KLine) { + v := s.mapper(k) + s.PushAndEmit(v) + }) + } return s } diff --git a/pkg/indicator/v2_rma.go b/pkg/indicator/v2_rma.go index 17650d9d93..0464ad96d7 100644 --- a/pkg/indicator/v2_rma.go +++ b/pkg/indicator/v2_rma.go @@ -2,7 +2,7 @@ package indicator type RMAStream struct { // embedded structs - Float64Series + *Float64Series // config fields Adjust bool diff --git a/pkg/indicator/v2_rsi.go b/pkg/indicator/v2_rsi.go index 81e439320b..11cf984c93 100644 --- a/pkg/indicator/v2_rsi.go +++ b/pkg/indicator/v2_rsi.go @@ -2,7 +2,7 @@ package indicator type RSIStream struct { // embedded structs - Float64Series + *Float64Series // config fields window int diff --git a/pkg/indicator/v2_rsi_test.go b/pkg/indicator/v2_rsi_test.go index 533a89e1a4..311624024a 100644 --- a/pkg/indicator/v2_rsi_test.go +++ b/pkg/indicator/v2_rsi_test.go @@ -67,7 +67,7 @@ func Test_RSI2(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // RSI2() - prices := &PriceStream{} + prices := ClosePrices(nil) rsi := RSI2(prices, tt.window) t.Logf("data length: %d", len(tt.values)) diff --git a/pkg/indicator/v2_sma.go b/pkg/indicator/v2_sma.go index 641cdb3d29..b6a79277c6 100644 --- a/pkg/indicator/v2_sma.go +++ b/pkg/indicator/v2_sma.go @@ -3,7 +3,7 @@ package indicator import "github.com/c9s/bbgo/pkg/types" type SMAStream struct { - Float64Series + *Float64Series window int rawValues *types.Queue } diff --git a/pkg/indicator/v2_stddev.go b/pkg/indicator/v2_stddev.go index 28a5a4d070..9e465f9704 100644 --- a/pkg/indicator/v2_stddev.go +++ b/pkg/indicator/v2_stddev.go @@ -3,7 +3,7 @@ package indicator import "github.com/c9s/bbgo/pkg/types" type StdDevStream struct { - Float64Series + *Float64Series rawValues *types.Queue diff --git a/pkg/indicator/v2_subtract.go b/pkg/indicator/v2_subtract.go index 7ccde2bf67..33a1917304 100644 --- a/pkg/indicator/v2_subtract.go +++ b/pkg/indicator/v2_subtract.go @@ -6,7 +6,7 @@ import ( // SubtractStream subscribes 2 upstream data, and then subtract these 2 values type SubtractStream struct { - Float64Series + *Float64Series a, b floats.Slice i int diff --git a/pkg/indicator/v2_tr.go b/pkg/indicator/v2_tr.go index 05c6350e20..98f4bdc7b0 100644 --- a/pkg/indicator/v2_tr.go +++ b/pkg/indicator/v2_tr.go @@ -9,7 +9,7 @@ import ( // This TRStream calculates the ATR first type TRStream struct { // embedded struct - Float64Series + *Float64Series // private states previousClose float64 From 295ae95da6410e0be00257b2b142922119197552 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Jun 2023 18:36:54 +0800 Subject: [PATCH 0994/1392] indicator: implement bollinger indicator --- pkg/indicator/v2_boll.go | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 pkg/indicator/v2_boll.go diff --git a/pkg/indicator/v2_boll.go b/pkg/indicator/v2_boll.go new file mode 100644 index 0000000000..a1f1e31970 --- /dev/null +++ b/pkg/indicator/v2_boll.go @@ -0,0 +1,52 @@ +package indicator + +type BollStream struct { + // the band series + *Float64Series + + UpBand, DownBand *Float64Series + + window int + k float64 + + SMA *SMAStream + StdDev *StdDevStream +} + +// BOOL2 is bollinger indicator +// the data flow: +// +// priceSource -> +// +// -> calculate SMA +// -> calculate stdDev -> calculate bandWidth -> get latest SMA -> upBand, downBand +func BOLL2(source Float64Source, window int, k float64) *BollStream { + // bind these indicators before our main calculator + sma := SMA2(source, window) + stdDev := StdDev2(source, window) + + s := &BollStream{ + Float64Series: NewFloat64Series(), + UpBand: NewFloat64Series(), + DownBand: NewFloat64Series(), + window: window, + k: k, + SMA: sma, + StdDev: stdDev, + } + s.Bind(source, s) + + // on band update + s.Float64Series.OnUpdate(func(band float64) { + mid := s.SMA.Last(0) + s.UpBand.PushAndEmit(mid + band) + s.DownBand.PushAndEmit(mid - band) + }) + return s +} + +func (s *BollStream) Calculate(v float64) float64 { + stdDev := s.StdDev.Last(0) + band := stdDev * s.k + return band +} From 0a5f31a80f0c0f9d1acd63ecd1829c9b849e8491 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Jun 2023 18:38:12 +0800 Subject: [PATCH 0995/1392] indicator: rename BollStream to BOLLStream --- pkg/indicator/v2_boll.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/indicator/v2_boll.go b/pkg/indicator/v2_boll.go index a1f1e31970..ee0dfd15b2 100644 --- a/pkg/indicator/v2_boll.go +++ b/pkg/indicator/v2_boll.go @@ -1,6 +1,6 @@ package indicator -type BollStream struct { +type BOLLStream struct { // the band series *Float64Series @@ -20,12 +20,12 @@ type BollStream struct { // // -> calculate SMA // -> calculate stdDev -> calculate bandWidth -> get latest SMA -> upBand, downBand -func BOLL2(source Float64Source, window int, k float64) *BollStream { +func BOLL2(source Float64Source, window int, k float64) *BOLLStream { // bind these indicators before our main calculator sma := SMA2(source, window) stdDev := StdDev2(source, window) - s := &BollStream{ + s := &BOLLStream{ Float64Series: NewFloat64Series(), UpBand: NewFloat64Series(), DownBand: NewFloat64Series(), @@ -45,7 +45,7 @@ func BOLL2(source Float64Source, window int, k float64) *BollStream { return s } -func (s *BollStream) Calculate(v float64) float64 { +func (s *BOLLStream) Calculate(v float64) float64 { stdDev := s.StdDev.Last(0) band := stdDev * s.k return band From e529a3271d87b3abcb0ed369c5a527751af5460a Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 12 Jun 2023 17:38:17 +0800 Subject: [PATCH 0996/1392] indicator: fix ewma2 initial value --- pkg/indicator/v2_ewma.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/indicator/v2_ewma.go b/pkg/indicator/v2_ewma.go index 45a15d7cab..16feb99010 100644 --- a/pkg/indicator/v2_ewma.go +++ b/pkg/indicator/v2_ewma.go @@ -19,6 +19,10 @@ func EWMA2(source Float64Source, window int) *EWMAStream { func (s *EWMAStream) Calculate(v float64) float64 { last := s.slice.Last(0) + if last == 0.0 { + return v + } + m := s.multiplier return (1.0-m)*last + m*v } From fded41b0ea433087f764e51c3e57df76ea20966a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:22:25 +0800 Subject: [PATCH 0997/1392] indicator: fix macd test case since we changed the ewma default value --- pkg/indicator/v2_macd_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/indicator/v2_macd_test.go b/pkg/indicator/v2_macd_test.go index 0fb9b647a1..6f86d0ffa0 100644 --- a/pkg/indicator/v2_macd_test.go +++ b/pkg/indicator/v2_macd_test.go @@ -35,7 +35,7 @@ func Test_MACD2(t *testing.T) { { name: "random_case", kLines: buildKLines(input), - want: 0.7967670223776384, + want: 0.7740187187598249, }, } From 0482ade44a10f7111fec20cd32b03d30848708a2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 12 Jun 2023 16:01:40 +0800 Subject: [PATCH 0998/1392] backtest: adjust best bid/ask price with tick size --- pkg/backtest/exchange.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index 5faa412897..b358973ee1 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -12,18 +12,18 @@ for each kline, the backtest engine: There are 2 ways that a strategy could work with backtest engine: -1. the strategy receives kline from the market data stream, and then it submits the order by the given market data to the backtest engine. - backtest engine receives the order and then pushes the trade and order updates to the user data stream. + 1. the strategy receives kline from the market data stream, and then it submits the order by the given market data to the backtest engine. + backtest engine receives the order and then pushes the trade and order updates to the user data stream. - the strategy receives the trade and update its position. + the strategy receives the trade and update its position. -2. the strategy places the orders when it starts. (like grid) the strategy then receives the order updates and then submit a new order - by its order update message. + 2. the strategy places the orders when it starts. (like grid) the strategy then receives the order updates and then submit a new order + by its order update message. We need to ensure that: -1. if the strategy submits the order from the market data stream, since it's a separate goroutine, the strategy should block the backtest engine - to process the trades before the next kline is published. + 1. if the strategy submits the order from the market data stream, since it's a separate goroutine, the strategy should block the backtest engine + to process the trades before the next kline is published. */ package backtest @@ -270,8 +270,8 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke Open: kline.Open, High: kline.High, Low: kline.Low, - Buy: kline.Close, - Sell: kline.Close, + Buy: kline.Close.Sub(matching.Market.TickSize), + Sell: kline.Close.Add(matching.Market.TickSize), }, nil } From a28081a5d284119c39244d8e68839777e0a582d0 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 12:22:43 +0800 Subject: [PATCH 0999/1392] xalign: add more checks --- pkg/strategy/xalign/strategy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index aa30d7412f..d50c9f1142 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -236,9 +236,9 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se orderBook := bbgo.NewActiveOrderBook("") orderBook.BindStream(session.UserDataStream) + s.orderBooks[sessionName] = orderBook s.sessions[sessionName] = session - s.orderBooks[sessionName] = orderBook } bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { @@ -313,6 +313,7 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange } else { log.Errorf("orderbook %s not found", selectedSession.Name) } + s.orderBooks[selectedSession.Name].Add(*createdOrder) } } } From 40f8283616e12b5371f4c49b6219371967f702d8 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 12:22:59 +0800 Subject: [PATCH 1000/1392] scmaker: basic prototype --- config/scmaker.yaml | 50 +++++ pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/scmaker/intensity.go | 44 ++++ pkg/strategy/scmaker/strategy.go | 326 ++++++++++++++++++++++++++++++ pkg/types/position.go | 7 +- 5 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 config/scmaker.yaml create mode 100644 pkg/strategy/scmaker/intensity.go create mode 100644 pkg/strategy/scmaker/strategy.go diff --git a/config/scmaker.yaml b/config/scmaker.yaml new file mode 100644 index 0000000000..144de5c554 --- /dev/null +++ b/config/scmaker.yaml @@ -0,0 +1,50 @@ +sessions: + binance: + exchange: max + envVarPrefix: max + + +exchangeStrategies: +- on: max + scmaker: + symbol: USDCUSDT + + ## adjustmentUpdateInterval is the interval for adjusting position + adjustmentUpdateInterval: 1m + + ## liquidityUpdateInterval is the interval for updating liquidity orders + liquidityUpdateInterval: 1h + + midPriceEMA: + interval: 1h + window: 99 + + ## priceRangeBollinger is used for the liquidity price range + priceRangeBollinger: + interval: 1h + window: 10 + k: 1.0 + + numOfLiquidityLayers: 10 + + liquidityLayerTick: 0.01 + + strengthInterval: 1m + + liquidityScale: + exp: + domain: [0, 10] + range: [100, 500] + +backtest: + sessions: + - max + startTime: "2023-05-01" + endTime: "2023-06-01" + symbols: + - USDCUSDT + account: + max: + balances: + USDC: 5000 + USDT: 5000 diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 0a76cfb4e9..995f9f5b8e 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -29,6 +29,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/rebalance" _ "github.com/c9s/bbgo/pkg/strategy/rsmaker" _ "github.com/c9s/bbgo/pkg/strategy/schedule" + _ "github.com/c9s/bbgo/pkg/strategy/scmaker" _ "github.com/c9s/bbgo/pkg/strategy/skeleton" _ "github.com/c9s/bbgo/pkg/strategy/supertrend" _ "github.com/c9s/bbgo/pkg/strategy/support" diff --git a/pkg/strategy/scmaker/intensity.go b/pkg/strategy/scmaker/intensity.go new file mode 100644 index 0000000000..b4f3cb3836 --- /dev/null +++ b/pkg/strategy/scmaker/intensity.go @@ -0,0 +1,44 @@ +package scmaker + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type IntensityStream struct { + *indicator.Float64Series + + Buy, Sell *indicator.RMAStream + window int +} + +func Intensity(source indicator.KLineSubscription, window int) *IntensityStream { + s := &IntensityStream{ + Float64Series: indicator.NewFloat64Series(), + window: window, + + Buy: indicator.RMA2(indicator.NewFloat64Series(), window, false), + Sell: indicator.RMA2(indicator.NewFloat64Series(), window, false), + } + + threshold := fixedpoint.NewFromFloat(100.0) + source.AddSubscriber(func(k types.KLine) { + volume := k.Volume.Float64() + + // ignore zero volume events or <= 10usd events + if volume == 0.0 || k.Close.Mul(k.Volume).Compare(threshold) <= 0 { + return + } + + c := k.Close.Compare(k.Open) + if c > 0 { + s.Buy.PushAndEmit(volume) + } else if c < 0 { + s.Sell.PushAndEmit(volume) + } + s.Float64Series.PushAndEmit(k.High.Sub(k.Low).Float64()) + }) + + return s +} diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go new file mode 100644 index 0000000000..ade173865d --- /dev/null +++ b/pkg/strategy/scmaker/strategy.go @@ -0,0 +1,326 @@ +package scmaker + +import ( + "context" + "fmt" + "math" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "scmaker" + +var ten = fixedpoint.NewFromInt(10) + +type BollingerConfig struct { + Interval types.Interval `json:"interval"` + Window int `json:"window"` + K float64 `json:"k"` +} + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +// scmaker is a stable coin market maker +type Strategy struct { + Environment *bbgo.Environment + Market types.Market + + Symbol string `json:"symbol"` + + NumOfLiquidityLayers int `json:"numOfLiquidityLayers"` + + LiquidityUpdateInterval types.Interval `json:"liquidityUpdateInterval"` + PriceRangeBollinger *BollingerConfig `json:"priceRangeBollinger"` + StrengthInterval types.Interval `json:"strengthInterval"` + + AdjustmentUpdateInterval types.Interval `json:"adjustmentUpdateInterval"` + + MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` + LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` + + Position *types.Position `json:"position,omitempty" persistence:"position"` + ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook + book *types.StreamOrderBook + + liquidityScale bbgo.Scale + + // indicators + ewma *indicator.EWMAStream + boll *indicator.BOLLStream + intensity *IntensityStream +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.AdjustmentUpdateInterval}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LiquidityUpdateInterval}) + + if s.MidPriceEMA != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.MidPriceEMA.Interval}) + } +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + + s.session = session + s.book = types.NewStreamBook(s.Symbol) + s.book.BindStream(session.UserDataStream) + + s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + + // If position is nil, we need to allocate a new position for calculation + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + // Always update the position fields + s.Position.Strategy = ID + s.Position.StrategyInstanceID = instanceID + + if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { + s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ + MakerFeeRate: s.session.MakerFeeRate, + TakerFeeRate: s.session.TakerFeeRate, + }) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + scale, err := s.LiquiditySlideRule.Scale() + if err != nil { + return err + } + + if err := scale.Solve(); err != nil { + return err + } + + s.liquidityScale = scale + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.Bind() + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + + s.initializeMidPriceEMA(session) + s.initializePriceRangeBollinger(session) + s.initializeIntensityIndicator(session) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.AdjustmentUpdateInterval, func(k types.KLine) { + s.placeAdjustmentOrders(ctx) + })) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.LiquidityUpdateInterval, func(k types.KLine) { + s.placeLiquidityOrders(ctx) + })) + + return nil +} + +func (s *Strategy) initializeMidPriceEMA(session *bbgo.ExchangeSession) { + kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.MidPriceEMA.Interval) + s.ewma = indicator.EWMA2(indicator.ClosePrices(kLines), s.MidPriceEMA.Window) +} + +func (s *Strategy) initializeIntensityIndicator(session *bbgo.ExchangeSession) { + kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.StrengthInterval) + s.intensity = Intensity(kLines, 10) +} + +func (s *Strategy) initializePriceRangeBollinger(session *bbgo.ExchangeSession) { + kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.PriceRangeBollinger.Interval) + closePrices := indicator.ClosePrices(kLines) + s.boll = indicator.BOLL2(closePrices, s.PriceRangeBollinger.Window, s.PriceRangeBollinger.K) +} + +func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { + if s.Position.IsDust() { + return + } +} + +func (s *Strategy) placeLiquidityOrders(ctx context.Context) { + _ = s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) + + ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + if logErr(err, "unable to query ticker") { + return + } + + baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) + quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) + + spread := ticker.Sell.Sub(ticker.Buy) + _ = spread + + midPriceEMA := s.ewma.Last(0) + midPrice := fixedpoint.NewFromFloat(midPriceEMA) + + makerQuota := &bbgo.QuotaTransaction{} + makerQuota.QuoteAsset.Add(quoteBal.Available) + makerQuota.BaseAsset.Add(baseBal.Available) + + bandWidth := s.boll.Last(0) + _ = bandWidth + + log.Infof("mid price ema: %f boll band width: %f", midPriceEMA, bandWidth) + + var liqOrders []types.SubmitOrder + for i := 0; i <= s.NumOfLiquidityLayers; i++ { + fi := fixedpoint.NewFromInt(int64(i)) + quantity := fixedpoint.NewFromFloat(s.liquidityScale.Call(float64(i))) + bidPrice := midPrice.Sub(s.Market.TickSize.Mul(fi)) + askPrice := midPrice.Add(s.Market.TickSize.Mul(fi)) + if i == 0 { + bidPrice = ticker.Buy + askPrice = ticker.Sell + } + + log.Infof("layer #%d %f/%f = %f", i, askPrice.Float64(), bidPrice.Float64(), quantity.Float64()) + + placeBuy := true + placeSell := true + averageCost := s.Position.AverageCost + // when long position, do not place sell orders below the average cost + if !s.Position.IsDust() { + if s.Position.IsLong() && askPrice.Compare(averageCost) < 0 { + placeSell = false + } + + if s.Position.IsShort() && bidPrice.Compare(averageCost) > 0 { + placeBuy = false + } + } + + quoteQuantity := quantity.Mul(bidPrice) + + if !makerQuota.QuoteAsset.Lock(quoteQuantity) { + placeBuy = false + } + + if !makerQuota.BaseAsset.Lock(quantity) { + placeSell = false + } + + if placeBuy { + liqOrders = append(liqOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: quantity, + Price: bidPrice, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }) + } + + if placeSell { + liqOrders = append(liqOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: quantity, + Price: askPrice, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }) + } + } + + _, err = s.orderExecutor.SubmitOrders(ctx, liqOrders...) + logErr(err, "unable to place liquidity orders") +} + +func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseQuantity fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) { + var expBase = fixedpoint.Zero + + switch side { + case types.SideTypeBuy: + if priceTick.Sign() > 0 { + priceTick = priceTick.Neg() + } + + case types.SideTypeSell: + if priceTick.Sign() < 0 { + priceTick = priceTick.Neg() + } + } + + decdigits := priceTick.Abs().NumIntDigits() + step := priceTick.Abs().MulExp(-decdigits + 1) + + for i := 0; i < numOrders; i++ { + quantityExp := fixedpoint.NewFromFloat(math.Exp(expBase.Float64())) + volume := baseQuantity.Mul(quantityExp) + amount := volume.Mul(price) + // skip order less than 10usd + if amount.Compare(ten) < 0 { + log.Warnf("amount too small (< 10usd). price=%s volume=%s amount=%s", + price.String(), volume.String(), amount.String()) + continue + } + + orders = append(orders, types.SubmitOrder{ + Symbol: symbol, + Side: side, + Type: types.OrderTypeLimit, + Price: price, + Quantity: volume, + }) + + log.Infof("%s order: %s @ %s", side, volume.String(), price.String()) + + if len(orders) >= numOrders { + break + } + + price = price.Add(priceTick) + expBase = expBase.Add(step) + } + + return orders +} + +func logErr(err error, msgAndArgs ...interface{}) bool { + if err == nil { + return false + } + + if len(msgAndArgs) == 0 { + log.WithError(err).Error(err.Error()) + } else if len(msgAndArgs) == 1 { + msg := msgAndArgs[0].(string) + log.WithError(err).Error(msg) + } else if len(msgAndArgs) > 1 { + msg := msgAndArgs[0].(string) + log.WithError(err).Errorf(msg, msgAndArgs[1:]...) + } + + return true +} diff --git a/pkg/types/position.go b/pkg/types/position.go index bd428fe438..6cee34e75d 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -170,7 +170,12 @@ func (p *Position) NewMarketCloseOrder(percentage fixedpoint.Value) *SubmitOrder } } -func (p *Position) IsDust(price fixedpoint.Value) bool { +func (p *Position) IsDust(a ...fixedpoint.Value) bool { + price := p.AverageCost + if len(a) > 0 { + price = a[0] + } + base := p.Base.Abs() return p.Market.IsDustQuantity(base, price) } From aa4f998382261bac69b9def4b9b7af114925e7c3 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 15:16:32 +0800 Subject: [PATCH 1001/1392] bbgo: add scale Sum method --- pkg/bbgo/scale.go | 70 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/pkg/bbgo/scale.go b/pkg/bbgo/scale.go index da5a5bc52a..207a42589b 100644 --- a/pkg/bbgo/scale.go +++ b/pkg/bbgo/scale.go @@ -12,6 +12,7 @@ type Scale interface { Formula() string FormulaOf(x float64) string Call(x float64) (y float64) + Sum(step float64) float64 } func init() { @@ -21,6 +22,7 @@ func init() { _ = Scale(&QuadraticScale{}) } +// f(x) := ab^x // y := ab^x // shift xs[0] to 0 (x - h) // a = y1 @@ -56,6 +58,14 @@ func (s *ExponentialScale) Solve() error { return nil } +func (s *ExponentialScale) Sum(step float64) float64 { + sum := 0.0 + for x := s.Domain[0]; x <= s.Domain[1]; x += step { + sum += s.Call(x) + } + return sum +} + func (s *ExponentialScale) String() string { return s.Formula() } @@ -100,6 +110,14 @@ func (s *LogarithmicScale) Call(x float64) (y float64) { return y } +func (s *LogarithmicScale) Sum(step float64) float64 { + sum := 0.0 + for x := s.Domain[0]; x <= s.Domain[1]; x += step { + sum += s.Call(x) + } + return sum +} + func (s *LogarithmicScale) String() string { return s.Formula() } @@ -158,6 +176,14 @@ func (s *LinearScale) Call(x float64) (y float64) { return y } +func (s *LinearScale) Sum(step float64) float64 { + sum := 0.0 + for x := s.Domain[0]; x <= s.Domain[1]; x += step { + sum += s.Call(x) + } + return sum +} + func (s *LinearScale) String() string { return s.Formula() } @@ -201,6 +227,14 @@ func (s *QuadraticScale) Call(x float64) (y float64) { return y } +func (s *QuadraticScale) Sum(step float64) float64 { + sum := 0.0 + for x := s.Domain[0]; x <= s.Domain[1]; x += step { + sum += s.Call(x) + } + return sum +} + func (s *QuadraticScale) String() string { return s.Formula() } @@ -266,18 +300,20 @@ func (rule *SlideRule) Scale() (Scale, error) { // LayerScale defines the scale DSL for maker layers, e.g., // // quantityScale: -// byLayer: -// exp: -// domain: [1, 5] -// range: [0.01, 1.0] +// +// byLayer: +// exp: +// domain: [1, 5] +// range: [0.01, 1.0] // // and // // quantityScale: -// byLayer: -// linear: -// domain: [1, 3] -// range: [0.01, 1.0] +// +// byLayer: +// linear: +// domain: [1, 3] +// range: [0.01, 1.0] type LayerScale struct { LayerRule *SlideRule `json:"byLayer"` } @@ -303,18 +339,20 @@ func (s *LayerScale) Scale(layer int) (quantity float64, err error) { // PriceVolumeScale defines the scale DSL for strategy, e.g., // // quantityScale: -// byPrice: -// exp: -// domain: [10_000, 50_000] -// range: [0.01, 1.0] +// +// byPrice: +// exp: +// domain: [10_000, 50_000] +// range: [0.01, 1.0] // // and // // quantityScale: -// byVolume: -// linear: -// domain: [10_000, 50_000] -// range: [0.01, 1.0] +// +// byVolume: +// linear: +// domain: [10_000, 50_000] +// range: [0.01, 1.0] type PriceVolumeScale struct { ByPriceRule *SlideRule `json:"byPrice"` ByVolumeRule *SlideRule `json:"byVolume"` From b8597a1803fbca7d8329bc8f8bc75dfdad0ae877 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 15:16:48 +0800 Subject: [PATCH 1002/1392] scmaker: calculate balance quantity --- config/scmaker.yaml | 2 +- go.sum | 1 + pkg/strategy/scmaker/strategy.go | 49 ++++++++++++++++++++++++-------- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index 144de5c554..efc3eb9457 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -34,7 +34,7 @@ exchangeStrategies: liquidityScale: exp: domain: [0, 10] - range: [100, 500] + range: [1, 4] backtest: sessions: diff --git a/go.sum b/go.sum index f9c5077c35..f8c6b72d2c 100644 --- a/go.sum +++ b/go.sum @@ -671,6 +671,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index ade173865d..44666b817a 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -27,7 +27,7 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } -// scmaker is a stable coin market maker +// Strategy scmaker is a stable coin market maker type Strategy struct { Environment *bbgo.Environment Market types.Market @@ -191,18 +191,43 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { log.Infof("mid price ema: %f boll band width: %f", midPriceEMA, bandWidth) - var liqOrders []types.SubmitOrder + n := s.liquidityScale.Sum(1.0) + + var bidPrices []fixedpoint.Value + var askPrices []fixedpoint.Value + + // calculate and collect prices for i := 0; i <= s.NumOfLiquidityLayers; i++ { fi := fixedpoint.NewFromInt(int64(i)) - quantity := fixedpoint.NewFromFloat(s.liquidityScale.Call(float64(i))) - bidPrice := midPrice.Sub(s.Market.TickSize.Mul(fi)) - askPrice := midPrice.Add(s.Market.TickSize.Mul(fi)) if i == 0 { - bidPrice = ticker.Buy - askPrice = ticker.Sell + bidPrices = append(bidPrices, ticker.Buy) + askPrices = append(askPrices, ticker.Sell) + } else if i == s.NumOfLiquidityLayers { + bwf := fixedpoint.NewFromFloat(bandWidth) + bidPrices = append(bidPrices, midPrice.Add(-bwf)) + askPrices = append(askPrices, midPrice.Add(bwf)) + } else { + bidPrice := midPrice.Sub(s.Market.TickSize.Mul(fi)) + askPrice := midPrice.Add(s.Market.TickSize.Mul(fi)) + bidPrices = append(bidPrices, bidPrice) + askPrices = append(askPrices, askPrice) } + } + + askX := baseBal.Available.Float64() / n + bidX := quoteBal.Available.Float64() / (n * (fixedpoint.Sum(bidPrices).Float64())) + + askX = math.Trunc(askX*1e8) / 1e8 + bidX = math.Trunc(bidX*1e8) / 1e8 + + var liqOrders []types.SubmitOrder + for i := 0; i <= s.NumOfLiquidityLayers; i++ { + bidQuantity := fixedpoint.NewFromFloat(s.liquidityScale.Call(float64(i)) * bidX) + askQuantity := fixedpoint.NewFromFloat(s.liquidityScale.Call(float64(i)) * askX) + bidPrice := bidPrices[i] + askPrice := askPrices[i] - log.Infof("layer #%d %f/%f = %f", i, askPrice.Float64(), bidPrice.Float64(), quantity.Float64()) + log.Infof("layer #%d %f/%f = %f/%f", i, askPrice.Float64(), bidPrice.Float64(), askQuantity.Float64(), bidQuantity.Float64()) placeBuy := true placeSell := true @@ -218,13 +243,13 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { } } - quoteQuantity := quantity.Mul(bidPrice) + quoteQuantity := bidQuantity.Mul(bidPrice) if !makerQuota.QuoteAsset.Lock(quoteQuantity) { placeBuy = false } - if !makerQuota.BaseAsset.Lock(quantity) { + if !makerQuota.BaseAsset.Lock(askQuantity) { placeSell = false } @@ -233,7 +258,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { Symbol: s.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimitMaker, - Quantity: quantity, + Quantity: bidQuantity, Price: bidPrice, Market: s.Market, TimeInForce: types.TimeInForceGTC, @@ -245,7 +270,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { Symbol: s.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimitMaker, - Quantity: quantity, + Quantity: askQuantity, Price: askPrice, Market: s.Market, TimeInForce: types.TimeInForceGTC, From f426d151a82a5e169bd48419474fb7782f0f5021 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:20:37 +0800 Subject: [PATCH 1003/1392] scmaker: final version --- config/scmaker.yaml | 9 +- pkg/strategy/scmaker/strategy.go | 175 ++++++++++++++++++++----------- 2 files changed, 123 insertions(+), 61 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index efc3eb9457..5cc6568495 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -2,7 +2,8 @@ sessions: binance: exchange: max envVarPrefix: max - + makerFeeRate: 0% + takerFeeRate: 0.025% exchangeStrategies: - on: max @@ -31,6 +32,8 @@ exchangeStrategies: strengthInterval: 1m + minProfit: 0% + liquidityScale: exp: domain: [0, 10] @@ -39,12 +42,14 @@ exchangeStrategies: backtest: sessions: - max - startTime: "2023-05-01" + startTime: "2023-05-20" endTime: "2023-06-01" symbols: - USDCUSDT account: max: + makerFeeRate: 0.0% + takerFeeRate: 0.025% balances: USDC: 5000 USDT: 5000 diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 44666b817a..65ac662346 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -45,6 +45,8 @@ type Strategy struct { MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` + MinProfit fixedpoint.Value `json:"minProfit"` + Position *types.Position `json:"position,omitempty" persistence:"position"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` @@ -98,6 +100,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position.Strategy = ID s.Position.StrategyInstanceID = instanceID + // if anyone of the fee rate is defined, this assumes that both are defined. + // so that zero maker fee could be applied if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ MakerFeeRate: s.session.MakerFeeRate, @@ -132,13 +136,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.initializePriceRangeBollinger(session) s.initializeIntensityIndicator(session) - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.AdjustmentUpdateInterval, func(k types.KLine) { - s.placeAdjustmentOrders(ctx) - })) + session.MarketDataStream.OnKLineClosed(func(k types.KLine) { + if k.Interval == s.AdjustmentUpdateInterval { + s.placeAdjustmentOrders(ctx) + } - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.LiquidityUpdateInterval, func(k types.KLine) { - s.placeLiquidityOrders(ctx) - })) + if k.Interval == s.LiquidityUpdateInterval { + s.placeLiquidityOrders(ctx) + } + }) return nil } @@ -160,9 +166,67 @@ func (s *Strategy) initializePriceRangeBollinger(session *bbgo.ExchangeSession) } func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { + _ = s.adjustmentOrderBook.GracefulCancel(ctx, s.session.Exchange) + if s.Position.IsDust() { return } + + ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + if logErr(err, "unable to query ticker") { + return + } + + baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) + quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) + + var adjOrders []types.SubmitOrder + + var posSize = s.Position.Base.Abs() + + if s.Position.IsShort() { + price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(-s.Market.TickSize), s.session.MakerFeeRate, s.MinProfit) + quoteQuantity := fixedpoint.Min(price.Mul(posSize), quoteBal.Available) + bidQuantity := quoteQuantity.Div(price) + + if s.Market.IsDustQuantity(bidQuantity, price) { + return + } + + adjOrders = append(adjOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeBuy, + Price: price, + Quantity: bidQuantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }) + } else if s.Position.IsLong() { + price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(s.Market.TickSize), s.session.MakerFeeRate, s.MinProfit) + askQuantity := fixedpoint.Min(posSize, baseBal.Available) + + if s.Market.IsDustQuantity(askQuantity, price) { + return + } + + adjOrders = append(adjOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeSell, + Price: price, + Quantity: askQuantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }) + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, adjOrders...) + if logErr(err, "unable to place liquidity orders") { + return + } + + s.adjustmentOrderBook.Add(createdOrders...) } func (s *Strategy) placeLiquidityOrders(ctx context.Context) { @@ -177,7 +241,6 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) spread := ticker.Sell.Sub(ticker.Buy) - _ = spread midPriceEMA := s.ewma.Last(0) midPrice := fixedpoint.NewFromFloat(midPriceEMA) @@ -187,9 +250,8 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { makerQuota.BaseAsset.Add(baseBal.Available) bandWidth := s.boll.Last(0) - _ = bandWidth - log.Infof("mid price ema: %f boll band width: %f", midPriceEMA, bandWidth) + log.Infof("spread: %f mid price ema: %f boll band width: %f", spread.Float64(), midPriceEMA, bandWidth) n := s.liquidityScale.Sum(1.0) @@ -214,8 +276,31 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { } } - askX := baseBal.Available.Float64() / n - bidX := quoteBal.Available.Float64() / (n * (fixedpoint.Sum(bidPrices).Float64())) + availableBase := baseBal.Available + availableQuote := quoteBal.Available + + /* + log.Infof("available balances: %f %s, %f %s", + availableBase.Float64(), s.Market.BaseCurrency, + availableQuote.Float64(), s.Market.QuoteCurrency) + */ + + log.Infof("balances before liq orders: %s, %s", + baseBal.String(), + quoteBal.String()) + + if !s.Position.IsDust() { + if s.Position.IsLong() { + availableBase = availableBase.Sub(s.Position.Base) + availableBase = s.Market.RoundDownQuantityByPrecision(availableBase) + } else if s.Position.IsShort() { + posSizeInQuote := s.Position.Base.Mul(ticker.Sell) + availableQuote = availableQuote.Sub(posSizeInQuote) + } + } + + askX := availableBase.Float64() / n + bidX := availableQuote.Float64() / (n * (fixedpoint.Sum(bidPrices).Float64())) askX = math.Trunc(askX*1e8) / 1e8 bidX = math.Trunc(bidX*1e8) / 1e8 @@ -227,7 +312,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { bidPrice := bidPrices[i] askPrice := askPrices[i] - log.Infof("layer #%d %f/%f = %f/%f", i, askPrice.Float64(), bidPrice.Float64(), askQuantity.Float64(), bidQuantity.Float64()) + log.Infof("liqudity layer #%d %f/%f = %f/%f", i, askPrice.Float64(), bidPrice.Float64(), askQuantity.Float64(), bidQuantity.Float64()) placeBuy := true placeSell := true @@ -245,11 +330,11 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { quoteQuantity := bidQuantity.Mul(bidPrice) - if !makerQuota.QuoteAsset.Lock(quoteQuantity) { + if s.Market.IsDustQuantity(bidQuantity, bidPrice) || !makerQuota.QuoteAsset.Lock(quoteQuantity) { placeBuy = false } - if !makerQuota.BaseAsset.Lock(askQuantity) { + if s.Market.IsDustQuantity(askQuantity, askPrice) || !makerQuota.BaseAsset.Lock(askQuantity) { placeSell = false } @@ -278,58 +363,30 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { } } - _, err = s.orderExecutor.SubmitOrders(ctx, liqOrders...) - logErr(err, "unable to place liquidity orders") -} - -func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseQuantity fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) { - var expBase = fixedpoint.Zero - - switch side { - case types.SideTypeBuy: - if priceTick.Sign() > 0 { - priceTick = priceTick.Neg() - } + makerQuota.Commit() - case types.SideTypeSell: - if priceTick.Sign() < 0 { - priceTick = priceTick.Neg() - } + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, liqOrders...) + if logErr(err, "unable to place liquidity orders") { + return } - decdigits := priceTick.Abs().NumIntDigits() - step := priceTick.Abs().MulExp(-decdigits + 1) - - for i := 0; i < numOrders; i++ { - quantityExp := fixedpoint.NewFromFloat(math.Exp(expBase.Float64())) - volume := baseQuantity.Mul(quantityExp) - amount := volume.Mul(price) - // skip order less than 10usd - if amount.Compare(ten) < 0 { - log.Warnf("amount too small (< 10usd). price=%s volume=%s amount=%s", - price.String(), volume.String(), amount.String()) - continue - } - - orders = append(orders, types.SubmitOrder{ - Symbol: symbol, - Side: side, - Type: types.OrderTypeLimit, - Price: price, - Quantity: volume, - }) + s.liquidityOrderBook.Add(createdOrders...) +} - log.Infof("%s order: %s @ %s", side, volume.String(), price.String()) +func profitProtectedPrice(side types.SideType, averageCost, price, feeRate, minProfit fixedpoint.Value) fixedpoint.Value { + switch side { + case types.SideTypeSell: + minProfitPrice := averageCost.Add( + averageCost.Mul(feeRate.Add(minProfit))) + return fixedpoint.Max(minProfitPrice, price) - if len(orders) >= numOrders { - break - } + case types.SideTypeBuy: + minProfitPrice := averageCost.Sub( + averageCost.Mul(feeRate.Add(minProfit))) + return fixedpoint.Min(minProfitPrice, price) - price = price.Add(priceTick) - expBase = expBase.Add(step) } - - return orders + return price } func logErr(err error, msgAndArgs ...interface{}) bool { From 81aa46e46bf82331638d81b4095a5f72e1996bd9 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:30:55 +0800 Subject: [PATCH 1004/1392] config: adjust liquidityScale --- config/scmaker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index 5cc6568495..4db747e3b2 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -36,7 +36,7 @@ exchangeStrategies: liquidityScale: exp: - domain: [0, 10] + domain: [0, 9] range: [1, 4] backtest: From 68c3c96b102c2f929866f47dd0276d327abe71a0 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:55:32 +0800 Subject: [PATCH 1005/1392] scmaker: fix balance lock and active order book update issue --- config/scmaker.yaml | 2 +- pkg/strategy/scmaker/strategy.go | 31 +++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index 4db747e3b2..dbbaa25b39 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -28,7 +28,7 @@ exchangeStrategies: numOfLiquidityLayers: 10 - liquidityLayerTick: 0.01 + liquidityLayerTick: 0.001 strengthInterval: 1m diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 65ac662346..9a26b21fa8 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -44,6 +44,7 @@ type Strategy struct { MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` + LiquidityLayerTick fixedpoint.Value `json:"liquidityLayerTick"` MinProfit fixedpoint.Value `json:"minProfit"` @@ -89,7 +90,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.book.BindStream(session.UserDataStream) s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + s.liquidityOrderBook.BindStream(session.UserDataStream) + s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + s.adjustmentOrderBook.BindStream(session.UserDataStream) // If position is nil, we need to allocate a new position for calculation if s.Position == nil { @@ -177,15 +181,21 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { return } + if _, err := s.session.UpdateAccount(ctx); err != nil { + logErr(err, "unable to update account") + return + } + baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) var adjOrders []types.SubmitOrder - var posSize = s.Position.Base.Abs() + posSize := s.Position.Base.Abs() + tickSize := s.Market.TickSize if s.Position.IsShort() { - price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(-s.Market.TickSize), s.session.MakerFeeRate, s.MinProfit) + price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(-tickSize), s.session.MakerFeeRate, s.MinProfit) quoteQuantity := fixedpoint.Min(price.Mul(posSize), quoteBal.Available) bidQuantity := quoteQuantity.Div(price) @@ -203,7 +213,7 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { TimeInForce: types.TimeInForceGTC, }) } else if s.Position.IsLong() { - price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(s.Market.TickSize), s.session.MakerFeeRate, s.MinProfit) + price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(tickSize), s.session.MakerFeeRate, s.MinProfit) askQuantity := fixedpoint.Min(posSize, baseBal.Available) if s.Market.IsDustQuantity(askQuantity, price) { @@ -230,17 +240,26 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { } func (s *Strategy) placeLiquidityOrders(ctx context.Context) { - _ = s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) + err := s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) + if logErr(err, "unable to cancel orders") { + return + } ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) if logErr(err, "unable to query ticker") { return } + if _, err := s.session.UpdateAccount(ctx); err != nil { + logErr(err, "unable to update account") + return + } + baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) spread := ticker.Sell.Sub(ticker.Buy) + tickSize := fixedpoint.Max(s.LiquidityLayerTick, s.Market.TickSize) midPriceEMA := s.ewma.Last(0) midPrice := fixedpoint.NewFromFloat(midPriceEMA) @@ -269,8 +288,8 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { bidPrices = append(bidPrices, midPrice.Add(-bwf)) askPrices = append(askPrices, midPrice.Add(bwf)) } else { - bidPrice := midPrice.Sub(s.Market.TickSize.Mul(fi)) - askPrice := midPrice.Add(s.Market.TickSize.Mul(fi)) + bidPrice := midPrice.Sub(tickSize.Mul(fi)) + askPrice := midPrice.Add(tickSize.Mul(fi)) bidPrices = append(bidPrices, bidPrice) askPrices = append(askPrices, askPrice) } From 372028ebe64ebe6fcc9f3dbfd9b7fa8f77d15ba2 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:58:44 +0800 Subject: [PATCH 1006/1392] scmaker: truncate price with price precision --- pkg/strategy/scmaker/strategy.go | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 9a26b21fa8..d817b0c3eb 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -280,19 +280,24 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { // calculate and collect prices for i := 0; i <= s.NumOfLiquidityLayers; i++ { fi := fixedpoint.NewFromInt(int64(i)) - if i == 0 { - bidPrices = append(bidPrices, ticker.Buy) - askPrices = append(askPrices, ticker.Sell) - } else if i == s.NumOfLiquidityLayers { + + bidPrice := ticker.Buy + askPrice := ticker.Sell + + if i == s.NumOfLiquidityLayers { bwf := fixedpoint.NewFromFloat(bandWidth) - bidPrices = append(bidPrices, midPrice.Add(-bwf)) - askPrices = append(askPrices, midPrice.Add(bwf)) - } else { - bidPrice := midPrice.Sub(tickSize.Mul(fi)) - askPrice := midPrice.Add(tickSize.Mul(fi)) - bidPrices = append(bidPrices, bidPrice) - askPrices = append(askPrices, askPrice) + bidPrice = midPrice.Add(-bwf) + askPrice = midPrice.Add(bwf) + } else if i > 0 { + bidPrice = midPrice.Sub(tickSize.Mul(fi)) + askPrice = midPrice.Add(tickSize.Mul(fi)) } + + bidPrice = s.Market.TruncatePrice(bidPrice) + askPrice = s.Market.TruncatePrice(askPrice) + + bidPrices = append(bidPrices, bidPrice) + askPrices = append(askPrices, askPrice) } availableBase := baseBal.Available From 8344193e81fa06546bb35fc54b5a7dd82dad4a52 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 17:00:47 +0800 Subject: [PATCH 1007/1392] scmaker: rename liquidityLayerTick to liquidityLayerTickSize --- config/scmaker.yaml | 2 +- pkg/strategy/scmaker/strategy.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index dbbaa25b39..a0b8498506 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -28,7 +28,7 @@ exchangeStrategies: numOfLiquidityLayers: 10 - liquidityLayerTick: 0.001 + liquidityLayerTickSize: 0.0001 strengthInterval: 1m diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index d817b0c3eb..cb7fa41ca8 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -42,9 +42,9 @@ type Strategy struct { AdjustmentUpdateInterval types.Interval `json:"adjustmentUpdateInterval"` - MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` - LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` - LiquidityLayerTick fixedpoint.Value `json:"liquidityLayerTick"` + MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` + LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` + LiquidityLayerTickSize fixedpoint.Value `json:"liquidityLayerTickSize"` MinProfit fixedpoint.Value `json:"minProfit"` @@ -195,7 +195,7 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { tickSize := s.Market.TickSize if s.Position.IsShort() { - price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(-tickSize), s.session.MakerFeeRate, s.MinProfit) + price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(tickSize.Neg()), s.session.MakerFeeRate, s.MinProfit) quoteQuantity := fixedpoint.Min(price.Mul(posSize), quoteBal.Available) bidQuantity := quoteQuantity.Div(price) @@ -259,7 +259,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) spread := ticker.Sell.Sub(ticker.Buy) - tickSize := fixedpoint.Max(s.LiquidityLayerTick, s.Market.TickSize) + tickSize := fixedpoint.Max(s.LiquidityLayerTickSize, s.Market.TickSize) midPriceEMA := s.ewma.Last(0) midPrice := fixedpoint.NewFromFloat(midPriceEMA) @@ -286,7 +286,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { if i == s.NumOfLiquidityLayers { bwf := fixedpoint.NewFromFloat(bandWidth) - bidPrice = midPrice.Add(-bwf) + bidPrice = midPrice.Add(bwf.Neg()) askPrice = midPrice.Add(bwf) } else if i > 0 { bidPrice = midPrice.Sub(tickSize.Mul(fi)) From 5f3ab4776d28261f0b704adc7fc13553636f831a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 17:05:58 +0800 Subject: [PATCH 1008/1392] config/scmaker: adjust minProfit ratio --- config/scmaker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index a0b8498506..bc009f3b68 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -32,7 +32,7 @@ exchangeStrategies: strengthInterval: 1m - minProfit: 0% + minProfit: 0.004% liquidityScale: exp: From 148869d46b254d157cb4f92536ae6283442ffea8 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 17:31:01 +0800 Subject: [PATCH 1009/1392] scmaker: clean up --- pkg/strategy/scmaker/strategy.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index cb7fa41ca8..99bfbfc15a 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -303,12 +303,6 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { availableBase := baseBal.Available availableQuote := quoteBal.Available - /* - log.Infof("available balances: %f %s, %f %s", - availableBase.Float64(), s.Market.BaseCurrency, - availableQuote.Float64(), s.Market.QuoteCurrency) - */ - log.Infof("balances before liq orders: %s, %s", baseBal.String(), quoteBal.String()) From 73726b91c782ef1912cfd944386cd9550f83d49d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Jun 2023 13:47:21 +0800 Subject: [PATCH 1010/1392] scmaker: check ticker price and adjust liq order prices --- pkg/strategy/scmaker/strategy.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 99bfbfc15a..291072c2b6 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -289,8 +289,17 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { bidPrice = midPrice.Add(bwf.Neg()) askPrice = midPrice.Add(bwf) } else if i > 0 { - bidPrice = midPrice.Sub(tickSize.Mul(fi)) - askPrice = midPrice.Add(tickSize.Mul(fi)) + sp := tickSize.Mul(fi) + bidPrice = midPrice.Sub(sp) + askPrice = midPrice.Add(sp) + + if bidPrice.Compare(ticker.Buy) < 0 { + bidPrice = ticker.Buy.Sub(sp) + } + + if askPrice.Compare(ticker.Sell) > 0 { + askPrice = ticker.Sell.Add(sp) + } } bidPrice = s.Market.TruncatePrice(bidPrice) From eb464cb5954fecd038a8740a356eea846065ec95 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Jun 2023 14:07:35 +0800 Subject: [PATCH 1011/1392] config/scmaker: adjust minProfit --- config/scmaker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index bc009f3b68..49be4bacd0 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -32,7 +32,7 @@ exchangeStrategies: strengthInterval: 1m - minProfit: 0.004% + minProfit: 0.01% liquidityScale: exp: From f7c5311ee86bf22f694d7e1cd0d7fcace5e43ea6 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Jun 2023 15:03:09 +0800 Subject: [PATCH 1012/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index 42d672c319..a412c6ac5d 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -60,4 +60,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index c953a66713..633ad757b2 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -43,4 +43,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 7da5698553..cd31611a43 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -52,4 +52,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index d906870f13..8028402304 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -42,4 +42,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index 5ab8436544..aa4462644f 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -41,4 +41,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index 6c2c94ddd4..bb87bd218f 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -51,4 +51,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index 45cbc970b0..e252e1f491 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -43,4 +43,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 692e6f45c4..0f0fb5d4e2 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -50,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 2977e92976..0f794e9aaa 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -44,4 +44,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index 1492b6e8ca..fb6ebef561 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -47,4 +47,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 48348add62..a6fde11c3e 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -44,4 +44,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index 4051b2c713..aa07804cd0 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -43,4 +43,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index f7f9cb7363..16aa164bad 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -40,4 +40,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index 786d1b45e4..412fb36509 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -43,4 +43,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 4e77b1b754..5ef04adf9c 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -43,4 +43,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index 105d7e150d..c9724ab550 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -43,4 +43,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index c5f9259812..544c4ced42 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -42,4 +42,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index aa157f8b79..c7c0639b97 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -46,4 +46,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index 48793d720c..22e318e7af 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -44,4 +44,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index 5bac1ea81e..d0d91ec545 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -42,4 +42,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index a384086592..68fece6e9b 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -51,4 +51,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 042a5e5e06..b63bcdea48 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -53,4 +53,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 98e0d7b11b..5cd55dbd6b 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -48,4 +48,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index 99a74fb914..627c62b76e 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -44,4 +44,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index 96f165273d..d68760f968 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -44,4 +44,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 4b2509fce5..3653ab8c69 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -42,4 +42,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 48190176b7..386c1f76a5 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -44,4 +44,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index 472ab06c78..e591631b4c 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -42,4 +42,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index 3d6779e5c8..e17fedf935 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -41,4 +41,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 4-May-2023 +###### Auto generated by spf13/cobra on 15-Jun-2023 From aa26dfaabc7831e11d53a8e412740207d51d575f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Jun 2023 15:03:09 +0800 Subject: [PATCH 1013/1392] bump version to v1.48.0 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 683a8beb67..e09709ad76 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.47.0-30d82b1e-dev" +const Version = "v1.48.0-50778c46-dev" -const VersionGitRef = "30d82b1e" +const VersionGitRef = "50778c46" diff --git a/pkg/version/version.go b/pkg/version/version.go index 0c978dba98..7923e029fd 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.47.0-30d82b1e" +const Version = "v1.48.0-50778c46" -const VersionGitRef = "30d82b1e" +const VersionGitRef = "50778c46" From 472a9fd5fdda8a91a165af9ba6c47f45aac09243 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Jun 2023 15:03:09 +0800 Subject: [PATCH 1014/1392] add v1.48.0 release note --- doc/release/v1.48.0.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 doc/release/v1.48.0.md diff --git a/doc/release/v1.48.0.md b/doc/release/v1.48.0.md new file mode 100644 index 0000000000..04fabd6fb3 --- /dev/null +++ b/doc/release/v1.48.0.md @@ -0,0 +1,39 @@ +## New Strategies + +- xalign +- scmaker + +## Features + +- v2 indicator apis + + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.47.0...main) + + - [#1197](https://github.com/c9s/bbgo/pull/1197): FEATURE: [strategy] add stable coin market maker + - [#1174](https://github.com/c9s/bbgo/pull/1174): FEATURE: [grid2] recover with twin orders + - [#1194](https://github.com/c9s/bbgo/pull/1194): IMPROVE: improve hhllstop message + - [#1196](https://github.com/c9s/bbgo/pull/1196): FIX: [xalign] add DryRun and fix quote amount calculation + - [#1195](https://github.com/c9s/bbgo/pull/1195): FEATURE: [strategy] add xalign strategy + - [#1192](https://github.com/c9s/bbgo/pull/1192): IMPROVE: improve order executor error checking, trailing stop and indicators + - [#1193](https://github.com/c9s/bbgo/pull/1193): FIX: [schedule] fix quantity truncation add minBaseBalance config + - [#1188](https://github.com/c9s/bbgo/pull/1188): FEATURE: [indicator] add multiply operator + - [#1185](https://github.com/c9s/bbgo/pull/1185): FIX: [autoborrow] add max borrowable check and add more notifications + - [#1189](https://github.com/c9s/bbgo/pull/1189): FEATURE: [indicator] add v2 stddev indicator + - [#1190](https://github.com/c9s/bbgo/pull/1190): FEATURE: [indicator] add v2 pivot low indicator + - [#1187](https://github.com/c9s/bbgo/pull/1187): FEATURE: [indicator] add stoch v2 + - [#1186](https://github.com/c9s/bbgo/pull/1186): FEATURE: [indicator] add v2 CMA indicator + - [#1184](https://github.com/c9s/bbgo/pull/1184): FEATURE: [indicator] add v2 MACD, SMA + - [#1183](https://github.com/c9s/bbgo/pull/1183): REFACTOR: [indicator] replace all Index(i) callers + - [#1182](https://github.com/c9s/bbgo/pull/1182): REFACTOR: add parameter index to the Last method + - [#1181](https://github.com/c9s/bbgo/pull/1181): FEATURE: [indicator] add new ATR, RMA indicators with the new API design + - [#1179](https://github.com/c9s/bbgo/pull/1179): FEATURE: new indicator API design + - [#1180](https://github.com/c9s/bbgo/pull/1180): FIX: [grid2] fix base quote investment check + - [#1176](https://github.com/c9s/bbgo/pull/1176): FIX: [grid2] fix base + quote calculation and add baseGridNumber config field + - [#1177](https://github.com/c9s/bbgo/pull/1177): FIX: some types and operations in SeriesExtended are not supported + - [#1172](https://github.com/c9s/bbgo/pull/1172): FEATURE: [grid2] truncate base quantity for quote+base mode + - [#1171](https://github.com/c9s/bbgo/pull/1171): FEATURE: Add types.StrInt64 for decoding integer in JSON string format + - [#1169](https://github.com/c9s/bbgo/pull/1169): WIP: FEATURE: add bitget exchange support + - [#1156](https://github.com/c9s/bbgo/pull/1156): REFACTOR: pull out Fast* methods to FastOrderExecutor + - [#1166](https://github.com/c9s/bbgo/pull/1166): FIX: [max] replace deprecated max v3 API + - [#1167](https://github.com/c9s/bbgo/pull/1167): FEATURE: support binance futures trading data From a7b2051858592bab14dfc4c877ee8958dbb47370 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Jun 2023 17:26:04 +0800 Subject: [PATCH 1015/1392] scmaker: fix the layer price --- config/scmaker.yaml | 4 ++-- pkg/strategy/scmaker/strategy.go | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index 49be4bacd0..9f8b2991c1 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -8,7 +8,7 @@ sessions: exchangeStrategies: - on: max scmaker: - symbol: USDCUSDT + symbol: &symbol USDCUSDT ## adjustmentUpdateInterval is the interval for adjusting position adjustmentUpdateInterval: 1m @@ -45,7 +45,7 @@ backtest: startTime: "2023-05-20" endTime: "2023-06-01" symbols: - - USDCUSDT + - *symbol account: max: makerFeeRate: 0.0% diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 291072c2b6..553608e8e9 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -280,6 +280,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { // calculate and collect prices for i := 0; i <= s.NumOfLiquidityLayers; i++ { fi := fixedpoint.NewFromInt(int64(i)) + sp := tickSize.Mul(fi) bidPrice := ticker.Buy askPrice := ticker.Sell @@ -289,17 +290,16 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { bidPrice = midPrice.Add(bwf.Neg()) askPrice = midPrice.Add(bwf) } else if i > 0 { - sp := tickSize.Mul(fi) bidPrice = midPrice.Sub(sp) askPrice = midPrice.Add(sp) + } - if bidPrice.Compare(ticker.Buy) < 0 { - bidPrice = ticker.Buy.Sub(sp) - } + if i > 0 && bidPrice.Compare(ticker.Buy) < 0 { + bidPrice = ticker.Buy.Sub(sp) + } - if askPrice.Compare(ticker.Sell) > 0 { - askPrice = ticker.Sell.Add(sp) - } + if i > 0 && askPrice.Compare(ticker.Sell) > 0 { + askPrice = ticker.Sell.Add(sp) } bidPrice = s.Market.TruncatePrice(bidPrice) From 2ed5095ffb9502f8714349adfd81b2a48c9da231 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 15 Jun 2023 17:35:52 +0800 Subject: [PATCH 1016/1392] feature/profitReport: pass 0 to Last() --- pkg/report/profit_report.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index e3faaa50f8..9d39b32786 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -122,7 +122,7 @@ func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { // Accumulated profit MA r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) - r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last()) + r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last(0)) // Accumulated Fee r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) From 561ee0aec775a15fd2b95b3856708673d5125345 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 08:30:35 +0800 Subject: [PATCH 1017/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index a412c6ac5d..83bada29f5 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -60,4 +60,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 633ad757b2..9462633ce4 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -43,4 +43,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index cd31611a43..6aea00c319 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -52,4 +52,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index 8028402304..8584613882 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -42,4 +42,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index aa4462644f..296a0f951f 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -41,4 +41,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index bb87bd218f..6661d89e35 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -51,4 +51,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index e252e1f491..44f1564629 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -43,4 +43,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 0f0fb5d4e2..3c3110716a 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -50,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 0f794e9aaa..7e2da6f27b 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -44,4 +44,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index fb6ebef561..b4c1639639 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -47,4 +47,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index a6fde11c3e..229b5a9b31 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -44,4 +44,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index aa07804cd0..bbdbbc7803 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -43,4 +43,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index 16aa164bad..ae9c0f1715 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -40,4 +40,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index 412fb36509..f0cf1354fa 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -43,4 +43,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 5ef04adf9c..5dccd16741 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -43,4 +43,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index c9724ab550..f85935cbcc 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -43,4 +43,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index 544c4ced42..40563abe44 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -42,4 +42,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index c7c0639b97..f89ca57989 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -46,4 +46,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index 22e318e7af..617a9303ae 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -44,4 +44,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index d0d91ec545..6aa51f6ae1 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -42,4 +42,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index 68fece6e9b..764ea6691f 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -51,4 +51,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index b63bcdea48..894a009733 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -53,4 +53,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 5cd55dbd6b..ee732af357 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -48,4 +48,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index 627c62b76e..6b504aea20 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -44,4 +44,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index d68760f968..e23459cbbe 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -44,4 +44,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 3653ab8c69..a66890f95a 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -42,4 +42,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 386c1f76a5..35e49aa3f7 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -44,4 +44,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index e591631b4c..e12eac3efd 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -42,4 +42,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index e17fedf935..86dd4a4adb 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -41,4 +41,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 15-Jun-2023 +###### Auto generated by spf13/cobra on 16-Jun-2023 From 17931d179e1867f9e8c237460cf2414450fec0cb Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 08:30:35 +0800 Subject: [PATCH 1018/1392] bump version to v1.48.1 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index e09709ad76..ab09d260bc 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.0-50778c46-dev" +const Version = "v1.48.1-a7b20518-dev" -const VersionGitRef = "50778c46" +const VersionGitRef = "a7b20518" diff --git a/pkg/version/version.go b/pkg/version/version.go index 7923e029fd..75d7bd44a7 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.0-50778c46" +const Version = "v1.48.1-a7b20518" -const VersionGitRef = "50778c46" +const VersionGitRef = "a7b20518" From d04bbc32a90a2715ec808934ea9f19296b3a0141 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 08:30:35 +0800 Subject: [PATCH 1019/1392] add v1.48.1 release note --- doc/release/v1.48.1.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 doc/release/v1.48.1.md diff --git a/doc/release/v1.48.1.md b/doc/release/v1.48.1.md new file mode 100644 index 0000000000..b148c7bbb2 --- /dev/null +++ b/doc/release/v1.48.1.md @@ -0,0 +1,2 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.48.0...main) + From e1c602c68fa3b063c086e5e6ab91648f2e09069a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 08:39:24 +0800 Subject: [PATCH 1020/1392] bump version to v1.48.1 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index ab09d260bc..db47327974 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.1-a7b20518-dev" +const Version = "v1.48.1-d04bbc32-dev" -const VersionGitRef = "a7b20518" +const VersionGitRef = "d04bbc32" diff --git a/pkg/version/version.go b/pkg/version/version.go index 75d7bd44a7..beb3e513f3 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.1-a7b20518" +const Version = "v1.48.1-d04bbc32" -const VersionGitRef = "a7b20518" +const VersionGitRef = "d04bbc32" From dc3901cc7fa9b694dc33bb8c26d2f7dcfdc73b0e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 13:03:37 +0800 Subject: [PATCH 1021/1392] xfunding: add more notificiation --- pkg/strategy/xfunding/strategy.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 99d75d9a82..454f62c932 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -33,6 +33,7 @@ const ID = "xfunding" // NoOp -> Opening // Opening -> Ready -> Closing // Closing -> Closed -> Opening +// //go:generate stringer -type=PositionState type PositionState int @@ -949,6 +950,9 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) bool { log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) + bbgo.Notify("%s funding rate %s is lower than the Low threshold %s, start closing position...", + s.Symbol, fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) + holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) if holdingPeriod < time.Duration(s.MinHoldingPeriod) { log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod.Duration()) @@ -986,6 +990,9 @@ func (s *Strategy) startClosingPosition() { } log.Infof("startClosingPosition") + + bbgo.Notify("Start to close position", s.FuturesPosition, s.SpotPosition) + s.setPositionState(PositionClosing) // reset the transfer stats From 46fecbbdeb2b28cd38ea64d4da2c2af0d8516e27 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 15:35:07 +0800 Subject: [PATCH 1022/1392] types: do not truncate quantity before adjustment --- pkg/types/market.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/types/market.go b/pkg/types/market.go index d9660c970f..bfcd0ac431 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -158,7 +158,6 @@ func (m Market) CanonicalizeVolume(val fixedpoint.Value) float64 { // AdjustQuantityByMinNotional adjusts the quantity to make the amount greater than the given minAmount func (m Market) AdjustQuantityByMinNotional(quantity, currentPrice fixedpoint.Value) fixedpoint.Value { // modify quantity for the min amount - quantity = m.TruncateQuantity(quantity) amount := currentPrice.Mul(quantity) if amount.Compare(m.MinNotional) < 0 { ratio := m.MinNotional.Div(amount) From 83609314974e543fb7e6e1eed6655323b4068966 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 13:46:20 +0800 Subject: [PATCH 1023/1392] fix test TestMarket_AdjustQuantityByMinNotional --- pkg/types/market_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/types/market_test.go b/pkg/types/market_test.go index b6fee9e8c9..7966a6d5aa 100644 --- a/pkg/types/market_test.go +++ b/pkg/types/market_test.go @@ -228,16 +228,16 @@ func TestMarket_AdjustQuantityByMinNotional(t *testing.T) { testCases := []struct { input string price fixedpoint.Value - expect string + expect fixedpoint.Value }{ - {"0.00573961", number(1750.99), "0.0058"}, - {"0.0019", number(1757.38), "0.0057"}, + {"0.00573961", number(1750.99), number("0.005739")}, + {"0.0019", number(1757.38), number("0.0057")}, } for _, testCase := range testCases { q := fixedpoint.MustNewFromString(testCase.input) q2 := market.AdjustQuantityByMinNotional(q, testCase.price) - assert.Equalf(t, testCase.expect, q2.String(), "input: %s stepSize: %s", testCase.input, market.StepSize.String()) + assert.InDelta(t, testCase.expect.Float64(), q2.Float64(), 0.000001, "input: %s stepSize: %s", testCase.input, market.StepSize.String()) assert.False(t, market.IsDustQuantity(q2, testCase.price)) } } From 2448fa6f8373f02c46fc36d1f5269edc17c01f82 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 13:46:45 +0800 Subject: [PATCH 1024/1392] scmaker: add MaxExposure option --- pkg/strategy/scmaker/strategy.go | 24 ++++++++++++++++++------ pkg/types/trade_stats_test.go | 13 +++++++++++-- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 553608e8e9..83a7bc6e61 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -46,6 +46,8 @@ type Strategy struct { LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` LiquidityLayerTickSize fixedpoint.Value `json:"liquidityLayerTickSize"` + MaxExposure fixedpoint.Value `json:"maxExposure"` + MinProfit fixedpoint.Value `json:"minProfit"` Position *types.Position `json:"position,omitempty" persistence:"position"` @@ -264,10 +266,6 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { midPriceEMA := s.ewma.Last(0) midPrice := fixedpoint.NewFromFloat(midPriceEMA) - makerQuota := &bbgo.QuotaTransaction{} - makerQuota.QuoteAsset.Add(quoteBal.Available) - makerQuota.BaseAsset.Add(baseBal.Available) - bandWidth := s.boll.Last(0) log.Infof("spread: %f mid price ema: %f boll band width: %f", spread.Float64(), midPriceEMA, bandWidth) @@ -294,11 +292,11 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { askPrice = midPrice.Add(sp) } - if i > 0 && bidPrice.Compare(ticker.Buy) < 0 { + if i > 0 && bidPrice.Compare(ticker.Buy) > 0 { bidPrice = ticker.Buy.Sub(sp) } - if i > 0 && askPrice.Compare(ticker.Sell) > 0 { + if i > 0 && askPrice.Compare(ticker.Sell) < 0 { askPrice = ticker.Sell.Add(sp) } @@ -312,6 +310,20 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { availableBase := baseBal.Available availableQuote := quoteBal.Available + // check max exposure + if s.MaxExposure.Sign() > 0 { + availableQuote = fixedpoint.Min(availableQuote, s.MaxExposure) + + baseQuoteValue := availableBase.Mul(ticker.Sell) + if baseQuoteValue.Compare(s.MaxExposure) > 0 { + availableBase = s.MaxExposure.Div(ticker.Sell) + } + } + + makerQuota := &bbgo.QuotaTransaction{} + makerQuota.QuoteAsset.Add(availableQuote) + makerQuota.BaseAsset.Add(availableBase) + log.Infof("balances before liq orders: %s, %s", baseBal.String(), quoteBal.String()) diff --git a/pkg/types/trade_stats_test.go b/pkg/types/trade_stats_test.go index a543476d44..24de27cf28 100644 --- a/pkg/types/trade_stats_test.go +++ b/pkg/types/trade_stats_test.go @@ -8,8 +8,17 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) -func number(v float64) fixedpoint.Value { - return fixedpoint.NewFromFloat(v) +func number(v interface{}) fixedpoint.Value { + switch tv := v.(type) { + case float64: + return fixedpoint.NewFromFloat(tv) + + case string: + return fixedpoint.MustNewFromString(tv) + + default: + panic("invalid number input") + } } func TestTradeStats_consecutiveCounterAndAmount(t *testing.T) { From 759dce1d5a00776868667b76529941fc41a2e9d0 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 14:51:37 +0800 Subject: [PATCH 1025/1392] types: fix number() call --- pkg/types/trade_stats_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/types/trade_stats_test.go b/pkg/types/trade_stats_test.go index 24de27cf28..10ed6348a5 100644 --- a/pkg/types/trade_stats_test.go +++ b/pkg/types/trade_stats_test.go @@ -13,6 +13,11 @@ func number(v interface{}) fixedpoint.Value { case float64: return fixedpoint.NewFromFloat(tv) + case int64: + return fixedpoint.NewFromInt(tv) + case int: + return fixedpoint.NewFromInt(int64(tv)) + case string: return fixedpoint.MustNewFromString(tv) From fd77a4aae6eac32336f9a513122075485f4e0de5 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 14:57:46 +0800 Subject: [PATCH 1026/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index 83bada29f5..a124be4435 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -60,4 +60,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 9462633ce4..63b61993b3 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -43,4 +43,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 6aea00c319..762b67902c 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -52,4 +52,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index 8584613882..002aa31322 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -42,4 +42,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index 296a0f951f..c4032e6e59 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -41,4 +41,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index 6661d89e35..38969ef080 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -51,4 +51,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index 44f1564629..dc3995dee0 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -43,4 +43,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 3c3110716a..1df646874d 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -50,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 7e2da6f27b..fd443b9b99 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -44,4 +44,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index b4c1639639..bcb6bb54bd 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -47,4 +47,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 229b5a9b31..379085b4e1 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -44,4 +44,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index bbdbbc7803..c8de325fda 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -43,4 +43,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index ae9c0f1715..a206e35a8e 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -40,4 +40,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index f0cf1354fa..e2166d42ba 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -43,4 +43,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 5dccd16741..f4a8adfe03 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -43,4 +43,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index f85935cbcc..871b824932 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -43,4 +43,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index 40563abe44..50872b130d 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -42,4 +42,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index f89ca57989..add0a18887 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -46,4 +46,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index 617a9303ae..d17286d4b5 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -44,4 +44,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index 6aa51f6ae1..25f2c531ea 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -42,4 +42,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index 764ea6691f..73304dd288 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -51,4 +51,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 894a009733..62981d64bf 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -53,4 +53,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index ee732af357..12c305a3fa 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -48,4 +48,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index 6b504aea20..cc685541f4 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -44,4 +44,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index e23459cbbe..4cb9e2a2a3 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -44,4 +44,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index a66890f95a..cbe7eaa3b2 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -42,4 +42,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 35e49aa3f7..0df61681c4 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -44,4 +44,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index e12eac3efd..4bd5983ad3 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -42,4 +42,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index 86dd4a4adb..a3ce8b221d 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -41,4 +41,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 16-Jun-2023 +###### Auto generated by spf13/cobra on 19-Jun-2023 From 6a5e35c06569416443af896b473961aeb824968d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 14:57:46 +0800 Subject: [PATCH 1027/1392] bump version to v1.48.2 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index db47327974..df503acaae 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.1-d04bbc32-dev" +const Version = "v1.48.2-759dce1d-dev" -const VersionGitRef = "d04bbc32" +const VersionGitRef = "759dce1d" diff --git a/pkg/version/version.go b/pkg/version/version.go index beb3e513f3..ebdc5fa58f 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.1-d04bbc32" +const Version = "v1.48.2-759dce1d" -const VersionGitRef = "d04bbc32" +const VersionGitRef = "759dce1d" From 98a78b5224a0e138edcf19b744509a78c60c250f Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 14:57:46 +0800 Subject: [PATCH 1028/1392] add v1.48.2 release note --- doc/release/v1.48.2.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 doc/release/v1.48.2.md diff --git a/doc/release/v1.48.2.md b/doc/release/v1.48.2.md new file mode 100644 index 0000000000..f90198df4d --- /dev/null +++ b/doc/release/v1.48.2.md @@ -0,0 +1,12 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.48.1...main) + +## Fixes + +- fix number() call +- fix test + +## Strategies + +- scmaker: add MaxExposure option +- xfunding: add more notificiation + From 58a13507bc1a51c25c0f35a53084670642fd33e8 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 15:22:43 +0800 Subject: [PATCH 1029/1392] scmaker: graceful cancel orders --- pkg/strategy/scmaker/strategy.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 83a7bc6e61..d166a2154a 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "sync" log "github.com/sirupsen/logrus" @@ -152,6 +153,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + err := s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) + logErr(err, "unable to cancel liquidity orders") + + err = s.adjustmentOrderBook.GracefulCancel(ctx, s.session.Exchange) + logErr(err, "unable to cancel adjustment orders") + }) + return nil } From f579fc7d93d37ff7e03d634ec4d56b84450003c8 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 15:25:10 +0800 Subject: [PATCH 1030/1392] scmaker: call cancel api before starting up --- pkg/strategy/scmaker/strategy.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index d166a2154a..0b0a3718f2 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -18,6 +18,11 @@ const ID = "scmaker" var ten = fixedpoint.NewFromInt(10) +type advancedOrderCancelApi interface { + CancelAllOrders(ctx context.Context) ([]types.Order, error) + CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error) +} + type BollingerConfig struct { Interval types.Interval `json:"interval"` Window int `json:"window"` @@ -129,6 +134,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return err } + if cancelApi, ok := session.Exchange.(advancedOrderCancelApi); ok { + _, _ = cancelApi.CancelOrdersBySymbol(ctx, s.Symbol) + } + s.liquidityScale = scale s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) From 1f3a13808b08ced3e259b5a4570c710b0069c3f2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 15:26:15 +0800 Subject: [PATCH 1031/1392] bump version to v1.48.3 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index df503acaae..53fe3f0444 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.2-759dce1d-dev" +const Version = "v1.48.3-f579fc7d-dev" -const VersionGitRef = "759dce1d" +const VersionGitRef = "f579fc7d" diff --git a/pkg/version/version.go b/pkg/version/version.go index ebdc5fa58f..25cc39db6d 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.2-759dce1d" +const Version = "v1.48.3-f579fc7d" -const VersionGitRef = "759dce1d" +const VersionGitRef = "f579fc7d" From 14d9b99dafc5c6264b5c734ed10c2dcdbc59985c Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 15:26:15 +0800 Subject: [PATCH 1032/1392] add v1.48.3 release note --- doc/release/v1.48.3.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/release/v1.48.3.md diff --git a/doc/release/v1.48.3.md b/doc/release/v1.48.3.md new file mode 100644 index 0000000000..b48db13826 --- /dev/null +++ b/doc/release/v1.48.3.md @@ -0,0 +1,6 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.48.2...main) + +## Fixes + +- scmaker: graceful shutdown should cancel all orders +- scmaker: clear orders when start up From 55b84134722749e2383588abe96b7b2241c4acca Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 15:38:55 +0800 Subject: [PATCH 1033/1392] scmaker: when user data stream is ready, place liquidity orders --- pkg/strategy/scmaker/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 0b0a3718f2..83c1d34d69 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -152,6 +152,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.initializePriceRangeBollinger(session) s.initializeIntensityIndicator(session) + session.UserDataStream.OnStart(func() { + s.placeLiquidityOrders(ctx) + }) + session.MarketDataStream.OnKLineClosed(func(k types.KLine) { if k.Interval == s.AdjustmentUpdateInterval { s.placeAdjustmentOrders(ctx) From 9b8c2b5ba40998c715464d37e4b862ecf5894742 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 15:39:34 +0800 Subject: [PATCH 1034/1392] bump version to v1.48.3 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 53fe3f0444..24ef5ecf99 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.3-f579fc7d-dev" +const Version = "v1.48.3-55b84134-dev" -const VersionGitRef = "f579fc7d" +const VersionGitRef = "55b84134" diff --git a/pkg/version/version.go b/pkg/version/version.go index 25cc39db6d..06bb1d0010 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.3-f579fc7d" +const Version = "v1.48.3-55b84134" -const VersionGitRef = "f579fc7d" +const VersionGitRef = "55b84134" From de00e5fa88e2d68173b88421f49acab92e251286 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 17:03:38 +0800 Subject: [PATCH 1035/1392] scmaker: preload indicators --- pkg/strategy/scmaker/strategy.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 83c1d34d69..5248a6a78c 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -179,20 +179,36 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } +func (s *Strategy) preloadKLines(inc *indicator.KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) { + if store, ok := session.MarketDataStore(symbol); ok { + if kLinesData, ok := store.KLinesOfInterval(interval); ok { + for _, k := range *kLinesData { + inc.EmitUpdate(k) + } + } + } +} + func (s *Strategy) initializeMidPriceEMA(session *bbgo.ExchangeSession) { kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.MidPriceEMA.Interval) s.ewma = indicator.EWMA2(indicator.ClosePrices(kLines), s.MidPriceEMA.Window) + + s.preloadKLines(kLines, session, s.Symbol, s.MidPriceEMA.Interval) } func (s *Strategy) initializeIntensityIndicator(session *bbgo.ExchangeSession) { kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.StrengthInterval) s.intensity = Intensity(kLines, 10) + + s.preloadKLines(kLines, session, s.Symbol, s.StrengthInterval) } func (s *Strategy) initializePriceRangeBollinger(session *bbgo.ExchangeSession) { kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.PriceRangeBollinger.Interval) closePrices := indicator.ClosePrices(kLines) s.boll = indicator.BOLL2(closePrices, s.PriceRangeBollinger.Window, s.PriceRangeBollinger.K) + + s.preloadKLines(kLines, session, s.Symbol, s.PriceRangeBollinger.Interval) } func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { From 833d9428338d2f25089b14ccedc7c858c87b7313 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 17:05:42 +0800 Subject: [PATCH 1036/1392] bump version to v1.48.4 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 24ef5ecf99..ea68145e62 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.3-55b84134-dev" +const Version = "v1.48.4-de00e5fa-dev" -const VersionGitRef = "55b84134" +const VersionGitRef = "de00e5fa" diff --git a/pkg/version/version.go b/pkg/version/version.go index 06bb1d0010..e9e0ca09c4 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.3-55b84134" +const Version = "v1.48.4-de00e5fa" -const VersionGitRef = "55b84134" +const VersionGitRef = "de00e5fa" From 91a2c7255cd1cedbc9208c2d61b9223eae1374ae Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 17:06:09 +0800 Subject: [PATCH 1037/1392] bump version to v1.48.4 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index ea68145e62..1b8a39071b 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.4-de00e5fa-dev" +const Version = "v1.48.4-833d9428-dev" -const VersionGitRef = "de00e5fa" +const VersionGitRef = "833d9428" diff --git a/pkg/version/version.go b/pkg/version/version.go index e9e0ca09c4..cb2f34d948 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.4-de00e5fa" +const Version = "v1.48.4-833d9428" -const VersionGitRef = "de00e5fa" +const VersionGitRef = "833d9428" From 0d2afef2762126e967753e2891009e096f2c11c4 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 19 Jun 2023 17:06:09 +0800 Subject: [PATCH 1038/1392] add v1.48.4 release note --- doc/release/v1.48.4.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 doc/release/v1.48.4.md diff --git a/doc/release/v1.48.4.md b/doc/release/v1.48.4.md new file mode 100644 index 0000000000..efc920adfb --- /dev/null +++ b/doc/release/v1.48.4.md @@ -0,0 +1,5 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.48.3...main) + +## Fixes + +- scmaker: preload indicator history klines From 935eaf94798fabcb86da6bd126d08955b6c1d5e3 Mon Sep 17 00:00:00 2001 From: chechia Date: Mon, 19 Jun 2023 20:34:56 +0800 Subject: [PATCH 1039/1392] FEATURE: allow additional labels --- charts/bbgo/templates/_helpers.tpl | 3 +++ charts/bbgo/values.yaml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/charts/bbgo/templates/_helpers.tpl b/charts/bbgo/templates/_helpers.tpl index 7060adbd88..6b9829c584 100644 --- a/charts/bbgo/templates/_helpers.tpl +++ b/charts/bbgo/templates/_helpers.tpl @@ -36,6 +36,9 @@ Common labels {{- define "bbgo.labels" -}} helm.sh/chart: {{ include "bbgo.chart" . }} {{ include "bbgo.selectorLabels" . }} +{{- if .Values.addtionalLabels }} +{{ toYaml .Values.additionalLabels }} +{{- end }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} diff --git a/charts/bbgo/values.yaml b/charts/bbgo/values.yaml index 179c317773..1fe20f5386 100644 --- a/charts/bbgo/values.yaml +++ b/charts/bbgo/values.yaml @@ -37,6 +37,8 @@ serviceAccount: # If not set and create is true, a name is generated using the fullname template name: "" +additionalLabels: {} + podAnnotations: {} podSecurityContext: {} From b83968b7d8e8093e31e2e5223bd92907f151fc78 Mon Sep 17 00:00:00 2001 From: chechia Date: Tue, 20 Jun 2023 09:36:32 +0800 Subject: [PATCH 1040/1392] CHORE: release helm chart --- charts/bbgo/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/bbgo/Chart.yaml b/charts/bbgo/Chart.yaml index 85c81631fb..fc8cf7ec74 100644 --- a/charts/bbgo/Chart.yaml +++ b/charts/bbgo/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.3.0 +version: 0.3.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to From f139da771d411a50eadd1f811c5cdd1178c74636 Mon Sep 17 00:00:00 2001 From: chechia Date: Tue, 20 Jun 2023 15:54:25 +0800 Subject: [PATCH 1041/1392] FIX: typo in helm chart additionalLabels --- charts/bbgo/Chart.yaml | 2 +- charts/bbgo/templates/_helpers.tpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/bbgo/Chart.yaml b/charts/bbgo/Chart.yaml index fc8cf7ec74..2d682eaa6c 100644 --- a/charts/bbgo/Chart.yaml +++ b/charts/bbgo/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.3.1 +version: 0.3.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/bbgo/templates/_helpers.tpl b/charts/bbgo/templates/_helpers.tpl index 6b9829c584..afb6d1b07d 100644 --- a/charts/bbgo/templates/_helpers.tpl +++ b/charts/bbgo/templates/_helpers.tpl @@ -36,7 +36,7 @@ Common labels {{- define "bbgo.labels" -}} helm.sh/chart: {{ include "bbgo.chart" . }} {{ include "bbgo.selectorLabels" . }} -{{- if .Values.addtionalLabels }} +{{- if .Values.additionalLabels }} {{ toYaml .Values.additionalLabels }} {{- end }} {{- if .Chart.AppVersion }} From d4cf39430e55cc66296d59ade73ea7979309a8d2 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 20 Jun 2023 17:18:15 +0800 Subject: [PATCH 1042/1392] xgap: fix group id range --- pkg/strategy/xgap/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xgap/strategy.go b/pkg/strategy/xgap/strategy.go index efa8b6445b..c7d0546fd9 100644 --- a/pkg/strategy/xgap/strategy.go +++ b/pkg/strategy/xgap/strategy.go @@ -210,7 +210,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.tradingSession.UserDataStream.OnTradeUpdate(s.handleTradeUpdate) instanceID := fmt.Sprintf("%s-%s", ID, s.Symbol) - s.groupID = util.FNV32(instanceID) + s.groupID = util.FNV32(instanceID) % math.MaxInt32 log.Infof("using group id %d from fnv32(%s)", s.groupID, instanceID) go func() { From e65f9d00cc47327d62749225262cb5bd9bce7d4a Mon Sep 17 00:00:00 2001 From: chechia Date: Wed, 21 Jun 2023 10:20:53 +0800 Subject: [PATCH 1043/1392] FEATURE: pass deployment labels to pod --- charts/bbgo/Chart.yaml | 2 +- charts/bbgo/templates/deployment.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/charts/bbgo/Chart.yaml b/charts/bbgo/Chart.yaml index 2d682eaa6c..2542e2f62d 100644 --- a/charts/bbgo/Chart.yaml +++ b/charts/bbgo/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.3.2 +version: 0.3.3 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/bbgo/templates/deployment.yaml b/charts/bbgo/templates/deployment.yaml index c62d2e4160..2f90f781a8 100644 --- a/charts/bbgo/templates/deployment.yaml +++ b/charts/bbgo/templates/deployment.yaml @@ -22,6 +22,7 @@ spec: {{- end }} labels: {{- include "bbgo.selectorLabels" . | nindent 8 }} + {{- include "bbgo.labels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: From 508f2fbe1e2a3c91d0610f7222af08f36e1bff87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=BF?= <4680567+narumiruna@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:01:27 +0800 Subject: [PATCH 1044/1392] fix typo in xalign config --- config/xalign.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/xalign.yaml b/config/xalign.yaml index ce68ccb20c..51956f74f4 100644 --- a/config/xalign.yaml +++ b/config/xalign.yaml @@ -35,7 +35,7 @@ crossExchangeStrategies: - binance ## quoteCurrencies config specifies which quote currency should be used for BUY order or SELL order. - ## when specifying [USDC,TWD] for "BUY", then it will consider BTCUSDT first then BTCTWD second. + ## when specifying [USDC,TWD] for "BUY", then it will consider BTCUSDC first then BTCTWD second. quoteCurrencies: buy: [USDC, TWD] sell: [USDT] From 76884a4ddf193d1d3caad5ef11ef794f4140b9ea Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 21 Jun 2023 15:56:59 +0800 Subject: [PATCH 1045/1392] xalign: add balance fault tolerance --- pkg/strategy/xalign/strategy.go | 51 +++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index d50c9f1142..fac24476ec 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -21,6 +21,12 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +type TimeBalance struct { + types.Balance + + Time time.Time +} + type QuoteCurrencyPreference struct { Buy []string `json:"buy"` Sell []string `json:"sell"` @@ -34,6 +40,10 @@ type Strategy struct { ExpectedBalances map[string]fixedpoint.Value `json:"expectedBalances"` UseTakerOrder bool `json:"useTakerOrder"` DryRun bool `json:"dryRun"` + BalanceToleranceRange float64 `json:"balanceToleranceRange"` + Duration types.Duration `json:"for"` + + faultBalanceRecords map[string][]TimeBalance sessions map[string]*bbgo.ExchangeSession orderBooks map[string]*bbgo.ActiveOrderBook @@ -63,6 +73,11 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { } +func (s *Strategy) Defaults() error { + s.BalanceToleranceRange = 0.01 + return nil +} + func (s *Strategy) Validate() error { if s.PreferredQuoteCurrencies == nil { return errors.New("quoteCurrencies is not defined") @@ -221,6 +236,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se instanceID := s.InstanceID() _ = instanceID + s.faultBalanceRecords = make(map[string][]TimeBalance) s.sessions = make(map[string]*bbgo.ExchangeSession) s.orderBooks = make(map[string]*bbgo.ActiveOrderBook) @@ -271,9 +287,25 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se return nil } +func (s *Strategy) recordBalance(totalBalances types.BalanceMap) { + now := time.Now() + for currency, expectedBalance := range s.ExpectedBalances { + q := s.calculateRefillQuantity(totalBalances, currency, expectedBalance) + rf := q.Div(expectedBalance).Abs().Float64() + if rf > s.BalanceToleranceRange { + balance := totalBalances[currency] + s.faultBalanceRecords[currency] = append(s.faultBalanceRecords[currency], TimeBalance{ + Time: now, + Balance: balance, + }) + } else { + // reset counter + s.faultBalanceRecords[currency] = nil + } + } +} + func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) { - totalBalances, sessionBalances := s.aggregateBalances(ctx, sessions) - _ = sessionBalances for sessionName, session := range sessions { ob, ok := s.orderBooks[sessionName] @@ -288,9 +320,24 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange } } + totalBalances, sessionBalances := s.aggregateBalances(ctx, sessions) + _ = sessionBalances + + s.recordBalance(totalBalances) + for currency, expectedBalance := range s.ExpectedBalances { q := s.calculateRefillQuantity(totalBalances, currency, expectedBalance) + if s.Duration > 0 { + log.Infof("checking fault balance records...") + if faultBalance, ok := s.faultBalanceRecords[currency]; ok && len(faultBalance) > 0 { + if time.Since(faultBalance[0].Time) < s.Duration.Duration() { + log.Infof("%s fault record since: %s < persistence period %s", currency, faultBalance[0].Time, s.Duration.Duration()) + continue + } + } + } + selectedSession, submitOrder := s.selectSessionForCurrency(ctx, sessions, currency, q) if selectedSession != nil && submitOrder != nil { log.Infof("placing order on %s: %+v", selectedSession.Name, submitOrder) From f6128b9bdc044b88a165c0cd276994af2f62c54b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 21 Jun 2023 15:59:15 +0800 Subject: [PATCH 1046/1392] xalign: support percentage string --- pkg/strategy/xalign/strategy.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index fac24476ec..5f19ca938b 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -40,7 +40,7 @@ type Strategy struct { ExpectedBalances map[string]fixedpoint.Value `json:"expectedBalances"` UseTakerOrder bool `json:"useTakerOrder"` DryRun bool `json:"dryRun"` - BalanceToleranceRange float64 `json:"balanceToleranceRange"` + BalanceToleranceRange fixedpoint.Value `json:"balanceToleranceRange"` Duration types.Duration `json:"for"` faultBalanceRecords map[string][]TimeBalance @@ -74,7 +74,7 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { } func (s *Strategy) Defaults() error { - s.BalanceToleranceRange = 0.01 + s.BalanceToleranceRange = fixedpoint.NewFromFloat(0.01) return nil } @@ -292,7 +292,8 @@ func (s *Strategy) recordBalance(totalBalances types.BalanceMap) { for currency, expectedBalance := range s.ExpectedBalances { q := s.calculateRefillQuantity(totalBalances, currency, expectedBalance) rf := q.Div(expectedBalance).Abs().Float64() - if rf > s.BalanceToleranceRange { + tr := s.BalanceToleranceRange.Float64() + if rf > tr { balance := totalBalances[currency] s.faultBalanceRecords[currency] = append(s.faultBalanceRecords[currency], TimeBalance{ Time: now, From c802fae21147b2a2abf041207ea4644a2705dfa8 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 21 Jun 2023 17:36:09 +0800 Subject: [PATCH 1047/1392] xalign: add logger --- pkg/strategy/xalign/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 5f19ca938b..ff7a16f1d3 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -8,7 +8,7 @@ import ( "sync" "time" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -17,6 +17,8 @@ import ( const ID = "xalign" +var log = logrus.WithField("strategy", ID) + func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } From 8e64b5293e45539cd195bf02421645e924143b55 Mon Sep 17 00:00:00 2001 From: gx578007 Date: Fri, 23 Jun 2023 21:30:32 +0800 Subject: [PATCH 1048/1392] MINOR: [grid2] delete order prices metric --- pkg/strategy/grid2/metrics.go | 15 ------------- pkg/strategy/grid2/recover.go | 1 - pkg/strategy/grid2/strategy.go | 41 ---------------------------------- 3 files changed, 57 deletions(-) diff --git a/pkg/strategy/grid2/metrics.go b/pkg/strategy/grid2/metrics.go index ed4f849cd1..b72bb4b3f0 100644 --- a/pkg/strategy/grid2/metrics.go +++ b/pkg/strategy/grid2/metrics.go @@ -10,7 +10,6 @@ var ( metricsGridNumOfOrdersWithCorrectPrice *prometheus.GaugeVec metricsGridNumOfMissingOrders *prometheus.GaugeVec metricsGridNumOfMissingOrdersWithCorrectPrice *prometheus.GaugeVec - metricsGridOrderPrices *prometheus.GaugeVec metricsGridProfit *prometheus.GaugeVec metricsGridUpperPrice *prometheus.GaugeVec @@ -102,19 +101,6 @@ func initMetrics(extendedLabels []string) { }, extendedLabels...), ) - metricsGridOrderPrices = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "bbgo_grid2_order_prices", - Help: "order prices", - }, - append([]string{ - "exchange", // exchange name - "symbol", // symbol of the market - "ith", - "side", - }, extendedLabels...), - ) - metricsGridProfit = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "bbgo_grid2_profit", @@ -202,7 +188,6 @@ func registerMetrics() { metricsGridNumOfMissingOrders, metricsGridNumOfMissingOrdersWithCorrectPrice, metricsGridProfit, - metricsGridOrderPrices, metricsGridLowerPrice, metricsGridUpperPrice, metricsGridQuoteInvestment, diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index d0ce99f701..d48eab1bba 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -16,7 +16,6 @@ import ( func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { defer func() { s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) }() historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e007369cec..044b3a5a4c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -894,7 +894,6 @@ func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.Exch bbgo.Sync(ctx, s) s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) } } @@ -1170,7 +1169,6 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) s.logger.Infof("ALL GRID ORDERS SUBMITTED") s.updateGridNumOfOrdersMetrics(grid) - s.updateOpenOrderPricesMetrics(createdOrders) return nil } @@ -1219,41 +1217,6 @@ func (s *Strategy) updateGridNumOfOrdersMetrics(grid *Grid) { } } -func (s *Strategy) updateOpenOrderPricesMetrics(orders []types.Order) { - orders = sortOrdersByPriceAscending(orders) - num := len(orders) - s.deleteOpenOrderPricesMetrics() - for idx, order := range orders { - labels := s.newPrometheusLabels() - labels["side"] = order.Side.String() - labels["ith"] = strconv.Itoa(num - idx) - metricsGridOrderPrices.With(labels).Set(order.Price.Float64()) - } -} - -func (s *Strategy) deleteOpenOrderPricesMetrics() { - for i := 1; i <= int(s.GridNum); i++ { - ithStr := strconv.Itoa(i) - labels := s.newPrometheusLabels() - labels["side"] = "BUY" - labels["ith"] = ithStr - metricsGridOrderPrices.Delete(labels) - labels = s.newPrometheusLabels() - labels["side"] = "SELL" - labels["ith"] = ithStr - metricsGridOrderPrices.Delete(labels) - } -} - -func sortOrdersByPriceAscending(orders []types.Order) []types.Order { - sort.Slice(orders, func(i, j int) bool { - a := orders[i] - b := orders[j] - return a.Price.Compare(b.Price) < 0 - }) - return orders -} - func (s *Strategy) debugGridOrders(submitOrders []types.SubmitOrder, lastPrice fixedpoint.Value) { if !s.Debug { return @@ -1538,7 +1501,6 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.setGrid(grid) s.EmitGridReady() s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } else { s.logger.Infof("GRID RECOVER: found missing prices: %v", missingPrices) @@ -1585,7 +1547,6 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.setGrid(grid) s.EmitGridReady() s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } @@ -1612,7 +1573,6 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService s.setGrid(grid) s.EmitGridReady() s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) for i := range filledOrders { // avoid using the iterator @@ -1629,7 +1589,6 @@ func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } From e8fe8082ccd31d97dbe6613d5e705b8655cce387 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 27 Jun 2023 16:17:00 +0800 Subject: [PATCH 1049/1392] cmd: remove ftx options --- pkg/cmd/root.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 3253bea76e..83278ec699 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -183,9 +183,6 @@ func init() { RootCmd.PersistentFlags().String("max-api-key", "", "max api key") RootCmd.PersistentFlags().String("max-api-secret", "", "max api secret") - RootCmd.PersistentFlags().String("ftx-api-key", "", "ftx api key") - RootCmd.PersistentFlags().String("ftx-api-secret", "", "ftx api secret") - RootCmd.PersistentFlags().String("ftx-subaccount", "", "subaccount name. Specify it if the credential is for subaccount.") RootCmd.PersistentFlags().String("cpu-profile", "", "cpu profile") viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) From 37da9dee0efd2e99116dd1a3247b61b7572946ae Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 27 Jun 2023 16:30:46 +0800 Subject: [PATCH 1050/1392] cmd: add log formatter option and refactor the logrus setup code --- pkg/cmd/root.go | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 83278ec699..6168b84cfb 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -51,6 +51,30 @@ var RootCmd = &cobra.Command{ env = "development" } + logFormatter, err := cmd.PersistentFlags().GetString("log-formatter") + if err != nil { + return err + } + + if len(logFormatter) == 0 { + switch env { + case "production", "prod", "stag", "staging": + // always use json formatter for production and staging + log.SetFormatter(&log.JSONFormatter{}) + default: + log.SetFormatter(&prefixed.TextFormatter{}) + } + } else { + switch logFormatter { + case "prefixed": + log.SetFormatter(&prefixed.TextFormatter{}) + case "text": + log.SetFormatter(&log.TextFormatter{}) + case "json": + log.SetFormatter(&log.JSONFormatter{}) + } + } + if token := viper.GetString("rollbar-token"); token != "" { log.Infof("found rollbar token %q, setting up rollbar hook...", util.MaskKey(token)) @@ -165,6 +189,8 @@ func init() { RootCmd.PersistentFlags().String("config", "bbgo.yaml", "config file") + RootCmd.PersistentFlags().String("log-formatter", "", "configure log formatter") + RootCmd.PersistentFlags().String("rollbar-token", "", "rollbar token") // A flag can be 'persistent' meaning that this flag will be available to @@ -211,10 +237,6 @@ func init() { return } - log.SetFormatter(&prefixed.TextFormatter{}) -} - -func Execute() { environment := os.Getenv("BBGO_ENV") logDir := "log" switch environment { @@ -231,8 +253,8 @@ func Execute() { if err != nil { log.Panic(err) } - logger := log.StandardLogger() - logger.AddHook( + + log.AddHook( lfshook.NewHook( lfshook.WriterMap{ log.DebugLevel: writer, @@ -240,12 +262,13 @@ func Execute() { log.WarnLevel: writer, log.ErrorLevel: writer, log.FatalLevel: writer, - }, - &log.JSONFormatter{}, - ), + }, &log.JSONFormatter{}), ) + } +} +func Execute() { if err := RootCmd.Execute(); err != nil { log.WithError(err).Fatalf("cannot execute command") } From 02fa4d822a6dcba132ad3d6b73b419dc36edf2cd Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 27 Jun 2023 16:32:46 +0800 Subject: [PATCH 1051/1392] cmd: fix persistent flags method call --- pkg/cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 6168b84cfb..67ccd6bba8 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -51,7 +51,7 @@ var RootCmd = &cobra.Command{ env = "development" } - logFormatter, err := cmd.PersistentFlags().GetString("log-formatter") + logFormatter, err := cmd.Flags().GetString("log-formatter") if err != nil { return err } From 4bc41bad9da2919d440204e0c5f44efc60de5b44 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 21 Jun 2023 17:53:19 +0800 Subject: [PATCH 1052/1392] bbgo: improve ProtectiveStopLoss notification message --- pkg/bbgo/exit_protective_stop_loss.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/bbgo/exit_protective_stop_loss.go b/pkg/bbgo/exit_protective_stop_loss.go index 73a743cd38..46b57b08ba 100644 --- a/pkg/bbgo/exit_protective_stop_loss.go +++ b/pkg/bbgo/exit_protective_stop_loss.go @@ -170,8 +170,12 @@ func (s *ProtectiveStopLoss) handleChange(ctx context.Context, position *types.P s.stopLossPrice = position.AverageCost.Mul(one.Add(s.StopLossRatio)) } - Notify("[ProtectiveStopLoss] %s protection stop loss activated, current price = %f, average cost = %f, stop loss price = %f", - position.Symbol, closePrice.Float64(), position.AverageCost.Float64(), s.stopLossPrice.Float64()) + Notify("[ProtectiveStopLoss] %s protection (%s) stop loss activated, SL = %f, currentPrice = %f, averageCost = %f", + position.Symbol, + s.StopLossRatio.Percentage(), + s.stopLossPrice.Float64(), + closePrice.Float64(), + position.AverageCost.Float64()) if s.PlaceStopOrder { if err := s.placeStopOrder(ctx, position, orderExecutor); err != nil { @@ -195,7 +199,11 @@ func (s *ProtectiveStopLoss) checkStopPrice(closePrice fixedpoint.Value, positio } if s.shouldStop(closePrice, position) { - Notify("[ProtectiveStopLoss] protection stop order is triggered at price %f", closePrice.Float64(), position) + Notify("[ProtectiveStopLoss] %s protection stop (%s) is triggered at price %f", + s.Symbol, + s.StopLossRatio.Percentage(), + closePrice.Float64(), + position) if err := s.orderExecutor.ClosePosition(context.Background(), one, "protectiveStopLoss"); err != nil { log.WithError(err).Errorf("failed to close position") } From fdf2a91604cbeb4be714c62e9b9a2cd3472d80e5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 21 Jun 2023 17:56:40 +0800 Subject: [PATCH 1053/1392] bbgo: enable enableMarketTradeStop --- pkg/bbgo/exit_protective_stop_loss.go | 2 +- pkg/bbgo/exit_roi_stop_loss.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/exit_protective_stop_loss.go b/pkg/bbgo/exit_protective_stop_loss.go index 46b57b08ba..456fc50c6e 100644 --- a/pkg/bbgo/exit_protective_stop_loss.go +++ b/pkg/bbgo/exit_protective_stop_loss.go @@ -9,7 +9,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -const enableMarketTradeStop = false +const enableMarketTradeStop = true // ProtectiveStopLoss provides a way to protect your profit but also keep a room for the price volatility // Set ActivationRatio to 1% means if the price is away from your average cost by 1%, we will activate the protective stop loss diff --git a/pkg/bbgo/exit_roi_stop_loss.go b/pkg/bbgo/exit_roi_stop_loss.go index b19832a08d..cb1c884f1c 100644 --- a/pkg/bbgo/exit_roi_stop_loss.go +++ b/pkg/bbgo/exit_roi_stop_loss.go @@ -50,7 +50,7 @@ func (s *RoiStopLoss) checkStopPrice(closePrice fixedpoint.Value, position *type // logrus.Debugf("ROIStopLoss: price=%f roi=%s stop=%s", closePrice.Float64(), roi.Percentage(), s.Percentage.Neg().Percentage()) if roi.Compare(s.Percentage.Neg()) < 0 { // stop loss - Notify("[RoiStopLoss] %s stop loss triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Neg().Percentage(), closePrice.Float64()) + Notify("[RoiStopLoss] %s stop loss triggered by ROI %s/%s, currentPrice = %f", position.Symbol, roi.Percentage(), s.Percentage.Neg().Percentage(), closePrice.Float64()) if s.CancelActiveOrders { _ = s.orderExecutor.GracefulCancel(context.Background()) } From ac1b5aa0e200e96ab91f0fff0fdb84b5869072fb Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 21 Jun 2023 17:58:18 +0800 Subject: [PATCH 1054/1392] bbgo: trigger price check when kline is updated (not just closed) --- pkg/bbgo/exit_protective_stop_loss.go | 7 +++++-- pkg/bbgo/exit_roi_stop_loss.go | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/exit_protective_stop_loss.go b/pkg/bbgo/exit_protective_stop_loss.go index 456fc50c6e..8a741dd6da 100644 --- a/pkg/bbgo/exit_protective_stop_loss.go +++ b/pkg/bbgo/exit_protective_stop_loss.go @@ -124,14 +124,17 @@ func (s *ProtectiveStopLoss) Bind(session *ExchangeSession, orderExecutor *Gener }) position := orderExecutor.Position() - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + + f := func(kline types.KLine) { isPositionOpened := !position.IsClosed() && !position.IsDust(kline.Close) if isPositionOpened { s.handleChange(context.Background(), position, kline.Close, s.orderExecutor) } else { s.stopLossPrice = fixedpoint.Zero } - })) + } + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, f)) + session.MarketDataStream.OnKLine(types.KLineWith(s.Symbol, types.Interval1m, f)) if !IsBackTesting && enableMarketTradeStop { session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { diff --git a/pkg/bbgo/exit_roi_stop_loss.go b/pkg/bbgo/exit_roi_stop_loss.go index cb1c884f1c..bdcf6e4809 100644 --- a/pkg/bbgo/exit_roi_stop_loss.go +++ b/pkg/bbgo/exit_roi_stop_loss.go @@ -26,9 +26,12 @@ func (s *RoiStopLoss) Bind(session *ExchangeSession, orderExecutor *GeneralOrder s.orderExecutor = orderExecutor position := orderExecutor.Position() - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + f := func(kline types.KLine) { s.checkStopPrice(kline.Close, position) - })) + } + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, f)) + session.MarketDataStream.OnKLine(types.KLineWith(s.Symbol, types.Interval1m, f)) if !IsBackTesting && enableMarketTradeStop { session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { From 5afd23b5c732c4c947598d29a4b6d86bff7328c3 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 21 Jun 2023 17:59:28 +0800 Subject: [PATCH 1055/1392] bbgo: trigger trailingStop when kline is updated --- pkg/bbgo/exit_trailing_stop.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/exit_trailing_stop.go b/pkg/bbgo/exit_trailing_stop.go index 672f2606df..f0db8892b5 100644 --- a/pkg/bbgo/exit_trailing_stop.go +++ b/pkg/bbgo/exit_trailing_stop.go @@ -52,11 +52,14 @@ func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrd s.latestHigh = fixedpoint.Zero position := orderExecutor.Position() - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + f := func(kline types.KLine) { if err := s.checkStopPrice(kline.Close, position); err != nil { log.WithError(err).Errorf("error") } - })) + } + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, f)) + session.MarketDataStream.OnKLine(types.KLineWith(s.Symbol, s.Interval, f)) if !IsBackTesting && enableMarketTradeStop { session.MarketDataStream.OnMarketTrade(types.TradeWith(position.Symbol, func(trade types.Trade) { From b5f2f57678aa429ca22644cb7586af4101aa730a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 21 Jun 2023 18:01:22 +0800 Subject: [PATCH 1056/1392] bbgo: introduce ENABLE_MARKET_TRADE_STOP env var --- pkg/bbgo/exit.go | 9 +++++++++ pkg/bbgo/exit_protective_stop_loss.go | 2 -- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index aedc936d6b..802ccb55f5 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -8,8 +8,17 @@ import ( "github.com/pkg/errors" "github.com/c9s/bbgo/pkg/dynamic" + "github.com/c9s/bbgo/pkg/util" ) +var enableMarketTradeStop = true + +func init() { + if v, defined := util.GetEnvVarBool("ENABLE_MARKET_TRADE_STOP"); defined { + enableMarketTradeStop = v + } +} + type ExitMethodSet []ExitMethod func (s *ExitMethodSet) SetAndSubscribe(session *ExchangeSession, parent interface{}) { diff --git a/pkg/bbgo/exit_protective_stop_loss.go b/pkg/bbgo/exit_protective_stop_loss.go index 8a741dd6da..d2e61815aa 100644 --- a/pkg/bbgo/exit_protective_stop_loss.go +++ b/pkg/bbgo/exit_protective_stop_loss.go @@ -9,8 +9,6 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -const enableMarketTradeStop = true - // ProtectiveStopLoss provides a way to protect your profit but also keep a room for the price volatility // Set ActivationRatio to 1% means if the price is away from your average cost by 1%, we will activate the protective stop loss // and the StopLossRatio is the minimal profit ratio you want to keep for your position. From 680f6a65a4ccd1beaefd2b878ce211d1f82b7e27 Mon Sep 17 00:00:00 2001 From: chechia Date: Tue, 27 Jun 2023 17:24:33 +0800 Subject: [PATCH 1057/1392] FEATURE: helm add deployment annotation --- charts/bbgo/Chart.yaml | 2 +- charts/bbgo/templates/deployment.yaml | 4 ++++ charts/bbgo/values.yaml | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/charts/bbgo/Chart.yaml b/charts/bbgo/Chart.yaml index 2542e2f62d..f932e0c34a 100644 --- a/charts/bbgo/Chart.yaml +++ b/charts/bbgo/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.3.3 +version: 0.3.4 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/bbgo/templates/deployment.yaml b/charts/bbgo/templates/deployment.yaml index 2f90f781a8..7809e4756c 100644 --- a/charts/bbgo/templates/deployment.yaml +++ b/charts/bbgo/templates/deployment.yaml @@ -2,6 +2,10 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "bbgo.fullname" . }} + {{- with .Values.deploymentAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} labels: {{- include "bbgo.labels" . | nindent 4 }} spec: diff --git a/charts/bbgo/values.yaml b/charts/bbgo/values.yaml index 1fe20f5386..2cd8b419c6 100644 --- a/charts/bbgo/values.yaml +++ b/charts/bbgo/values.yaml @@ -39,6 +39,8 @@ serviceAccount: additionalLabels: {} +deploymentAnnotations: {} + podAnnotations: {} podSecurityContext: {} From 0360d9fa8b94812ba2a22749be5035fdce343c18 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 28 Jun 2023 18:09:10 +0800 Subject: [PATCH 1058/1392] block and query order until the market order for closing position is filled --- pkg/bbgo/order_executor_general.go | 49 ++++++++++++++++++++++-------- pkg/types/position.go | 22 ++++++++++++++ 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 80bdba8a5d..1c4d854260 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -3,8 +3,8 @@ package bbgo import ( "context" "fmt" + "strconv" "strings" - "sync/atomic" "time" "github.com/pkg/errors" @@ -14,6 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/util/backoff" ) var ErrExceededSubmitOrderRetryLimit = errors.New("exceeded submit order retry limit") @@ -42,7 +43,6 @@ type GeneralOrderExecutor struct { maxRetries uint disableNotify bool - closing int64 } func NewGeneralOrderExecutor(session *ExchangeSession, symbol, strategy, strategyInstanceID string, position *types.Position) *GeneralOrderExecutor { @@ -442,23 +442,22 @@ func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context, orders ...typ return nil } +var ErrPositionAlreadyClosing = errors.New("position is already in closing process, can't close it again") + // ClosePosition closes the current position by a percentage. // percentage 0.1 means close 10% position // tag is the order tag you want to attach, you may pass multiple tags, the tags will be combined into one tag string by commas. func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error { - submitOrder := e.position.NewMarketCloseOrder(percentage) - if submitOrder == nil { - return nil + if !e.position.SetClosing(true) { + return ErrPositionAlreadyClosing } + defer e.position.SetClosing(false) - if e.closing > 0 { - log.Errorf("position is already closing") + submitOrder := e.position.NewMarketCloseOrder(percentage) + if submitOrder == nil { return nil } - atomic.AddInt64(&e.closing, 1) - defer atomic.StoreInt64(&e.closing, 0) - if e.session.Futures { // Futures: Use base qty in e.position submitOrder.Quantity = e.position.GetBase().Abs() submitOrder.ReduceOnly = true @@ -496,8 +495,34 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix Notify("Closing %s position %s with tags: %s", e.symbol, percentage.Percentage(), tagStr) - _, err := e.SubmitOrders(ctx, *submitOrder) - return err + createdOrders, err := e.SubmitOrders(ctx, *submitOrder) + if err != nil { + return err + } + + if queryOrderService, ok := e.session.Exchange.(types.ExchangeOrderQueryService); ok { + switch submitOrder.Type { + case types.OrderTypeMarket: + _ = backoff.RetryGeneral(ctx, func() error { + order, err2 := queryOrderService.QueryOrder(ctx, types.OrderQuery{ + Symbol: e.symbol, + OrderID: strconv.FormatUint(createdOrders[0].OrderID, 10), + }) + + if err2 != nil { + return err2 + } + + if order.Status != types.OrderStatusFilled { + return errors.New("order is not filled yet") + } + + return nil + }) + } + } + + return nil } func (e *GeneralOrderExecutor) TradeCollector() *TradeCollector { diff --git a/pkg/types/position.go b/pkg/types/position.go index 6cee34e75d..5f2bf58f79 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -58,6 +58,9 @@ type Position struct { AccumulatedProfit fixedpoint.Value `json:"accumulatedProfit,omitempty" db:"accumulated_profit"` + // closing is a flag for marking this position is closing + closing bool + sync.Mutex // Modify position callbacks @@ -428,6 +431,25 @@ func (p *Position) BindStream(stream Stream) { }) } +func (p *Position) SetClosing(c bool) bool { + p.Lock() + defer p.Unlock() + + if p.closing && c { + return false + } + + p.closing = c + return true +} + +func (p *Position) IsClosing() (c bool) { + p.Lock() + c = p.closing + p.Unlock() + return c +} + func (p *Position) AddTrades(trades []Trade) (fixedpoint.Value, fixedpoint.Value, bool) { var totalProfitAmount, totalNetProfit fixedpoint.Value for _, trade := range trades { From 195ace63b06dc3d6286b2eb16836aa84f5bfa72d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 28 Jun 2023 18:11:00 +0800 Subject: [PATCH 1059/1392] check if it's in back testing mode --- pkg/bbgo/order_executor_general.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 1c4d854260..22279db78a 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -500,7 +500,7 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix return err } - if queryOrderService, ok := e.session.Exchange.(types.ExchangeOrderQueryService); ok { + if queryOrderService, ok := e.session.Exchange.(types.ExchangeOrderQueryService); ok && !IsBackTesting { switch submitOrder.Type { case types.OrderTypeMarket: _ = backoff.RetryGeneral(ctx, func() error { From 131345a762b2ff1c98bbb248cf690914e0c26b67 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 28 Jun 2023 18:13:11 +0800 Subject: [PATCH 1060/1392] types: add TestPosition_SetClosing test --- pkg/types/position_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/types/position_test.go b/pkg/types/position_test.go index 0b9b983200..6e7b6c747b 100644 --- a/pkg/types/position_test.go +++ b/pkg/types/position_test.go @@ -313,3 +313,15 @@ func TestPosition(t *testing.T) { }) } } + +func TestPosition_SetClosing(t *testing.T) { + p := NewPosition("BTCUSDT", "BTC", "USDT") + ret := p.SetClosing(true) + assert.True(t, ret) + + ret = p.SetClosing(true) + assert.False(t, ret) + + ret = p.SetClosing(false) + assert.True(t, ret) +} From b6dba18f77c54bae8b8678677512ae2475e680eb Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 10:56:07 +0800 Subject: [PATCH 1061/1392] all: move retry functions to the retry package --- pkg/bbgo/order_executor_general.go | 23 ++------- pkg/exchange/retry/order.go | 76 ++++++++++++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 51 +++----------------- 3 files changed, 88 insertions(+), 62 deletions(-) create mode 100644 pkg/exchange/retry/order.go diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 22279db78a..8f889e9470 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -3,7 +3,6 @@ package bbgo import ( "context" "fmt" - "strconv" "strings" "time" @@ -11,10 +10,10 @@ import ( log "github.com/sirupsen/logrus" "go.uber.org/multierr" + "github.com/c9s/bbgo/pkg/exchange/retry" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" - "github.com/c9s/bbgo/pkg/util/backoff" ) var ErrExceededSubmitOrderRetryLimit = errors.New("exceeded submit order retry limit") @@ -503,22 +502,10 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix if queryOrderService, ok := e.session.Exchange.(types.ExchangeOrderQueryService); ok && !IsBackTesting { switch submitOrder.Type { case types.OrderTypeMarket: - _ = backoff.RetryGeneral(ctx, func() error { - order, err2 := queryOrderService.QueryOrder(ctx, types.OrderQuery{ - Symbol: e.symbol, - OrderID: strconv.FormatUint(createdOrders[0].OrderID, 10), - }) - - if err2 != nil { - return err2 - } - - if order.Status != types.OrderStatusFilled { - return errors.New("order is not filled yet") - } - - return nil - }) + _, err2 := retry.QueryOrderUntilSuccessful(ctx, queryOrderService, createdOrders[0].Symbol, createdOrders[0].OrderID) + if err2 != nil { + log.WithError(err2).Errorf("unable to query order") + } } } diff --git a/pkg/exchange/retry/order.go b/pkg/exchange/retry/order.go new file mode 100644 index 0000000000..553aee00a1 --- /dev/null +++ b/pkg/exchange/retry/order.go @@ -0,0 +1,76 @@ +package retry + +import ( + "context" + "errors" + "strconv" + + backoff2 "github.com/cenkalti/backoff/v4" + + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util/backoff" +) + +type advancedOrderCancelService interface { + CancelAllOrders(ctx context.Context) ([]types.Order, error) + CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error) + CancelOrdersByGroupID(ctx context.Context, groupID uint32) ([]types.Order, error) +} + +func QueryOrderUntilSuccessful(ctx context.Context, queryOrderService types.ExchangeOrderQueryService, symbol string, orderId uint64) (o *types.Order, err error) { + err = backoff.RetryGeneral(ctx, func() (err2 error) { + o, err2 = queryOrderService.QueryOrder(ctx, types.OrderQuery{ + Symbol: symbol, + OrderID: strconv.FormatUint(orderId, 10), + }) + + if err2 != nil || o == nil { + return err2 + } + + if o.Status != types.OrderStatusFilled { + return errors.New("order is not filled yet") + } + + return err2 + }) + + return o, err +} + +func GeneralBackoff(ctx context.Context, op backoff2.Operation) (err error) { + err = backoff2.Retry(op, backoff2.WithContext( + backoff2.WithMaxRetries( + backoff2.NewExponentialBackOff(), + 101), + ctx)) + return err +} + +func QueryOpenOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, symbol string) (openOrders []types.Order, err error) { + var op = func() (err2 error) { + openOrders, err2 = ex.QueryOpenOrders(ctx, symbol) + return err2 + } + + err = GeneralBackoff(ctx, op) + return openOrders, err +} + +func CancelAllOrdersUntilSuccessful(ctx context.Context, service advancedOrderCancelService) error { + var op = func() (err2 error) { + _, err2 = service.CancelAllOrders(ctx) + return err2 + } + + return GeneralBackoff(ctx, op) +} + +func CancelOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, orders ...types.Order) error { + var op = func() (err2 error) { + err2 = ex.CancelOrders(ctx, orders...) + return err2 + } + + return GeneralBackoff(ctx, op) +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 044b3a5a4c..f9111c5c2a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -11,7 +11,6 @@ import ( "sync" "time" - "github.com/cenkalti/backoff/v4" "github.com/google/uuid" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" @@ -19,6 +18,7 @@ import ( "go.uber.org/multierr" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/exchange/retry" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" @@ -974,7 +974,7 @@ func (s *Strategy) cancelAll(ctx context.Context) error { for { s.logger.Infof("checking %s open orders...", s.Symbol) - openOrders, err := queryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) + openOrders, err := retry.QueryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) if err != nil { s.logger.WithError(err).Errorf("CancelOrdersByGroupID api call error") werr = multierr.Append(werr, err) @@ -987,7 +987,7 @@ func (s *Strategy) cancelAll(ctx context.Context) error { s.logger.Infof("found %d open orders left, using cancel all orders api", len(openOrders)) s.logger.Infof("using cancal all orders api for canceling grid orders...") - if err := cancelAllOrdersUntilSuccessful(ctx, service); err != nil { + if err := retry.CancelAllOrdersUntilSuccessful(ctx, service); err != nil { s.logger.WithError(err).Errorf("CancelAllOrders api call error") werr = multierr.Append(werr, err) } @@ -1393,12 +1393,12 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin func (s *Strategy) clearOpenOrders(ctx context.Context, session *bbgo.ExchangeSession) error { // clear open orders when start - openOrders, err := queryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) + openOrders, err := retry.QueryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) if err != nil { return err } - return cancelOrdersUntilSuccessful(ctx, session.Exchange, openOrders...) + return retry.CancelOrdersUntilSuccessful(ctx, session.Exchange, openOrders...) } func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.ExchangeSession) (fixedpoint.Value, error) { @@ -1996,7 +1996,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio } func (s *Strategy) recoverByScanningOrders(ctx context.Context, session *bbgo.ExchangeSession) error { - openOrders, err := queryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) + openOrders, err := retry.QueryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) if err != nil { return err } @@ -2047,7 +2047,7 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha } func (s *Strategy) cancelDuplicatedPriceOpenOrders(ctx context.Context, session *bbgo.ExchangeSession) error { - openOrders, err := queryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) + openOrders, err := retry.QueryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) if err != nil { return err } @@ -2105,40 +2105,3 @@ func (s *Strategy) newClientOrderID() string { } return "" } - -func generalBackoff(ctx context.Context, op backoff.Operation) (err error) { - err = backoff.Retry(op, backoff.WithContext( - backoff.WithMaxRetries( - backoff.NewExponentialBackOff(), - 101), - ctx)) - return err -} - -func cancelAllOrdersUntilSuccessful(ctx context.Context, service advancedOrderCancelApi) error { - var op = func() (err2 error) { - _, err2 = service.CancelAllOrders(ctx) - return err2 - } - - return generalBackoff(ctx, op) -} - -func cancelOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, orders ...types.Order) error { - var op = func() (err2 error) { - err2 = ex.CancelOrders(ctx, orders...) - return err2 - } - - return generalBackoff(ctx, op) -} - -func queryOpenOrdersUntilSuccessful(ctx context.Context, ex types.Exchange, symbol string) (openOrders []types.Order, err error) { - var op = func() (err2 error) { - openOrders, err2 = ex.QueryOpenOrders(ctx, symbol) - return err2 - } - - err = generalBackoff(ctx, op) - return openOrders, err -} From 2b65012b37c4a76411b4f2bb0c345b7610801e76 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 13:29:31 +0800 Subject: [PATCH 1062/1392] bbgo: openPosition should check if it's still closing --- pkg/bbgo/order_executor_general.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 22279db78a..c6eca7ad98 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -390,6 +390,10 @@ func (e *GeneralOrderExecutor) NewOrderFromOpenPosition(ctx context.Context, opt // @return types.OrderSlice: Created orders with information from exchange. // @return error: Error message. func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) (types.OrderSlice, error) { + if e.position.IsClosing() { + return nil, errors.Wrap(ErrPositionAlreadyClosing, "unable to open position") + } + submitOrder, err := e.NewOrderFromOpenPosition(ctx, &options) if err != nil { return nil, err @@ -442,7 +446,7 @@ func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context, orders ...typ return nil } -var ErrPositionAlreadyClosing = errors.New("position is already in closing process, can't close it again") +var ErrPositionAlreadyClosing = errors.New("position is already in closing process") // ClosePosition closes the current position by a percentage. // percentage 0.1 means close 10% position From bde5147c6d864bee72086a25c765bacdf9720a49 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 14:12:35 +0800 Subject: [PATCH 1063/1392] add CODEOWNERS file --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..0ca1873a70 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +pkg/strategy/grid2 @kbearXD @gx578007 From c6f7723620fff474c192985e57ff9ee98de5240a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 14:26:12 +0800 Subject: [PATCH 1064/1392] bbgo: rename env ENABLE_MARKET_TRADE_STOP to DISABLE_MARKET_TRADE_STOP since we've set it default to true --- pkg/bbgo/exit.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index 802ccb55f5..de8e6a07ed 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -14,8 +14,8 @@ import ( var enableMarketTradeStop = true func init() { - if v, defined := util.GetEnvVarBool("ENABLE_MARKET_TRADE_STOP"); defined { - enableMarketTradeStop = v + if v, defined := util.GetEnvVarBool("DISABLE_MARKET_TRADE_STOP"); defined && v { + enableMarketTradeStop = false } } From 9a98c4995e7a3539f95281084f569324ce2e949e Mon Sep 17 00:00:00 2001 From: randy Date: Thu, 29 Jun 2023 15:06:03 +0800 Subject: [PATCH 1065/1392] Add two risk controls for strategies: postion and circuit break. --- doc/topics/riskcontrols.md | 87 ++++++++++++++++++ pkg/risk/riskcontrol/circuit_break.go | 40 ++++++++ pkg/risk/riskcontrol/circuit_break_test.go | 79 ++++++++++++++++ pkg/risk/riskcontrol/position.go | 42 +++++++++ pkg/risk/riskcontrol/position_test.go | 92 +++++++++++++++++++ .../positionriskcontrol_callbacks.go | 18 ++++ 6 files changed, 358 insertions(+) create mode 100644 doc/topics/riskcontrols.md create mode 100644 pkg/risk/riskcontrol/circuit_break.go create mode 100644 pkg/risk/riskcontrol/circuit_break_test.go create mode 100644 pkg/risk/riskcontrol/position.go create mode 100644 pkg/risk/riskcontrol/position_test.go create mode 100644 pkg/risk/riskcontrol/positionriskcontrol_callbacks.go diff --git a/doc/topics/riskcontrols.md b/doc/topics/riskcontrols.md new file mode 100644 index 0000000000..8f548cc1a0 --- /dev/null +++ b/doc/topics/riskcontrols.md @@ -0,0 +1,87 @@ +# Risk Control +------------ + +### 1. Introduction + +Two types of risk controls for strategies is created: +- Position-limit Risk Control (pkg/risk/riskcontrol/position.go) +- Circuit-break Risk Control (pkg/risk/riskcontrol/circuit_break.go) + +### 2. Position-Limit Risk Control + +Initialization: +``` + s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.HardLimit, s.Quantity, s.orderExecutor.TradeCollector()) + s.positionRiskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Market: s.Market, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, + }) + if err != nil { + log.WithError(err).Errorf("failed to submit orders") + return + } + log.Infof("created orders: %+v", createdOrders) + }) +``` + +Strategy should provide OnReleasePosition callback, which will be called when position (positive or negative) is over hard limit. + +Modify quantity before submitting orders: + +``` + buyQuantity, sellQuantity := s.positionRiskControl.ModifiedQuantity(s.Position.Base) +``` + +It calculates buy and sell quantity shrinking by hard limit and position. + +### 3. Circuit-Break Risk Control + +Initialization +``` + s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( + s.Position, + session.StandardIndicatorSet(s.Symbol).EWMA( + types.IntervalWindow{ + Window: EWMAWindow, + Interval: types.Interval1m, + }), + s.CircuitBreakCondition, + s.ProfitStats) +``` +Should pass in position and profit states. Also need an price EWMA to calculate unrealized profit. + + +Validate parameters: +``` + if s.CircuitBreakCondition.Float64() > 0 { + return fmt.Errorf("circuitBreakCondition should be non-positive") + } + return nil +``` +Circuit break condition should be non-greater than zero. + +Check for circuit break before submitting orders: +``` + // Circuit break when accumulated losses are over break condition + if s.circuitBreakRiskControl.IsHalted() { + return + } + + submitOrders, err := s.generateSubmitOrders(ctx) + if err != nil { + log.WithError(err).Error("failed to generate submit orders") + return + } + log.Infof("submit orders: %+v", submitOrders) + + if s.DryRun { + log.Infof("dry run, not submitting orders") + return + } +``` + +Notice that if there are multiple place to submit orders, it is recommended to check in one place in Strategy.Run() and re-use that flag before submitting orders. That can avoid duplicated logs generated from IsHalted(). diff --git a/pkg/risk/riskcontrol/circuit_break.go b/pkg/risk/riskcontrol/circuit_break.go new file mode 100644 index 0000000000..db528b3a8d --- /dev/null +++ b/pkg/risk/riskcontrol/circuit_break.go @@ -0,0 +1,40 @@ +package riskcontrol + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" + log "github.com/sirupsen/logrus" +) + +type CircuitBreakRiskControl struct { + // Since price could be fluctuated large, + // use an EWMA to smooth it in running time + price *indicator.EWMA + position *types.Position + profitStats *types.ProfitStats + breakCondition fixedpoint.Value +} + +func NewCircuitBreakRiskControl( + position *types.Position, + price *indicator.EWMA, + breakCondition fixedpoint.Value, + profitStats *types.ProfitStats) *CircuitBreakRiskControl { + + return &CircuitBreakRiskControl{ + price: price, + position: position, + profitStats: profitStats, + breakCondition: breakCondition, + } +} + +// IsHalted returns whether we reached the circuit break condition set for this day? +func (c *CircuitBreakRiskControl) IsHalted() bool { + var unrealized = c.position.UnrealizedProfit(fixedpoint.NewFromFloat(c.price.Last(0))) + log.Infof("[CircuitBreakRiskControl] Realized P&L = %v, Unrealized P&L = %v\n", + c.profitStats.TodayPnL, + unrealized) + return unrealized.Add(c.profitStats.TodayPnL).Compare(c.breakCondition) <= 0 +} diff --git a/pkg/risk/riskcontrol/circuit_break_test.go b/pkg/risk/riskcontrol/circuit_break_test.go new file mode 100644 index 0000000000..8d2252634d --- /dev/null +++ b/pkg/risk/riskcontrol/circuit_break_test.go @@ -0,0 +1,79 @@ +package riskcontrol + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +func Test_IsHalted(t *testing.T) { + + var ( + price = 30000.00 + realizedPnL = fixedpoint.NewFromFloat(-100.0) + breakCondition = fixedpoint.NewFromFloat(-500.00) + ) + + window := types.IntervalWindow{Window: 30, Interval: types.Interval1m} + priceEWMA := &indicator.EWMA{IntervalWindow: window} + priceEWMA.Update(price) + + cases := []struct { + name string + position fixedpoint.Value + averageCost fixedpoint.Value + isHalted bool + }{ + { + name: "PositivePositionReachBreakCondition", + position: fixedpoint.NewFromFloat(10.0), + averageCost: fixedpoint.NewFromFloat(30040.0), + isHalted: true, + }, { + name: "PositivePositionOverBreakCondition", + position: fixedpoint.NewFromFloat(10.0), + averageCost: fixedpoint.NewFromFloat(30050.0), + isHalted: true, + }, { + name: "PositivePositionUnderBreakCondition", + position: fixedpoint.NewFromFloat(10.0), + averageCost: fixedpoint.NewFromFloat(30030.0), + isHalted: false, + }, { + name: "NegativePositionReachBreakCondition", + position: fixedpoint.NewFromFloat(-10.0), + averageCost: fixedpoint.NewFromFloat(29960.0), + isHalted: true, + }, { + name: "NegativePositionOverBreakCondition", + position: fixedpoint.NewFromFloat(-10.0), + averageCost: fixedpoint.NewFromFloat(29950.0), + isHalted: true, + }, { + name: "NegativePositionUnderBreakCondition", + position: fixedpoint.NewFromFloat(-10.0), + averageCost: fixedpoint.NewFromFloat(29970.0), + isHalted: false, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var riskControl = NewCircuitBreakRiskControl( + &types.Position{ + Base: tc.position, + AverageCost: tc.averageCost, + }, + priceEWMA, + breakCondition, + &types.ProfitStats{ + TodayPnL: realizedPnL, + }, + ) + assert.Equal(t, tc.isHalted, riskControl.IsHalted()) + }) + } +} diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go new file mode 100644 index 0000000000..44022c55d5 --- /dev/null +++ b/pkg/risk/riskcontrol/position.go @@ -0,0 +1,42 @@ +package riskcontrol + +import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + log "github.com/sirupsen/logrus" +) + +//go:generate callbackgen -type PositionRiskControl +type PositionRiskControl struct { + hardLimit fixedpoint.Value + quantity fixedpoint.Value + + releasePositionCallbacks []func(quantity fixedpoint.Value, side types.SideType) +} + +func NewPositionRiskControl(hardLimit, quantity fixedpoint.Value, tradeCollector *bbgo.TradeCollector) *PositionRiskControl { + p := &PositionRiskControl{ + hardLimit: hardLimit, + quantity: quantity, + } + // register position update handler: check if position is over hardlimit + tradeCollector.OnPositionUpdate(func(position *types.Position) { + if fixedpoint.Compare(position.Base, hardLimit) > 0 { + log.Infof("Position %v is over hardlimit %v, releasing:\n", position.Base, hardLimit) + p.EmitReleasePosition(position.Base.Sub(hardLimit), types.SideTypeSell) + } else if fixedpoint.Compare(position.Base, hardLimit.Neg()) < 0 { + log.Infof("Position %v is over hardlimit %v, releasing:\n", position.Base, hardLimit) + p.EmitReleasePosition(position.Base.Neg().Sub(hardLimit), types.SideTypeBuy) + } + }) + return p +} + +// ModifiedQuantity returns quantity controlled by position risks +// For buy orders, mod quantity = min(hardlimit - position, quanity), limiting by positive position +// For sell orders, mod quantity = min(hardlimit - (-position), quanity), limiting by negative position +func (p *PositionRiskControl) ModifiedQuantity(position fixedpoint.Value) (buyQuanity, sellQuantity fixedpoint.Value) { + return fixedpoint.Min(p.hardLimit.Sub(position), p.quantity), + fixedpoint.Min(p.hardLimit.Add(position), p.quantity) +} diff --git a/pkg/risk/riskcontrol/position_test.go b/pkg/risk/riskcontrol/position_test.go new file mode 100644 index 0000000000..fe0aa7f14d --- /dev/null +++ b/pkg/risk/riskcontrol/position_test.go @@ -0,0 +1,92 @@ +package riskcontrol + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func Test_ModifiedQuantity(t *testing.T) { + + riskControl := NewPositionRiskControl(fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2), &bbgo.TradeCollector{}) + + cases := []struct { + name string + position fixedpoint.Value + buyQuantity fixedpoint.Value + sellQuantity fixedpoint.Value + }{ + { + name: "BuyOverHardLimit", + position: fixedpoint.NewFromInt(9), + buyQuantity: fixedpoint.NewFromInt(1), + sellQuantity: fixedpoint.NewFromInt(2), + }, + { + name: "SellOverHardLimit", + position: fixedpoint.NewFromInt(-9), + buyQuantity: fixedpoint.NewFromInt(2), + sellQuantity: fixedpoint.NewFromInt(1), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + buyQuantity, sellQuantity := riskControl.ModifiedQuantity(tc.position) + assert.Equal(t, tc.buyQuantity, buyQuantity) + assert.Equal(t, tc.sellQuantity, sellQuantity) + }) + } +} + +func TestReleasePositionCallbacks(t *testing.T) { + + var position fixedpoint.Value + + tradeCollector := &bbgo.TradeCollector{} + riskControl := NewPositionRiskControl(fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2), tradeCollector) + riskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { + if side == types.SideTypeBuy { + position = position.Add(quantity) + } else { + position = position.Sub(quantity) + } + }) + + cases := []struct { + name string + position fixedpoint.Value + resultPosition fixedpoint.Value + }{ + { + name: "PostivePositionWithinLimit", + position: fixedpoint.NewFromInt(8), + resultPosition: fixedpoint.NewFromInt(8), + }, + { + name: "NegativePositionWithinLimit", + position: fixedpoint.NewFromInt(-8), + resultPosition: fixedpoint.NewFromInt(-8), + }, + { + name: "PostivePositionOverLimit", + position: fixedpoint.NewFromInt(11), + resultPosition: fixedpoint.NewFromInt(10), + }, + { + name: "NegativePositionOverLimit", + position: fixedpoint.NewFromInt(-11), + resultPosition: fixedpoint.NewFromInt(-10), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + position = tc.position + tradeCollector.EmitPositionUpdate(&types.Position{Base: tc.position}) + assert.Equal(t, tc.resultPosition, position) + }) + } +} diff --git a/pkg/risk/riskcontrol/positionriskcontrol_callbacks.go b/pkg/risk/riskcontrol/positionriskcontrol_callbacks.go new file mode 100644 index 0000000000..63f12385ea --- /dev/null +++ b/pkg/risk/riskcontrol/positionriskcontrol_callbacks.go @@ -0,0 +1,18 @@ +// Code generated by "callbackgen -type PositionRiskControl"; DO NOT EDIT. + +package riskcontrol + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func (p *PositionRiskControl) OnReleasePosition(cb func(quantity fixedpoint.Value, side types.SideType)) { + p.releasePositionCallbacks = append(p.releasePositionCallbacks, cb) +} + +func (p *PositionRiskControl) EmitReleasePosition(quantity fixedpoint.Value, side types.SideType) { + for _, cb := range p.releasePositionCallbacks { + cb(quantity, side) + } +} From ce40549e880f76404a33ffe95c3fb98c3e6e1648 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 17:17:32 +0800 Subject: [PATCH 1066/1392] all: rename QueryOrderUntilSuccessful to QueryOrderUntilFilled --- pkg/bbgo/order_executor_general.go | 2 +- pkg/exchange/retry/order.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index bbb3248a51..adf2771120 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -506,7 +506,7 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix if queryOrderService, ok := e.session.Exchange.(types.ExchangeOrderQueryService); ok && !IsBackTesting { switch submitOrder.Type { case types.OrderTypeMarket: - _, err2 := retry.QueryOrderUntilSuccessful(ctx, queryOrderService, createdOrders[0].Symbol, createdOrders[0].OrderID) + _, err2 := retry.QueryOrderUntilFilled(ctx, queryOrderService, createdOrders[0].Symbol, createdOrders[0].OrderID) if err2 != nil { log.WithError(err2).Errorf("unable to query order") } diff --git a/pkg/exchange/retry/order.go b/pkg/exchange/retry/order.go index 553aee00a1..8c50de6803 100644 --- a/pkg/exchange/retry/order.go +++ b/pkg/exchange/retry/order.go @@ -17,7 +17,7 @@ type advancedOrderCancelService interface { CancelOrdersByGroupID(ctx context.Context, groupID uint32) ([]types.Order, error) } -func QueryOrderUntilSuccessful(ctx context.Context, queryOrderService types.ExchangeOrderQueryService, symbol string, orderId uint64) (o *types.Order, err error) { +func QueryOrderUntilFilled(ctx context.Context, queryOrderService types.ExchangeOrderQueryService, symbol string, orderId uint64) (o *types.Order, err error) { err = backoff.RetryGeneral(ctx, func() (err2 error) { o, err2 = queryOrderService.QueryOrder(ctx, types.OrderQuery{ Symbol: symbol, From 7e894ef0cd33802e07b9bec7d5ee28f08ef4a074 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 17:19:22 +0800 Subject: [PATCH 1067/1392] update command doc files --- doc/commands/bbgo.md | 6 ++---- doc/commands/bbgo_account.md | 6 ++---- doc/commands/bbgo_backtest.md | 6 ++---- doc/commands/bbgo_balances.md | 6 ++---- doc/commands/bbgo_build.md | 6 ++---- doc/commands/bbgo_cancel-order.md | 6 ++---- doc/commands/bbgo_deposits.md | 6 ++---- doc/commands/bbgo_execute-order.md | 6 ++---- doc/commands/bbgo_get-order.md | 6 ++---- doc/commands/bbgo_hoptimize.md | 6 ++---- doc/commands/bbgo_kline.md | 6 ++---- doc/commands/bbgo_list-orders.md | 6 ++---- doc/commands/bbgo_margin.md | 6 ++---- doc/commands/bbgo_margin_interests.md | 6 ++---- doc/commands/bbgo_margin_loans.md | 6 ++---- doc/commands/bbgo_margin_repays.md | 6 ++---- doc/commands/bbgo_market.md | 6 ++---- doc/commands/bbgo_optimize.md | 6 ++---- doc/commands/bbgo_orderbook.md | 6 ++---- doc/commands/bbgo_orderupdate.md | 6 ++---- doc/commands/bbgo_pnl.md | 6 ++---- doc/commands/bbgo_run.md | 6 ++---- doc/commands/bbgo_submit-order.md | 6 ++---- doc/commands/bbgo_sync.md | 6 ++---- doc/commands/bbgo_trades.md | 6 ++---- doc/commands/bbgo_tradeupdate.md | 6 ++---- doc/commands/bbgo_transfer-history.md | 6 ++---- doc/commands/bbgo_userdatastream.md | 6 ++---- doc/commands/bbgo_version.md | 6 ++---- 29 files changed, 58 insertions(+), 116 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index a124be4435..415beba88b 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -15,10 +15,8 @@ bbgo [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. -h, --help help for bbgo + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -60,4 +58,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 63b61993b3..39f27404ba 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -23,9 +23,7 @@ bbgo account [--session SESSION] [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -43,4 +41,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 762b67902c..e58cd8a819 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -32,9 +32,7 @@ bbgo backtest [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -52,4 +50,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index 002aa31322..a02912cd79 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -22,9 +22,7 @@ bbgo balances [--session SESSION] [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -42,4 +40,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index c4032e6e59..4d63a6e4fe 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -21,9 +21,7 @@ bbgo build [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -41,4 +39,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index 38969ef080..e74029bd17 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -31,9 +31,7 @@ bbgo cancel-order [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -51,4 +49,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index dc3995dee0..130edbf92c 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -23,9 +23,7 @@ bbgo deposits [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -43,4 +41,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 1df646874d..6f1b63579c 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -30,9 +30,7 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -50,4 +48,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index fd443b9b99..0625951501 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -24,9 +24,7 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -44,4 +42,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index bcb6bb54bd..76b8393c83 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -27,9 +27,7 @@ bbgo hoptimize [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -47,4 +45,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 379085b4e1..dd2d273ae6 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -24,9 +24,7 @@ bbgo kline [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -44,4 +42,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index c8de325fda..729a5599fd 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -23,9 +23,7 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -43,4 +41,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index a206e35a8e..4f0a7e6f14 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -17,9 +17,7 @@ margin related history --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -40,4 +38,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index e2166d42ba..5b1cb1a73f 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -23,9 +23,7 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -43,4 +41,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index f4a8adfe03..87d7ceaa84 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -23,9 +23,7 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -43,4 +41,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index 871b824932..cbe2f24df4 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -23,9 +23,7 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -43,4 +41,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index 50872b130d..346b3eae57 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -22,9 +22,7 @@ bbgo market [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -42,4 +40,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index add0a18887..15c67f0d93 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -26,9 +26,7 @@ bbgo optimize [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -46,4 +44,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index d17286d4b5..c23ce759fe 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -24,9 +24,7 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -44,4 +42,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index 25f2c531ea..ab4bfee7cf 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -22,9 +22,7 @@ bbgo orderupdate [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -42,4 +40,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index 73304dd288..b8ebb82dc4 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -31,9 +31,7 @@ bbgo pnl [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -51,4 +49,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 62981d64bf..d195be27c1 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -33,9 +33,7 @@ bbgo run [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -53,4 +51,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 12c305a3fa..7e37075a45 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -28,9 +28,7 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -48,4 +46,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index cc685541f4..5f888e941f 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -24,9 +24,7 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -44,4 +42,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index 4cb9e2a2a3..8d688ed4be 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -24,9 +24,7 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -44,4 +42,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index cbe7eaa3b2..46863e9d35 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -22,9 +22,7 @@ bbgo tradeupdate --session=[exchange_name] [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -42,4 +40,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 0df61681c4..e1a42e03b1 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -24,9 +24,7 @@ bbgo transfer-history [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -44,4 +42,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index 4bd5983ad3..cf9b033b2d 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -22,9 +22,7 @@ bbgo userdatastream [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -42,4 +40,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index a3ce8b221d..c4b0147ebc 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -21,9 +21,7 @@ bbgo version [flags] --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") - --ftx-api-key string ftx api key - --ftx-api-secret string ftx api secret - --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --log-formatter string configure log formatter --max-api-key string max api key --max-api-secret string max api secret --metrics enable prometheus metrics @@ -41,4 +39,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 19-Jun-2023 +###### Auto generated by spf13/cobra on 29-Jun-2023 From 2d9890a18fe0f47a304f0bd127a8a8d8a7988a6e Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 17:19:22 +0800 Subject: [PATCH 1068/1392] bump version to v1.49.0 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 1b8a39071b..001ff0cb3d 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.4-833d9428-dev" +const Version = "v1.49.0-8a89408f-dev" -const VersionGitRef = "833d9428" +const VersionGitRef = "8a89408f" diff --git a/pkg/version/version.go b/pkg/version/version.go index cb2f34d948..3a2dfe8551 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.48.4-833d9428" +const Version = "v1.49.0-8a89408f" -const VersionGitRef = "833d9428" +const VersionGitRef = "8a89408f" From ead9b9737b26d7f97886405628656fd1b691854b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 17:19:22 +0800 Subject: [PATCH 1069/1392] add v1.49.0 release note --- doc/release/v1.49.0.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 doc/release/v1.49.0.md diff --git a/doc/release/v1.49.0.md b/doc/release/v1.49.0.md new file mode 100644 index 0000000000..5b69a8d943 --- /dev/null +++ b/doc/release/v1.49.0.md @@ -0,0 +1,16 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.48.4...main) + + - [#1212](https://github.com/c9s/bbgo/pull/1212): FEATURE: add risk controls for strategies + - [#1211](https://github.com/c9s/bbgo/pull/1211): CHORE: bbgo: rename env ENABLE_MARKET_TRADE_STOP to DISABLE_MARKET_TRADE_STOP + - [#1206](https://github.com/c9s/bbgo/pull/1206): IMPROVE: improve stop loss methods + - [#1210](https://github.com/c9s/bbgo/pull/1210): REFACTOR: move retry functions + - [#1209](https://github.com/c9s/bbgo/pull/1209): FEATURE: helm add deployment annotation + - [#1208](https://github.com/c9s/bbgo/pull/1208): FIX: add log formatter option and update settings for staging + - [#1207](https://github.com/c9s/bbgo/pull/1207): MINOR: [grid2] delete order prices metric + - [#1205](https://github.com/c9s/bbgo/pull/1205): FIX: [xalign] add balance fault tolerance range + - [#1204](https://github.com/c9s/bbgo/pull/1204): FIX: typo in xalign config + - [#1203](https://github.com/c9s/bbgo/pull/1203): FEATURE: pass deployment labels to pod + - [#1202](https://github.com/c9s/bbgo/pull/1202): FIX: [xgap] fix group id range + - [#1201](https://github.com/c9s/bbgo/pull/1201): FIX: typo in helm chart additionalLabels + - [#1200](https://github.com/c9s/bbgo/pull/1200): CHORE: release helm chart + - [#1199](https://github.com/c9s/bbgo/pull/1199): FEATURE: allow additional labels From dddf7c57ba9f1b1c05ce3bf59aca33e4bef591d4 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 17:44:36 +0800 Subject: [PATCH 1070/1392] bbgo: add v2 indicator set --- pkg/bbgo/indicator_loader.go | 98 ++++++++++++++++++++++++++++++ pkg/bbgo/standard_indicator_set.go | 1 + pkg/indicator/klinestream.go | 6 ++ 3 files changed, 105 insertions(+) create mode 100644 pkg/bbgo/indicator_loader.go diff --git a/pkg/bbgo/indicator_loader.go b/pkg/bbgo/indicator_loader.go new file mode 100644 index 0000000000..dc5dee7d89 --- /dev/null +++ b/pkg/bbgo/indicator_loader.go @@ -0,0 +1,98 @@ +package bbgo + +import ( + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// IndicatorSet is the v2 standard indicator set +// This will replace StandardIndicator in the future +type IndicatorSet struct { + Symbol string + + stream types.Stream + store *MarketDataStore + + // caches + kLines map[types.Interval]*indicator.KLineStream + closePrices map[types.Interval]*indicator.PriceStream +} + +func NewIndicatorLoader(symbol string, stream types.Stream, store *MarketDataStore) *IndicatorSet { + return &IndicatorSet{ + Symbol: symbol, + store: store, + stream: stream, + + kLines: make(map[types.Interval]*indicator.KLineStream), + closePrices: make(map[types.Interval]*indicator.PriceStream), + } +} + +func (i *IndicatorSet) KLines(interval types.Interval) *indicator.KLineStream { + if kLines, ok := i.kLines[interval]; ok { + return kLines + } + + kLines := indicator.KLines(i.stream, i.Symbol, interval) + if kLinesWindow, ok := i.store.KLinesOfInterval(interval); ok { + kLines.AddBackLog(*kLinesWindow) + } + + i.kLines[interval] = kLines + return kLines +} + +func (i *IndicatorSet) OPEN(interval types.Interval) *indicator.PriceStream { + return indicator.OpenPrices(i.KLines(interval)) +} + +func (i *IndicatorSet) HIGH(interval types.Interval) *indicator.PriceStream { + return indicator.HighPrices(i.KLines(interval)) +} + +func (i *IndicatorSet) LOW(interval types.Interval) *indicator.PriceStream { + return indicator.LowPrices(i.KLines(interval)) +} + +func (i *IndicatorSet) CLOSE(interval types.Interval) *indicator.PriceStream { + if closePrices, ok := i.closePrices[interval]; ok { + return closePrices + } + + closePrices := indicator.ClosePrices(i.KLines(interval)) + i.closePrices[interval] = closePrices + return closePrices +} + +func (i *IndicatorSet) RSI(iw types.IntervalWindow) *indicator.RSIStream { + return indicator.RSI2(i.CLOSE(iw.Interval), iw.Window) +} + +func (i *IndicatorSet) EMA(iw types.IntervalWindow) *indicator.EWMAStream { + return i.EWMA(iw) +} + +func (i *IndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMAStream { + return indicator.EWMA2(i.CLOSE(iw.Interval), iw.Window) +} + +func (i *IndicatorSet) STOCH(iw types.IntervalWindow, dPeriod int) *indicator.StochStream { + return indicator.Stoch2(i.KLines(iw.Interval), iw.Window, dPeriod) +} + +func (i *IndicatorSet) BOLL(iw types.IntervalWindow, k float64) *indicator.BOLLStream { + return indicator.BOLL2(i.CLOSE(iw.Interval), iw.Window, k) +} + +func (i *IndicatorSet) MACD(interval types.Interval, shortWindow, longWindow, signalWindow int) *indicator.MACDStream { + return indicator.MACD2(i.CLOSE(interval), shortWindow, longWindow, signalWindow) +} + +func (i *IndicatorSet) ATR(interval types.Interval, window int) *indicator.ATRStream { + return indicator.ATR2(i.KLines(interval), window) +} + +func (i *IndicatorSet) ATRP(interval types.Interval, window int) *indicator.ATRPStream { + return indicator.ATRP2(i.KLines(interval), window) +} diff --git a/pkg/bbgo/standard_indicator_set.go b/pkg/bbgo/standard_indicator_set.go index 71befe8908..e0be1fbb41 100644 --- a/pkg/bbgo/standard_indicator_set.go +++ b/pkg/bbgo/standard_indicator_set.go @@ -161,6 +161,7 @@ func (s *StandardIndicatorSet) MACD(iw types.IntervalWindow, shortPeriod, longPe if ok { return inc } + inc = &indicator.MACDLegacy{MACDConfig: config} s.macdIndicators[config] = inc s.initAndBind(inc, config.IntervalWindow.Interval) diff --git a/pkg/indicator/klinestream.go b/pkg/indicator/klinestream.go index 1838584bf1..9783655f92 100644 --- a/pkg/indicator/klinestream.go +++ b/pkg/indicator/klinestream.go @@ -36,6 +36,12 @@ func (s *KLineStream) AddSubscriber(f func(k types.KLine)) { } } +func (s *KLineStream) AddBackLog(kLines []types.KLine) { + for _, k := range kLines { + s.kLines = append(s.kLines, k) + } +} + // KLines creates a KLine stream that pushes the klines to the subscribers func KLines(source types.Stream, symbol string, interval types.Interval) *KLineStream { s := &KLineStream{} From eafd77704637fd23b3e180d3d2068ed4b678ddc3 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 17:49:04 +0800 Subject: [PATCH 1071/1392] add indicators v2 api to session --- pkg/bbgo/indicator_loader.go | 2 +- pkg/bbgo/session.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/indicator_loader.go b/pkg/bbgo/indicator_loader.go index dc5dee7d89..19fa5813b1 100644 --- a/pkg/bbgo/indicator_loader.go +++ b/pkg/bbgo/indicator_loader.go @@ -18,7 +18,7 @@ type IndicatorSet struct { closePrices map[types.Interval]*indicator.PriceStream } -func NewIndicatorLoader(symbol string, stream types.Stream, store *MarketDataStore) *IndicatorSet { +func NewIndicatorSet(symbol string, stream types.Stream, store *MarketDataStore) *IndicatorSet { return &IndicatorSet{ Symbol: symbol, store: store, diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 895226f69d..7f5c61929e 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -107,6 +107,9 @@ type ExchangeSession struct { // standard indicators of each market standardIndicatorSets map[string]*StandardIndicatorSet + // indicators is the v2 api indicators + indicators map[string]*IndicatorSet + orderStores map[string]*OrderStore usedSymbols map[string]struct{} @@ -136,6 +139,7 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession { positions: make(map[string]*types.Position), marketDataStores: make(map[string]*MarketDataStore), standardIndicatorSets: make(map[string]*StandardIndicatorSet), + indicators: make(map[string]*IndicatorSet), orderStores: make(map[string]*OrderStore), usedSymbols: make(map[string]struct{}), initializedSymbols: make(map[string]struct{}), @@ -484,6 +488,20 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ return nil } +// Indicators returns the IndicatorSet struct that maintains the kLines stream cache and price stream cache +// It also provides helper methods +func (session *ExchangeSession) Indicators(symbol string) *IndicatorSet { + set, ok := session.indicators[symbol] + if ok { + return set + } + + store, _ := session.MarketDataStore(symbol) + set = NewIndicatorSet(symbol, session.MarketDataStream, store) + session.indicators[symbol] = set + return set +} + func (session *ExchangeSession) StandardIndicatorSet(symbol string) *StandardIndicatorSet { set, ok := session.standardIndicatorSets[symbol] if ok { From f91a4c2979c0b835c2ed0f52c5baa63692bbe935 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 17:55:55 +0800 Subject: [PATCH 1072/1392] indicator: simplify add klines --- pkg/indicator/klinestream.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/indicator/klinestream.go b/pkg/indicator/klinestream.go index 9783655f92..8955c1c600 100644 --- a/pkg/indicator/klinestream.go +++ b/pkg/indicator/klinestream.go @@ -37,9 +37,7 @@ func (s *KLineStream) AddSubscriber(f func(k types.KLine)) { } func (s *KLineStream) AddBackLog(kLines []types.KLine) { - for _, k := range kLines { - s.kLines = append(s.kLines, k) - } + s.kLines = append(s.kLines, kLines...) } // KLines creates a KLine stream that pushes the klines to the subscribers From e3be2a8af63a27168828dadb28159149ab65d139 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 18:04:39 +0800 Subject: [PATCH 1073/1392] bollmaker: replace bollinger indicator with v2 indicator --- pkg/strategy/bollmaker/dynamic_spread.go | 10 +++++----- pkg/strategy/bollmaker/strategy.go | 8 ++++---- pkg/strategy/bollmaker/trend.go | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/bollmaker/dynamic_spread.go b/pkg/strategy/bollmaker/dynamic_spread.go index 51785f3df9..b6ede21569 100644 --- a/pkg/strategy/bollmaker/dynamic_spread.go +++ b/pkg/strategy/bollmaker/dynamic_spread.go @@ -1,9 +1,10 @@ package bollmaker import ( - "github.com/pkg/errors" "math" + "github.com/pkg/errors" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" @@ -27,7 +28,7 @@ type DynamicSpreadSettings struct { } // Initialize dynamic spreads and preload SMAs -func (ds *DynamicSpreadSettings) Initialize(symbol string, session *bbgo.ExchangeSession, neutralBoll, defaultBoll *indicator.BOLL) { +func (ds *DynamicSpreadSettings) Initialize(symbol string, session *bbgo.ExchangeSession, neutralBoll, defaultBoll *indicator.BOLLStream) { switch { case ds.AmpSpreadSettings != nil: ds.AmpSpreadSettings.initialize(symbol, session) @@ -163,11 +164,10 @@ type DynamicSpreadBollWidthRatioSettings struct { // A positive number. The greater factor, the sharper weighting function. Default set to 1.0 . Sensitivity float64 `json:"sensitivity"` - neutralBoll *indicator.BOLL - defaultBoll *indicator.BOLL + defaultBoll, neutralBoll *indicator.BOLLStream } -func (ds *DynamicSpreadBollWidthRatioSettings) initialize(neutralBoll, defaultBoll *indicator.BOLL) { +func (ds *DynamicSpreadBollWidthRatioSettings) initialize(neutralBoll, defaultBoll *indicator.BOLLStream) { ds.neutralBoll = neutralBoll ds.defaultBoll = defaultBoll if ds.Sensitivity <= 0. { diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index efd1742d21..a2233b44d4 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -158,10 +158,10 @@ type Strategy struct { groupID uint32 // defaultBoll is the BOLLINGER indicator we used for predicting the price. - defaultBoll *indicator.BOLL + defaultBoll *indicator.BOLLStream // neutralBoll is the neutral price section - neutralBoll *indicator.BOLL + neutralBoll *indicator.BOLLStream // StrategyController bbgo.StrategyController @@ -465,8 +465,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // StrategyController s.Status = types.StrategyStatusRunning - s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) - s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth) + s.neutralBoll = session.Indicators(s.Symbol).BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) + s.defaultBoll = session.Indicators(s.Symbol).BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth) // Setup dynamic spread if s.DynamicSpread.IsEnabled() { diff --git a/pkg/strategy/bollmaker/trend.go b/pkg/strategy/bollmaker/trend.go index 4c0cb7fcdf..7e4ddcf714 100644 --- a/pkg/strategy/bollmaker/trend.go +++ b/pkg/strategy/bollmaker/trend.go @@ -11,16 +11,16 @@ const ( UnknownTrend PriceTrend = "unknown" ) -func detectPriceTrend(inc *indicator.BOLL, price float64) PriceTrend { +func detectPriceTrend(inc *indicator.BOLLStream, price float64) PriceTrend { if inBetween(price, inc.DownBand.Last(0), inc.UpBand.Last(0)) { return NeutralTrend } - if price < inc.LastDownBand() { + if price < inc.DownBand.Last(0) { return DownTrend } - if price > inc.LastUpBand() { + if price > inc.UpBand.Last(0) { return UpTrend } From 5c5543d78a42f8f861224b1fcdeab4d4e06ab059 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 29 Jun 2023 21:08:43 +0800 Subject: [PATCH 1074/1392] bbgo: when err == nil, should just return the created orders --- pkg/bbgo/order_execution.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 4ac34b72a0..ecfb0e5e0a 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -71,6 +71,7 @@ func (e *ExchangeOrderExecutionRouter) CancelOrdersTo(ctx context.Context, sessi } // ExchangeOrderExecutor is an order executor wrapper for single exchange instance. +// //go:generate callbackgen -type ExchangeOrderExecutor type ExchangeOrderExecutor struct { // MinQuoteBalance fixedpoint.Value `json:"minQuoteBalance,omitempty" yaml:"minQuoteBalance,omitempty"` @@ -340,6 +341,8 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ createdOrders, errIdx, err2 = BatchPlaceOrder(ctx, exchange, orderCallback, submitOrders...) if err2 != nil { werr = multierr.Append(werr, err2) + } else if err2 == nil { + return createdOrders, nil, nil } } From fc7edc5c8089488cb50bc801788ae16949d855c4 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 01:05:18 +0800 Subject: [PATCH 1075/1392] grid2: call TryLock in updateGridNumOfOrdersMetricsWithLock --- pkg/strategy/grid2/strategy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f9111c5c2a..11abe0f7bd 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1179,7 +1179,11 @@ func (s *Strategy) updateFilledOrderMetrics(order types.Order) { } func (s *Strategy) updateGridNumOfOrdersMetricsWithLock() { - s.updateGridNumOfOrdersMetrics(s.getGrid()) + if s.mu.TryLock() { + grid := s.grid + s.mu.Unlock() + s.updateGridNumOfOrdersMetrics(grid) + } } func (s *Strategy) updateGridNumOfOrdersMetrics(grid *Grid) { From 77e31e92742b9ed9cfdc9a749b8e29feeddc6934 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 01:12:10 +0800 Subject: [PATCH 1076/1392] types: split pendingRemoval lock scope --- pkg/types/ordermap.go | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/pkg/types/ordermap.go b/pkg/types/ordermap.go index 358d640a4a..bc09392697 100644 --- a/pkg/types/ordermap.go +++ b/pkg/types/ordermap.go @@ -125,28 +125,36 @@ func (m *SyncOrderMap) Remove(orderID uint64) (exists bool) { return exists } -func (m *SyncOrderMap) Add(o Order) { +func (m *SyncOrderMap) processPendingRemoval() { + if len(m.pendingRemoval) == 0 { + return + } + m.Lock() defer m.Unlock() - m.orders.Add(o) - - if len(m.pendingRemoval) > 0 { - expireTime := time.Now().Add(-5 * time.Minute) - removing := make(map[uint64]struct{}) - for orderID, creationTime := range m.pendingRemoval { - if m.orders.Exists(orderID) || creationTime.Before(expireTime) { - m.orders.Remove(orderID) - removing[orderID] = struct{}{} - } + expireTime := time.Now().Add(-5 * time.Minute) + removing := make(map[uint64]struct{}) + for orderID, creationTime := range m.pendingRemoval { + if m.orders.Exists(orderID) || creationTime.Before(expireTime) { + m.orders.Remove(orderID) + removing[orderID] = struct{}{} } + } - for orderID := range removing { - delete(m.pendingRemoval, orderID) - } + for orderID := range removing { + delete(m.pendingRemoval, orderID) } } +func (m *SyncOrderMap) Add(o Order) { + m.Lock() + m.orders.Add(o) + m.Unlock() + + m.processPendingRemoval() +} + func (m *SyncOrderMap) Update(o Order) { m.Lock() m.orders.Update(o) From b29c1aa972e45aa43cacdb05d97e5b3fd14b2e58 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 10:35:34 +0800 Subject: [PATCH 1077/1392] bbgo: add warning --- pkg/bbgo/indicator_loader.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/bbgo/indicator_loader.go b/pkg/bbgo/indicator_loader.go index 19fa5813b1..55a1e0ad1f 100644 --- a/pkg/bbgo/indicator_loader.go +++ b/pkg/bbgo/indicator_loader.go @@ -1,6 +1,8 @@ package bbgo import ( + "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" ) @@ -37,6 +39,8 @@ func (i *IndicatorSet) KLines(interval types.Interval) *indicator.KLineStream { kLines := indicator.KLines(i.stream, i.Symbol, interval) if kLinesWindow, ok := i.store.KLinesOfInterval(interval); ok { kLines.AddBackLog(*kLinesWindow) + } else { + logrus.Warnf("market data store %s kline history not found, unable to backfill the kline stream data", interval) } i.kLines[interval] = kLines From 064932ea9d108f06027d3775a16dfc896feb1916 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 10:37:42 +0800 Subject: [PATCH 1078/1392] indicator: add VOLUME api --- pkg/bbgo/indicator_loader.go | 4 ++++ pkg/indicator/v2_price.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/pkg/bbgo/indicator_loader.go b/pkg/bbgo/indicator_loader.go index 55a1e0ad1f..ecdddda1f6 100644 --- a/pkg/bbgo/indicator_loader.go +++ b/pkg/bbgo/indicator_loader.go @@ -69,6 +69,10 @@ func (i *IndicatorSet) CLOSE(interval types.Interval) *indicator.PriceStream { return closePrices } +func (i *IndicatorSet) VOLUME(interval types.Interval) *indicator.PriceStream { + return indicator.Volumes(i.KLines(interval)) +} + func (i *IndicatorSet) RSI(iw types.IntervalWindow) *indicator.RSIStream { return indicator.RSI2(i.CLOSE(iw.Interval), iw.Window) } diff --git a/pkg/indicator/v2_price.go b/pkg/indicator/v2_price.go index 6e4c61844b..871bee01c0 100644 --- a/pkg/indicator/v2_price.go +++ b/pkg/indicator/v2_price.go @@ -51,3 +51,7 @@ func HighPrices(source KLineSubscription) *PriceStream { func OpenPrices(source KLineSubscription) *PriceStream { return Price(source, KLineOpenPriceMapper) } + +func Volumes(source KLineSubscription) *PriceStream { + return Price(source, KLineVolumeMapper) +} From 9885a685370d26963cab7ae8acb36e036ca3df29 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 10:38:38 +0800 Subject: [PATCH 1079/1392] bbgo: rename AddBackLog to BackFill --- pkg/bbgo/indicator_loader.go | 2 +- pkg/indicator/klinestream.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/indicator_loader.go b/pkg/bbgo/indicator_loader.go index ecdddda1f6..8dfa08a48c 100644 --- a/pkg/bbgo/indicator_loader.go +++ b/pkg/bbgo/indicator_loader.go @@ -38,7 +38,7 @@ func (i *IndicatorSet) KLines(interval types.Interval) *indicator.KLineStream { kLines := indicator.KLines(i.stream, i.Symbol, interval) if kLinesWindow, ok := i.store.KLinesOfInterval(interval); ok { - kLines.AddBackLog(*kLinesWindow) + kLines.BackFill(*kLinesWindow) } else { logrus.Warnf("market data store %s kline history not found, unable to backfill the kline stream data", interval) } diff --git a/pkg/indicator/klinestream.go b/pkg/indicator/klinestream.go index 8955c1c600..156c8b97e9 100644 --- a/pkg/indicator/klinestream.go +++ b/pkg/indicator/klinestream.go @@ -36,7 +36,7 @@ func (s *KLineStream) AddSubscriber(f func(k types.KLine)) { } } -func (s *KLineStream) AddBackLog(kLines []types.KLine) { +func (s *KLineStream) BackFill(kLines []types.KLine) { s.kLines = append(s.kLines, kLines...) } From dcb091cab132c49709f94c0e235142f188e2f695 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 10:46:40 +0800 Subject: [PATCH 1080/1392] bbgo: add TestIndicatorSet_closeCache test --- .../{indicator_loader.go => indicator_set.go} | 0 pkg/bbgo/indicator_set_test.go | 31 +++++++++++++++++++ 2 files changed, 31 insertions(+) rename pkg/bbgo/{indicator_loader.go => indicator_set.go} (100%) create mode 100644 pkg/bbgo/indicator_set_test.go diff --git a/pkg/bbgo/indicator_loader.go b/pkg/bbgo/indicator_set.go similarity index 100% rename from pkg/bbgo/indicator_loader.go rename to pkg/bbgo/indicator_set.go diff --git a/pkg/bbgo/indicator_set_test.go b/pkg/bbgo/indicator_set_test.go new file mode 100644 index 0000000000..e1bbb3f582 --- /dev/null +++ b/pkg/bbgo/indicator_set_test.go @@ -0,0 +1,31 @@ +package bbgo + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" +) + +func TestIndicatorSet_closeCache(t *testing.T) { + symbol := "BTCUSDT" + store := NewMarketDataStore(symbol) + store.KLineWindows[types.Interval1m] = &types.KLineWindow{ + {Open: number(19000.0), Close: number(19100.0)}, + {Open: number(19100.0), Close: number(19200.0)}, + {Open: number(19200.0), Close: number(19300.0)}, + {Open: number(19300.0), Close: number(19200.0)}, + {Open: number(19200.0), Close: number(19100.0)}, + } + + stream := types.NewStandardStream() + + indicatorSet := NewIndicatorSet(symbol, &stream, store) + + close1m := indicatorSet.CLOSE(types.Interval1m) + assert.NotNil(t, close1m) + + close1m2 := indicatorSet.CLOSE(types.Interval1m) + assert.Equal(t, close1m, close1m2) +} From 775ad7d9063402bc4603eebeee930583f92a0bda Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 10:58:07 +0800 Subject: [PATCH 1081/1392] indicator: improve kline stream backfill --- pkg/indicator/klinestream.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg/indicator/klinestream.go b/pkg/indicator/klinestream.go index 156c8b97e9..11215b3e7f 100644 --- a/pkg/indicator/klinestream.go +++ b/pkg/indicator/klinestream.go @@ -28,16 +28,21 @@ func (s *KLineStream) Last(i int) *types.KLine { func (s *KLineStream) AddSubscriber(f func(k types.KLine)) { s.OnUpdate(f) - if len(s.kLines) > 0 { - // push historical klines to the subscriber - for _, k := range s.kLines { - f(k) - } + if len(s.kLines) == 0 { + return + } + + // push historical klines to the subscriber + for _, k := range s.kLines { + f(k) } } func (s *KLineStream) BackFill(kLines []types.KLine) { - s.kLines = append(s.kLines, kLines...) + for _, k := range kLines { + s.kLines = append(s.kLines, k) + s.EmitUpdate(k) + } } // KLines creates a KLine stream that pushes the klines to the subscribers From ea1025d79026b49ee5fb72b1c6e7031299f7903e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 10:58:25 +0800 Subject: [PATCH 1082/1392] indicator: implement Subscribe method on PriceStream --- pkg/bbgo/indicator_set_test.go | 23 ++++++++++++++++++++--- pkg/indicator/v2_price.go | 26 +++++++++++++++++++++----- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/pkg/bbgo/indicator_set_test.go b/pkg/bbgo/indicator_set_test.go index e1bbb3f582..0cf7050b6f 100644 --- a/pkg/bbgo/indicator_set_test.go +++ b/pkg/bbgo/indicator_set_test.go @@ -8,7 +8,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -func TestIndicatorSet_closeCache(t *testing.T) { +func newTestIndicatorSet() *IndicatorSet { symbol := "BTCUSDT" store := NewMarketDataStore(symbol) store.KLineWindows[types.Interval1m] = &types.KLineWindow{ @@ -17,15 +17,32 @@ func TestIndicatorSet_closeCache(t *testing.T) { {Open: number(19200.0), Close: number(19300.0)}, {Open: number(19300.0), Close: number(19200.0)}, {Open: number(19200.0), Close: number(19100.0)}, + {Open: number(19100.0), Close: number(19500.0)}, + {Open: number(19500.0), Close: number(19600.0)}, + {Open: number(19600.0), Close: number(19700.0)}, } stream := types.NewStandardStream() - indicatorSet := NewIndicatorSet(symbol, &stream, store) - + return indicatorSet +} + +func TestIndicatorSet_closeCache(t *testing.T) { + indicatorSet := newTestIndicatorSet() + close1m := indicatorSet.CLOSE(types.Interval1m) assert.NotNil(t, close1m) close1m2 := indicatorSet.CLOSE(types.Interval1m) assert.Equal(t, close1m, close1m2) } + +func TestIndicatorSet_rsi(t *testing.T) { + indicatorSet := newTestIndicatorSet() + + rsi1m := indicatorSet.RSI(types.IntervalWindow{Interval: types.Interval1m, Window: 7}) + assert.NotNil(t, rsi1m) + + rsiLast := rsi1m.Last(0) + assert.InDelta(t, 80, rsiLast, 0.0000001) +} diff --git a/pkg/indicator/v2_price.go b/pkg/indicator/v2_price.go index 871bee01c0..d4c546e57f 100644 --- a/pkg/indicator/v2_price.go +++ b/pkg/indicator/v2_price.go @@ -22,15 +22,31 @@ func Price(source KLineSubscription, mapper KLineValueMapper) *PriceStream { mapper: mapper, } - if source != nil { - source.AddSubscriber(func(k types.KLine) { - v := s.mapper(k) - s.PushAndEmit(v) - }) + if source == nil { + return s } + + source.AddSubscriber(func(k types.KLine) { + v := s.mapper(k) + s.PushAndEmit(v) + }) return s } +// AddSubscriber adds the subscriber function and push historical data to the subscriber +func (s *PriceStream) AddSubscriber(f func(v float64)) { + s.OnUpdate(f) + + if len(s.slice) == 0 { + return + } + + // push historical value to the subscriber + for _, v := range s.slice { + f(v) + } +} + func (s *PriceStream) PushAndEmit(v float64) { s.slice.Push(v) s.EmitUpdate(v) From a3a1586e244362dcd83ed0493493c79ff2470a54 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 11:02:42 +0800 Subject: [PATCH 1083/1392] bbgo: add TestIndicatorSet_EWMA test --- pkg/bbgo/indicator_set_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/indicator_set_test.go b/pkg/bbgo/indicator_set_test.go index 0cf7050b6f..12146c179d 100644 --- a/pkg/bbgo/indicator_set_test.go +++ b/pkg/bbgo/indicator_set_test.go @@ -37,7 +37,7 @@ func TestIndicatorSet_closeCache(t *testing.T) { assert.Equal(t, close1m, close1m2) } -func TestIndicatorSet_rsi(t *testing.T) { +func TestIndicatorSet_RSI(t *testing.T) { indicatorSet := newTestIndicatorSet() rsi1m := indicatorSet.RSI(types.IntervalWindow{Interval: types.Interval1m, Window: 7}) @@ -46,3 +46,13 @@ func TestIndicatorSet_rsi(t *testing.T) { rsiLast := rsi1m.Last(0) assert.InDelta(t, 80, rsiLast, 0.0000001) } + +func TestIndicatorSet_EWMA(t *testing.T) { + indicatorSet := newTestIndicatorSet() + + ema1m := indicatorSet.EWMA(types.IntervalWindow{Interval: types.Interval1m, Window: 7}) + assert.NotNil(t, ema1m) + + emaLast := ema1m.Last(0) + assert.InDelta(t, 19424.224853515625, emaLast, 0.0000001) +} From 0e2f69e8375b2d92f4bda3098a33142cb94ad2e8 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 11:05:03 +0800 Subject: [PATCH 1084/1392] bbgo: just use else condition --- pkg/bbgo/order_execution.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index ecfb0e5e0a..095160535e 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -341,7 +341,7 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ createdOrders, errIdx, err2 = BatchPlaceOrder(ctx, exchange, orderCallback, submitOrders...) if err2 != nil { werr = multierr.Append(werr, err2) - } else if err2 == nil { + } else { return createdOrders, nil, nil } } From 085114b244c91412e2f7fa61bac559d889f290e2 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 11:07:02 +0800 Subject: [PATCH 1085/1392] grid2: add warning message when failed to acquire the lock --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 11abe0f7bd..6a38c5f1f7 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1183,6 +1183,8 @@ func (s *Strategy) updateGridNumOfOrdersMetricsWithLock() { grid := s.grid s.mu.Unlock() s.updateGridNumOfOrdersMetrics(grid) + } else { + s.logger.Warnf("updateGridNumOfOrdersMetricsWithLock: failed to acquire the lock to update metrics") } } From fe9038106d48a0592d15691b252ff8394bfcb6c9 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 11:41:13 +0800 Subject: [PATCH 1086/1392] types: wrap pendingRemoval with lock --- pkg/types/ordermap.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/types/ordermap.go b/pkg/types/ordermap.go index bc09392697..e7cfca1a4d 100644 --- a/pkg/types/ordermap.go +++ b/pkg/types/ordermap.go @@ -126,13 +126,13 @@ func (m *SyncOrderMap) Remove(orderID uint64) (exists bool) { } func (m *SyncOrderMap) processPendingRemoval() { + m.Lock() + defer m.Unlock() + if len(m.pendingRemoval) == 0 { return } - m.Lock() - defer m.Unlock() - expireTime := time.Now().Add(-5 * time.Minute) removing := make(map[uint64]struct{}) for orderID, creationTime := range m.pendingRemoval { From 184d282e1a60ba6ac3280e5c5af320dcb47e1c19 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 12:02:40 +0800 Subject: [PATCH 1087/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index 415beba88b..f8cd27ff6c 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -58,4 +58,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 39f27404ba..8ebf6da1e7 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -41,4 +41,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index e58cd8a819..1dce55e94d 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -50,4 +50,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index a02912cd79..c5b34fd5bd 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -40,4 +40,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index 4d63a6e4fe..ddcc41575c 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -39,4 +39,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index e74029bd17..b7e074697c 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -49,4 +49,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index 130edbf92c..e4f5de6baf 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -41,4 +41,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 6f1b63579c..9e46cd6c9a 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -48,4 +48,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 0625951501..8cde124b86 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -42,4 +42,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index 76b8393c83..093b1f991d 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -45,4 +45,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index dd2d273ae6..3f531da159 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -42,4 +42,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index 729a5599fd..282a393910 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -41,4 +41,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index 4f0a7e6f14..8af3b7045c 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -38,4 +38,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index 5b1cb1a73f..62aea931a2 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -41,4 +41,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 87d7ceaa84..7ff5b2efbb 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -41,4 +41,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index cbe2f24df4..8e9f997c57 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -41,4 +41,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index 346b3eae57..dfb8c95cb9 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -40,4 +40,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index 15c67f0d93..5b99474784 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -44,4 +44,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index c23ce759fe..39d306797b 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -42,4 +42,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index ab4bfee7cf..5046b06fac 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -40,4 +40,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index b8ebb82dc4..05a0ffca0f 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -49,4 +49,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index d195be27c1..a0d3dae51a 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -51,4 +51,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 7e37075a45..6ddd877976 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -46,4 +46,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index 5f888e941f..e676746c7d 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -42,4 +42,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index 8d688ed4be..20d67153aa 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -42,4 +42,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 46863e9d35..ec300938d1 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -40,4 +40,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index e1a42e03b1..6764c81634 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -42,4 +42,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index cf9b033b2d..4c13aa131e 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -40,4 +40,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index c4b0147ebc..4881f38009 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -39,4 +39,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 29-Jun-2023 +###### Auto generated by spf13/cobra on 30-Jun-2023 From daec6b5f30cd806677ab7e0d6596fd667647d239 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 12:02:40 +0800 Subject: [PATCH 1088/1392] bump version to v1.50.0 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 001ff0cb3d..231f0771d9 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.49.0-8a89408f-dev" +const Version = "v1.50.0-3929eb20-dev" -const VersionGitRef = "8a89408f" +const VersionGitRef = "3929eb20" diff --git a/pkg/version/version.go b/pkg/version/version.go index 3a2dfe8551..3d913533f8 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.49.0-8a89408f" +const Version = "v1.50.0-3929eb20" -const VersionGitRef = "8a89408f" +const VersionGitRef = "3929eb20" From 3aa2aed688839d6ceff0f234d3d3ea1797c70283 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 30 Jun 2023 12:02:40 +0800 Subject: [PATCH 1089/1392] add v1.50.0 release note --- doc/release/v1.50.0.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 doc/release/v1.50.0.md diff --git a/doc/release/v1.50.0.md b/doc/release/v1.50.0.md new file mode 100644 index 0000000000..3c230717bd --- /dev/null +++ b/doc/release/v1.50.0.md @@ -0,0 +1,5 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.49.0...main) + + - [#1214](https://github.com/c9s/bbgo/pull/1214): REFACTOR: [bollmaker] upgrade to indicator v2 + - [#1213](https://github.com/c9s/bbgo/pull/1213): FEATURE: add v2 indicator set api + - [#1215](https://github.com/c9s/bbgo/pull/1215): FIX: [grid2] use tryLock during the openGrid process From 3c0ade57f83d335b71eb03f865c661de47982ca4 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 30 Jun 2023 13:42:10 +0800 Subject: [PATCH 1090/1392] exit/hhllStop: fix bugs --- pkg/bbgo/exit_hh_ll_stop.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index f737bae561..a5e6ef37b1 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -65,32 +65,42 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close s.activated = false } else if s.activated { if position.IsLong() { - r := fixedpoint.One.Add(s.DeactivationRatio) - if closePrice.Compare(position.AverageCost.Mul(r)) >= 0 { + r_deactive := fixedpoint.One.Add(s.DeactivationRatio) + r_active := fixedpoint.One.Add(s.ActivationRatio) + if closePrice.Compare(position.AverageCost.Mul(r_deactive)) >= 0 { s.activated = false - Notify("[hhllStop] Stop of %s deactivated for long position, deactivation ratio %s:", s.Symbol, s.DeactivationRatio.Percentage()) + Notify("[hhllStop] Stop of %s deactivated for long position, deactivation ratio %s", s.Symbol, s.DeactivationRatio.Percentage()) + } else if closePrice.Compare(position.AverageCost.Mul(r_active)) < 0 { + s.activated = false + Notify("[hhllStop] Stop of %s deactivated for long position, activation ratio %s", s.Symbol, s.ActivationRatio.Percentage()) } } else if position.IsShort() { - r := fixedpoint.One.Sub(s.DeactivationRatio) + r_deactive := fixedpoint.One.Sub(s.DeactivationRatio) + r_active := fixedpoint.One.Sub(s.ActivationRatio) // for short position, if the close price is less than the activation price then this is a profit position. - if closePrice.Compare(position.AverageCost.Mul(r)) <= 0 { + if closePrice.Compare(position.AverageCost.Mul(r_deactive)) <= 0 { + s.activated = false + Notify("[hhllStop] Stop of %s deactivated for short position, deactivation ratio %s", s.Symbol, s.DeactivationRatio.Percentage()) + } else if closePrice.Compare(position.AverageCost.Mul(r_active)) > 0 { s.activated = false - Notify("[hhllStop] Stop of %s deactivated for short position, deactivation ratio %s:", s.Symbol, s.DeactivationRatio.Percentage()) + Notify("[hhllStop] Stop of %s deactivated for short position, activation ratio %s", s.Symbol, s.ActivationRatio.Percentage()) } } } else { if position.IsLong() { - r := fixedpoint.One.Add(s.ActivationRatio) - if closePrice.Compare(position.AverageCost.Mul(r)) >= 0 { + r_deactive := fixedpoint.One.Add(s.DeactivationRatio) + r_active := fixedpoint.One.Add(s.ActivationRatio) + if closePrice.Compare(position.AverageCost.Mul(r_active)) >= 0 && closePrice.Compare(position.AverageCost.Mul(r_deactive)) < 0 { s.activated = true - Notify("[hhllStop] %s stop is activated for long position, activation ratio %s:", s.Symbol, s.ActivationRatio.Percentage()) + Notify("[hhllStop] %s stop is activated for long position, activation ratio %s, deactivation ratio %s", s.Symbol, s.ActivationRatio.Percentage(), s.DeactivationRatio.Percentage()) } } else if position.IsShort() { - r := fixedpoint.One.Sub(s.ActivationRatio) + r_deactive := fixedpoint.One.Sub(s.DeactivationRatio) + r_active := fixedpoint.One.Sub(s.ActivationRatio) // for short position, if the close price is less than the activation price then this is a profit position. - if closePrice.Compare(position.AverageCost.Mul(r)) <= 0 { + if closePrice.Compare(position.AverageCost.Mul(r_active)) <= 0 && closePrice.Compare(position.AverageCost.Mul(r_deactive)) > 0 { s.activated = true - Notify("[hhllStop] %s stop is activated for short position, activation ratio %s:", s.Symbol, s.ActivationRatio.Percentage()) + Notify("[hhllStop] %s stop is activated for short position, activation ratio %s, deactivation ratio %s", s.Symbol, s.ActivationRatio.Percentage(), s.DeactivationRatio.Percentage()) } } } From 43c49aa41ddb709898beaef8cc7901822e57506a Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 30 Jun 2023 13:51:47 +0800 Subject: [PATCH 1091/1392] exit/hhllStop: readability --- pkg/bbgo/exit_hh_ll_stop.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index a5e6ef37b1..7431f21d5b 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -62,47 +62,76 @@ func (s *HigherHighLowerLowStop) Subscribe(session *ExchangeSession) { // determine whether this stop should be activated func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, closePrice fixedpoint.Value) { if position.IsClosed() || position.IsDust(closePrice) { + s.activated = false + } else if s.activated { + if position.IsLong() { + r_deactive := fixedpoint.One.Add(s.DeactivationRatio) r_active := fixedpoint.One.Add(s.ActivationRatio) + if closePrice.Compare(position.AverageCost.Mul(r_deactive)) >= 0 { + s.activated = false Notify("[hhllStop] Stop of %s deactivated for long position, deactivation ratio %s", s.Symbol, s.DeactivationRatio.Percentage()) + } else if closePrice.Compare(position.AverageCost.Mul(r_active)) < 0 { + s.activated = false Notify("[hhllStop] Stop of %s deactivated for long position, activation ratio %s", s.Symbol, s.ActivationRatio.Percentage()) + } + } else if position.IsShort() { + r_deactive := fixedpoint.One.Sub(s.DeactivationRatio) r_active := fixedpoint.One.Sub(s.ActivationRatio) + // for short position, if the close price is less than the activation price then this is a profit position. if closePrice.Compare(position.AverageCost.Mul(r_deactive)) <= 0 { + s.activated = false Notify("[hhllStop] Stop of %s deactivated for short position, deactivation ratio %s", s.Symbol, s.DeactivationRatio.Percentage()) + } else if closePrice.Compare(position.AverageCost.Mul(r_active)) > 0 { + s.activated = false Notify("[hhllStop] Stop of %s deactivated for short position, activation ratio %s", s.Symbol, s.ActivationRatio.Percentage()) + } + } } else { + if position.IsLong() { + r_deactive := fixedpoint.One.Add(s.DeactivationRatio) r_active := fixedpoint.One.Add(s.ActivationRatio) + if closePrice.Compare(position.AverageCost.Mul(r_active)) >= 0 && closePrice.Compare(position.AverageCost.Mul(r_deactive)) < 0 { + s.activated = true Notify("[hhllStop] %s stop is activated for long position, activation ratio %s, deactivation ratio %s", s.Symbol, s.ActivationRatio.Percentage(), s.DeactivationRatio.Percentage()) + } + } else if position.IsShort() { + r_deactive := fixedpoint.One.Sub(s.DeactivationRatio) r_active := fixedpoint.One.Sub(s.ActivationRatio) + // for short position, if the close price is less than the activation price then this is a profit position. if closePrice.Compare(position.AverageCost.Mul(r_active)) <= 0 && closePrice.Compare(position.AverageCost.Mul(r_deactive)) > 0 { + s.activated = true Notify("[hhllStop] %s stop is activated for short position, activation ratio %s, deactivation ratio %s", s.Symbol, s.ActivationRatio.Percentage(), s.DeactivationRatio.Percentage()) + } + } + } } From 936a3c95d96b4ad3eb57a301f03735faee6d0aec Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 30 Jun 2023 13:55:07 +0800 Subject: [PATCH 1092/1392] exit/hhllStop: readability --- pkg/bbgo/exit_hh_ll_stop.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index 7431f21d5b..64069dcab8 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -64,8 +64,11 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close if position.IsClosed() || position.IsDust(closePrice) { s.activated = false + return - } else if s.activated { + } + + if s.activated { if position.IsLong() { From 12e3e9b5f89ec9f87bde874a100737e40ad367b6 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 30 Jun 2023 14:03:46 +0800 Subject: [PATCH 1093/1392] exit/hhllStop: readability --- pkg/bbgo/exit_hh_ll_stop.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index 64069dcab8..b29c20d203 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -61,6 +61,7 @@ func (s *HigherHighLowerLowStop) Subscribe(session *ExchangeSession) { // updateActivated checks the position cost against the close price, activation ratio, and deactivation ratio to // determine whether this stop should be activated func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, closePrice fixedpoint.Value) { + // deactivate when no position if position.IsClosed() || position.IsDust(closePrice) { s.activated = false @@ -68,19 +69,27 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close } + // activation/deactivation price + var price_deactive fixedpoint.Value + var price_active fixedpoint.Value + if position.IsLong() { + price_deactive = position.AverageCost.Mul(fixedpoint.One.Add(s.DeactivationRatio)) + price_active = position.AverageCost.Mul(fixedpoint.One.Add(s.ActivationRatio)) + } else { + price_deactive = position.AverageCost.Mul(fixedpoint.One.Sub(s.DeactivationRatio)) + price_active = position.AverageCost.Mul(fixedpoint.One.Sub(s.ActivationRatio)) + } + if s.activated { if position.IsLong() { - r_deactive := fixedpoint.One.Add(s.DeactivationRatio) - r_active := fixedpoint.One.Add(s.ActivationRatio) - - if closePrice.Compare(position.AverageCost.Mul(r_deactive)) >= 0 { + if closePrice.Compare(price_deactive) >= 0 { s.activated = false Notify("[hhllStop] Stop of %s deactivated for long position, deactivation ratio %s", s.Symbol, s.DeactivationRatio.Percentage()) - } else if closePrice.Compare(position.AverageCost.Mul(r_active)) < 0 { + } else if closePrice.Compare(price_active) < 0 { s.activated = false Notify("[hhllStop] Stop of %s deactivated for long position, activation ratio %s", s.Symbol, s.ActivationRatio.Percentage()) @@ -89,16 +98,13 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close } else if position.IsShort() { - r_deactive := fixedpoint.One.Sub(s.DeactivationRatio) - r_active := fixedpoint.One.Sub(s.ActivationRatio) - // for short position, if the close price is less than the activation price then this is a profit position. - if closePrice.Compare(position.AverageCost.Mul(r_deactive)) <= 0 { + if closePrice.Compare(price_deactive) <= 0 { s.activated = false Notify("[hhllStop] Stop of %s deactivated for short position, deactivation ratio %s", s.Symbol, s.DeactivationRatio.Percentage()) - } else if closePrice.Compare(position.AverageCost.Mul(r_active)) > 0 { + } else if closePrice.Compare(price_active) > 0 { s.activated = false Notify("[hhllStop] Stop of %s deactivated for short position, activation ratio %s", s.Symbol, s.ActivationRatio.Percentage()) @@ -110,10 +116,7 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close if position.IsLong() { - r_deactive := fixedpoint.One.Add(s.DeactivationRatio) - r_active := fixedpoint.One.Add(s.ActivationRatio) - - if closePrice.Compare(position.AverageCost.Mul(r_active)) >= 0 && closePrice.Compare(position.AverageCost.Mul(r_deactive)) < 0 { + if closePrice.Compare(price_active) >= 0 && closePrice.Compare(price_deactive) < 0 { s.activated = true Notify("[hhllStop] %s stop is activated for long position, activation ratio %s, deactivation ratio %s", s.Symbol, s.ActivationRatio.Percentage(), s.DeactivationRatio.Percentage()) @@ -122,11 +125,8 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close } else if position.IsShort() { - r_deactive := fixedpoint.One.Sub(s.DeactivationRatio) - r_active := fixedpoint.One.Sub(s.ActivationRatio) - // for short position, if the close price is less than the activation price then this is a profit position. - if closePrice.Compare(position.AverageCost.Mul(r_active)) <= 0 && closePrice.Compare(position.AverageCost.Mul(r_deactive)) > 0 { + if closePrice.Compare(price_active) <= 0 && closePrice.Compare(price_deactive) > 0 { s.activated = true Notify("[hhllStop] %s stop is activated for short position, activation ratio %s, deactivation ratio %s", s.Symbol, s.ActivationRatio.Percentage(), s.DeactivationRatio.Percentage()) From 2fe19119a7bb9b70684866e948880db28886c1ce Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 30 Jun 2023 14:10:25 +0800 Subject: [PATCH 1094/1392] exit/hhllStop: avoid using underscore in variable names --- pkg/bbgo/exit_hh_ll_stop.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index b29c20d203..4c0e9f66c2 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -70,26 +70,26 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close } // activation/deactivation price - var price_deactive fixedpoint.Value - var price_active fixedpoint.Value + var priceDeactive fixedpoint.Value + var priceActive fixedpoint.Value if position.IsLong() { - price_deactive = position.AverageCost.Mul(fixedpoint.One.Add(s.DeactivationRatio)) - price_active = position.AverageCost.Mul(fixedpoint.One.Add(s.ActivationRatio)) + priceDeactive = position.AverageCost.Mul(fixedpoint.One.Add(s.DeactivationRatio)) + priceActive = position.AverageCost.Mul(fixedpoint.One.Add(s.ActivationRatio)) } else { - price_deactive = position.AverageCost.Mul(fixedpoint.One.Sub(s.DeactivationRatio)) - price_active = position.AverageCost.Mul(fixedpoint.One.Sub(s.ActivationRatio)) + priceDeactive = position.AverageCost.Mul(fixedpoint.One.Sub(s.DeactivationRatio)) + priceActive = position.AverageCost.Mul(fixedpoint.One.Sub(s.ActivationRatio)) } if s.activated { if position.IsLong() { - if closePrice.Compare(price_deactive) >= 0 { + if closePrice.Compare(priceDeactive) >= 0 { s.activated = false Notify("[hhllStop] Stop of %s deactivated for long position, deactivation ratio %s", s.Symbol, s.DeactivationRatio.Percentage()) - } else if closePrice.Compare(price_active) < 0 { + } else if closePrice.Compare(priceActive) < 0 { s.activated = false Notify("[hhllStop] Stop of %s deactivated for long position, activation ratio %s", s.Symbol, s.ActivationRatio.Percentage()) @@ -99,12 +99,12 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close } else if position.IsShort() { // for short position, if the close price is less than the activation price then this is a profit position. - if closePrice.Compare(price_deactive) <= 0 { + if closePrice.Compare(priceDeactive) <= 0 { s.activated = false Notify("[hhllStop] Stop of %s deactivated for short position, deactivation ratio %s", s.Symbol, s.DeactivationRatio.Percentage()) - } else if closePrice.Compare(price_active) > 0 { + } else if closePrice.Compare(priceActive) > 0 { s.activated = false Notify("[hhllStop] Stop of %s deactivated for short position, activation ratio %s", s.Symbol, s.ActivationRatio.Percentage()) @@ -116,7 +116,7 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close if position.IsLong() { - if closePrice.Compare(price_active) >= 0 && closePrice.Compare(price_deactive) < 0 { + if closePrice.Compare(priceActive) >= 0 && closePrice.Compare(priceDeactive) < 0 { s.activated = true Notify("[hhllStop] %s stop is activated for long position, activation ratio %s, deactivation ratio %s", s.Symbol, s.ActivationRatio.Percentage(), s.DeactivationRatio.Percentage()) @@ -126,7 +126,7 @@ func (s *HigherHighLowerLowStop) updateActivated(position *types.Position, close } else if position.IsShort() { // for short position, if the close price is less than the activation price then this is a profit position. - if closePrice.Compare(price_active) <= 0 && closePrice.Compare(price_deactive) > 0 { + if closePrice.Compare(priceActive) <= 0 && closePrice.Compare(priceDeactive) > 0 { s.activated = true Notify("[hhllStop] %s stop is activated for short position, activation ratio %s, deactivation ratio %s", s.Symbol, s.ActivationRatio.Percentage(), s.DeactivationRatio.Percentage()) From 334204b46a52748e8b81b31981ac24cab825ef8e Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 1 Jul 2023 13:24:23 +0800 Subject: [PATCH 1095/1392] bbgo: add deprecation warning --- pkg/bbgo/session.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 7f5c61929e..88bb1ec715 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -503,6 +503,8 @@ func (session *ExchangeSession) Indicators(symbol string) *IndicatorSet { } func (session *ExchangeSession) StandardIndicatorSet(symbol string) *StandardIndicatorSet { + log.Warnf("StandardIndicatorSet() is deprecated in v1.49.0 and which will be removed in the next version, please use Indicators() instead") + set, ok := session.standardIndicatorSets[symbol] if ok { return set From 3f7710303f46e952692b9607cebf524ab09d719d Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 2 Jul 2023 14:13:24 +0800 Subject: [PATCH 1096/1392] fix .Indicators nil map --- pkg/bbgo/session.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 88bb1ec715..0fe3444378 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -808,6 +808,7 @@ func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) err session.marketDataStores = make(map[string]*MarketDataStore) session.positions = make(map[string]*types.Position) session.standardIndicatorSets = make(map[string]*StandardIndicatorSet) + session.indicators = make(map[string]*IndicatorSet) session.orderStores = make(map[string]*OrderStore) session.OrderExecutor = &ExchangeOrderExecutor{ // copy the notification system so that we can route From 94e7e80cf02239fee1e2725ee81175c56b737650 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 2 Jul 2023 14:14:06 +0800 Subject: [PATCH 1097/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index f8cd27ff6c..998cf94394 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -58,4 +58,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 8ebf6da1e7..fb55c1bab9 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -41,4 +41,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 1dce55e94d..7d3b2a2a89 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -50,4 +50,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index c5b34fd5bd..55c4848d5a 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -40,4 +40,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index ddcc41575c..625fde3675 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -39,4 +39,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index b7e074697c..ff2bfdb1ef 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -49,4 +49,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index e4f5de6baf..d71de327b4 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -41,4 +41,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 9e46cd6c9a..1d5ab2ae67 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -48,4 +48,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 8cde124b86..024fe92d48 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -42,4 +42,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index 093b1f991d..7318bd535d 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -45,4 +45,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 3f531da159..629469c2cb 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -42,4 +42,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index 282a393910..5d94ef018d 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -41,4 +41,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index 8af3b7045c..94ba912301 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -38,4 +38,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index 62aea931a2..b1db273c0f 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -41,4 +41,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 7ff5b2efbb..fa728d4c57 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -41,4 +41,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index 8e9f997c57..d2996cb43f 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -41,4 +41,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index dfb8c95cb9..d80f06e053 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -40,4 +40,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index 5b99474784..1cefc37b5b 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -44,4 +44,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index 39d306797b..058d1fb2e7 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -42,4 +42,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index 5046b06fac..77430f6309 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -40,4 +40,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index 05a0ffca0f..c3206629f9 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -49,4 +49,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index a0d3dae51a..9b3414c08b 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -51,4 +51,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 6ddd877976..49f7f3df53 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -46,4 +46,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index e676746c7d..d1ff8a3b45 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -42,4 +42,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index 20d67153aa..c74dbb118a 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -42,4 +42,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index ec300938d1..23a16f1b1e 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -40,4 +40,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 6764c81634..703a6d2f36 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -42,4 +42,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index 4c13aa131e..40e0b8e359 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -40,4 +40,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index 4881f38009..a7e2eed90d 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -39,4 +39,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 30-Jun-2023 +###### Auto generated by spf13/cobra on 2-Jul-2023 From 471df81b299b54606ebd49f98f173c1c6019bb1a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 2 Jul 2023 14:14:06 +0800 Subject: [PATCH 1098/1392] bump version to v1.50.1 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 231f0771d9..06cc47fe03 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.50.0-3929eb20-dev" +const Version = "v1.50.1-3f771030-dev" -const VersionGitRef = "3929eb20" +const VersionGitRef = "3f771030" diff --git a/pkg/version/version.go b/pkg/version/version.go index 3d913533f8..76f20909e7 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.50.0-3929eb20" +const Version = "v1.50.1-3f771030" -const VersionGitRef = "3929eb20" +const VersionGitRef = "3f771030" From 19552b066e4b66f5230641e0b076216d569f3a11 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 2 Jul 2023 14:14:06 +0800 Subject: [PATCH 1099/1392] add v1.50.1 release note --- doc/release/v1.50.1.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 doc/release/v1.50.1.md diff --git a/doc/release/v1.50.1.md b/doc/release/v1.50.1.md new file mode 100644 index 0000000000..14ed1eaf23 --- /dev/null +++ b/doc/release/v1.50.1.md @@ -0,0 +1,4 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.50.0...main) + + - [#1216](https://github.com/c9s/bbgo/pull/1216): Fix: exit/hhllStop bugs + - Fixed indicator nil map issue From d60dbe5e0b5a94c616e545736aa288c4c57d55e3 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 3 Jul 2023 15:07:34 +0800 Subject: [PATCH 1100/1392] refactor interval slice code and add sort test --- pkg/cmd/backtest.go | 30 +++++++++++++++++++----------- pkg/types/interval.go | 7 +++++++ pkg/types/interval_test.go | 11 +++++++++++ 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index b8d6886039..3982aab5d2 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -697,29 +697,37 @@ func confirmation(s string) bool { } } +func getExchangeIntervals(ex types.Exchange) map[types.Interval]int { + exCustom, ok := ex.(types.CustomIntervalProvider) + if ok { + return exCustom.SupportedInterval() + } + return types.SupportedIntervals +} + +func sortIntervals(intervals []types.Interval) types.IntervalSlice { + sort.Slice(intervals, func(i, j int) bool { + return intervals[i].Duration() < intervals[j].Duration() + }) + return intervals +} + func sync(ctx context.Context, userConfig *bbgo.Config, backtestService *service.BacktestService, sourceExchanges map[types.ExchangeName]types.Exchange, syncFrom, syncTo time.Time) error { for _, symbol := range userConfig.Backtest.Symbols { for _, sourceExchange := range sourceExchanges { - exCustom, ok := sourceExchange.(types.CustomIntervalProvider) + var supportIntervals = getExchangeIntervals(sourceExchange) - var supportIntervals map[types.Interval]int - if ok { - supportIntervals = exCustom.SupportedInterval() - } else { - supportIntervals = types.SupportedIntervals - } if !userConfig.Backtest.SyncSecKLines { delete(supportIntervals, types.Interval1s) } // sort intervals - var intervals []types.Interval + var intervals types.IntervalSlice for interval := range supportIntervals { intervals = append(intervals, interval) } - sort.Slice(intervals, func(i, j int) bool { - return intervals[i].Duration() < intervals[j].Duration() - }) + + intervals.Sort() for _, interval := range intervals { if err := backtestService.Sync(ctx, sourceExchange, symbol, interval, syncFrom, syncTo); err != nil { diff --git a/pkg/types/interval.go b/pkg/types/interval.go index 10c0243afd..054db6d5fd 100644 --- a/pkg/types/interval.go +++ b/pkg/types/interval.go @@ -3,6 +3,7 @@ package types import ( "encoding/json" "fmt" + "sort" "strings" "time" ) @@ -80,6 +81,12 @@ func (i Interval) String() string { type IntervalSlice []Interval +func (s IntervalSlice) Sort() { + sort.Slice(s, func(i, j int) bool { + return s[i].Duration() < s[j].Duration() + }) +} + func (s IntervalSlice) StringSlice() (slice []string) { for _, interval := range s { slice = append(slice, `"`+interval.String()+`"`) diff --git a/pkg/types/interval_test.go b/pkg/types/interval_test.go index e87ab1575d..f181d6ef92 100644 --- a/pkg/types/interval_test.go +++ b/pkg/types/interval_test.go @@ -13,3 +13,14 @@ func TestParseInterval(t *testing.T) { assert.Equal(t, ParseInterval("72d"), 72*24*60*60) assert.Equal(t, ParseInterval("3Mo"), 3*30*24*60*60) } + +func TestIntervalSort(t *testing.T) { + intervals := IntervalSlice{Interval2h, Interval1m, Interval1h, Interval1d} + intervals.Sort() + assert.Equal(t, IntervalSlice{ + Interval1m, + Interval1h, + Interval2h, + Interval1d, + }, intervals) +} From ea130e434c8676ecb3b86ba742c52206e211a2f6 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 3 Jul 2023 15:14:48 +0800 Subject: [PATCH 1101/1392] types,cmd: add IntervalMap type to refactor the interval code --- pkg/cmd/backtest.go | 16 ++-------------- pkg/types/interval.go | 12 +++++++++++- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 3982aab5d2..6ebd73a136 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "sort" "strings" "syscall" "time" @@ -697,7 +696,7 @@ func confirmation(s string) bool { } } -func getExchangeIntervals(ex types.Exchange) map[types.Interval]int { +func getExchangeIntervals(ex types.Exchange) types.IntervalMap { exCustom, ok := ex.(types.CustomIntervalProvider) if ok { return exCustom.SupportedInterval() @@ -705,13 +704,6 @@ func getExchangeIntervals(ex types.Exchange) map[types.Interval]int { return types.SupportedIntervals } -func sortIntervals(intervals []types.Interval) types.IntervalSlice { - sort.Slice(intervals, func(i, j int) bool { - return intervals[i].Duration() < intervals[j].Duration() - }) - return intervals -} - func sync(ctx context.Context, userConfig *bbgo.Config, backtestService *service.BacktestService, sourceExchanges map[types.ExchangeName]types.Exchange, syncFrom, syncTo time.Time) error { for _, symbol := range userConfig.Backtest.Symbols { for _, sourceExchange := range sourceExchanges { @@ -722,11 +714,7 @@ func sync(ctx context.Context, userConfig *bbgo.Config, backtestService *service } // sort intervals - var intervals types.IntervalSlice - for interval := range supportIntervals { - intervals = append(intervals, interval) - } - + var intervals = supportIntervals.Slice() intervals.Sort() for _, interval := range intervals { diff --git a/pkg/types/interval.go b/pkg/types/interval.go index 054db6d5fd..338006193e 100644 --- a/pkg/types/interval.go +++ b/pkg/types/interval.go @@ -142,7 +142,17 @@ func ParseInterval(input Interval) int { return t } -var SupportedIntervals = map[Interval]int{ +type IntervalMap map[Interval]int + +func (m IntervalMap) Slice() (slice IntervalSlice) { + for interval := range m { + slice = append(slice, interval) + } + + return slice +} + +var SupportedIntervals = IntervalMap{ Interval1s: 1, Interval1m: 1 * 60, Interval3m: 3 * 60, From 532b6f783d5be49e013ab5a079df39045746e4e2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 3 Jul 2023 15:27:37 +0800 Subject: [PATCH 1102/1392] types: remove unused Interval1ms --- pkg/types/interval.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/types/interval.go b/pkg/types/interval.go index 338006193e..3410b0657e 100644 --- a/pkg/types/interval.go +++ b/pkg/types/interval.go @@ -94,7 +94,6 @@ func (s IntervalSlice) StringSlice() (slice []string) { return slice } -var Interval1ms = Interval("1ms") var Interval1s = Interval("1s") var Interval1m = Interval("1m") var Interval3m = Interval("3m") From b877d07f74ff88c08a16a99bc0078c433106983a Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 3 Jul 2023 16:06:04 +0800 Subject: [PATCH 1103/1392] exit/hhllStop: log hhll detection instead of notify --- pkg/bbgo/exit_hh_ll_stop.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/exit_hh_ll_stop.go b/pkg/bbgo/exit_hh_ll_stop.go index 4c0e9f66c2..6d6847af54 100644 --- a/pkg/bbgo/exit_hh_ll_stop.go +++ b/pkg/bbgo/exit_hh_ll_stop.go @@ -146,10 +146,10 @@ func (s *HigherHighLowerLowStop) updateHighLowNumber(kline types.KLine) { low := kline.GetLow() if s.klines.GetHigh().Compare(high) < 0 { s.highLows = append(s.highLows, types.DirectionUp) - Notify("[hhllStop] detected %s new higher high %f", s.Symbol, high.Float64()) + log.Debugf("[hhllStop] detected %s new higher high %f", s.Symbol, high.Float64()) } else if s.klines.GetLow().Compare(low) > 0 { s.highLows = append(s.highLows, types.DirectionDown) - Notify("[hhllStop] detected %s new lower low %f", s.Symbol, low.Float64()) + log.Debugf("[hhllStop] detected %s new lower low %f", s.Symbol, low.Float64()) } else { s.highLows = append(s.highLows, types.DirectionNone) } From 3052dd5add875fde83aa903515d77d74219ffae0 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 3 Jul 2023 16:22:01 +0800 Subject: [PATCH 1104/1392] scmaker: add liquiditySkew support --- config/scmaker.yaml | 18 ++++++++------- pkg/strategy/scmaker/strategy.go | 39 ++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index 9f8b2991c1..12e40fead4 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -1,5 +1,5 @@ sessions: - binance: + max: exchange: max envVarPrefix: max makerFeeRate: 0% @@ -15,6 +15,15 @@ exchangeStrategies: ## liquidityUpdateInterval is the interval for updating liquidity orders liquidityUpdateInterval: 1h + liquiditySkew: 1.5 + numOfLiquidityLayers: 10 + liquidityLayerTickSize: 0.0001 + liquidityScale: + exp: + domain: [0, 9] + range: [1, 4] + + maxExposure: 10_000 midPriceEMA: interval: 1h @@ -26,18 +35,11 @@ exchangeStrategies: window: 10 k: 1.0 - numOfLiquidityLayers: 10 - - liquidityLayerTickSize: 0.0001 strengthInterval: 1m minProfit: 0.01% - liquidityScale: - exp: - domain: [0, 9] - range: [1, 4] backtest: sessions: diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 5248a6a78c..38056b630e 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -51,6 +51,7 @@ type Strategy struct { MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` LiquidityLayerTickSize fixedpoint.Value `json:"liquidityLayerTickSize"` + LiquiditySkew fixedpoint.Value `json:"liquiditySkew"` MaxExposure fixedpoint.Value `json:"maxExposure"` @@ -292,6 +293,15 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { return } + if ticker.Buy.IsZero() && ticker.Sell.IsZero() { + ticker.Sell = ticker.Last.Add(s.Market.TickSize) + ticker.Buy = ticker.Last.Sub(s.Market.TickSize) + } else if ticker.Buy.IsZero() { + ticker.Buy = ticker.Sell.Sub(s.Market.TickSize) + } else if ticker.Sell.IsZero() { + ticker.Sell = ticker.Buy.Add(s.Market.TickSize) + } + if _, err := s.session.UpdateAccount(ctx); err != nil { logErr(err, "unable to update account") return @@ -310,7 +320,19 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { log.Infof("spread: %f mid price ema: %f boll band width: %f", spread.Float64(), midPriceEMA, bandWidth) - n := s.liquidityScale.Sum(1.0) + sum := s.liquidityScale.Sum(1.0) + askSum := sum + bidSum := sum + + log.Infof("liquidity sum: %f / %f", askSum, bidSum) + + skew := s.LiquiditySkew.Float64() + useSkew := !s.LiquiditySkew.IsZero() + if useSkew { + askSum = sum / skew + bidSum = sum * skew + log.Infof("adjusted liqudity skew: %f / %f", askSum, bidSum) + } var bidPrices []fixedpoint.Value var askPrices []fixedpoint.Value @@ -319,17 +341,19 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { for i := 0; i <= s.NumOfLiquidityLayers; i++ { fi := fixedpoint.NewFromInt(int64(i)) sp := tickSize.Mul(fi) + spreadBidPrice := midPrice.Sub(sp) + spreadAskPrice := midPrice.Add(sp) bidPrice := ticker.Buy askPrice := ticker.Sell if i == s.NumOfLiquidityLayers { bwf := fixedpoint.NewFromFloat(bandWidth) - bidPrice = midPrice.Add(bwf.Neg()) - askPrice = midPrice.Add(bwf) + bidPrice = fixedpoint.Min(midPrice.Add(bwf.Neg()), spreadBidPrice) + askPrice = fixedpoint.Max(midPrice.Add(bwf), spreadAskPrice) } else if i > 0 { - bidPrice = midPrice.Sub(sp) - askPrice = midPrice.Add(sp) + bidPrice = spreadBidPrice + askPrice = spreadAskPrice } if i > 0 && bidPrice.Compare(ticker.Buy) > 0 { @@ -378,8 +402,8 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { } } - askX := availableBase.Float64() / n - bidX := availableQuote.Float64() / (n * (fixedpoint.Sum(bidPrices).Float64())) + askX := availableBase.Float64() / askSum + bidX := availableQuote.Float64() / (bidSum * (fixedpoint.Sum(bidPrices).Float64())) askX = math.Trunc(askX*1e8) / 1e8 bidX = math.Trunc(bidX*1e8) / 1e8 @@ -450,6 +474,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { } s.liquidityOrderBook.Add(createdOrders...) + log.Infof("%d liq orders are placed successfully", len(liqOrders)) } func profitProtectedPrice(side types.SideType, averageCost, price, feeRate, minProfit fixedpoint.Value) fixedpoint.Value { From ae3f3715512b563a6fc55c544e505f6e8002fb6f Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 3 Jul 2023 17:09:13 +0800 Subject: [PATCH 1105/1392] all: refactor risk control and integrate risk control into scmaker --- config/scmaker.yaml | 18 +++++++ doc/topics/riskcontrols.md | 62 ++++++++++++---------- pkg/indicator/float64series.go | 4 +- pkg/risk/riskcontrol/circuit_break.go | 31 +++++------ pkg/risk/riskcontrol/circuit_break_test.go | 4 +- pkg/risk/riskcontrol/position.go | 13 +++-- pkg/strategy/scmaker/strategy.go | 45 ++++++++++++++++ 7 files changed, 125 insertions(+), 52 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index 12e40fead4..b735153477 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -23,8 +23,26 @@ exchangeStrategies: domain: [0, 9] range: [1, 4] + + ## maxExposure controls how much balance should be used for placing the maker orders maxExposure: 10_000 + ## circuitBreakEMA is used for calculating the price for circuitBreak + circuitBreakEMA: + interval: 1m + window: 14 + + ## circuitBreakLossThreshold is the maximum loss threshold for realized+unrealized PnL + circuitBreakLossThreshold: -10.0 + + ## positionHardLimit is the maximum position limit + positionHardLimit: 500.0 + + ## maxPositionQuantity is the maximum quantity per order that could be controlled in positionHardLimit, + ## this parameter is used with positionHardLimit togerther + maxPositionQuantity: 10.0 + + midPriceEMA: interval: 1h window: 99 diff --git a/doc/topics/riskcontrols.md b/doc/topics/riskcontrols.md index 8f548cc1a0..90dfea94e5 100644 --- a/doc/topics/riskcontrols.md +++ b/doc/topics/riskcontrols.md @@ -10,22 +10,26 @@ Two types of risk controls for strategies is created: ### 2. Position-Limit Risk Control Initialization: -``` - s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.HardLimit, s.Quantity, s.orderExecutor.TradeCollector()) - s.positionRiskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { - createdOrders, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Market: s.Market, - Side: side, - Type: types.OrderTypeMarket, - Quantity: quantity, - }) - if err != nil { - log.WithError(err).Errorf("failed to submit orders") - return - } - log.Infof("created orders: %+v", createdOrders) + +```go +s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.PositionHardLimit, s.MaxPositionQuantity, s.orderExecutor.TradeCollector()) + +s.positionRiskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Market: s.Market, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, }) + + if err != nil { + log.WithError(err).Errorf("failed to submit orders") + return + } + + log.Infof("created position release orders: %+v", createdOrders) +}) ``` Strategy should provide OnReleasePosition callback, which will be called when position (positive or negative) is over hard limit. @@ -41,27 +45,27 @@ It calculates buy and sell quantity shrinking by hard limit and position. ### 3. Circuit-Break Risk Control Initialization + +```go +s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( + s.Position, + session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), + s.CircuitBreakLossThreshold, + s.ProfitStats) ``` - s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( - s.Position, - session.StandardIndicatorSet(s.Symbol).EWMA( - types.IntervalWindow{ - Window: EWMAWindow, - Interval: types.Interval1m, - }), - s.CircuitBreakCondition, - s.ProfitStats) -``` + Should pass in position and profit states. Also need an price EWMA to calculate unrealized profit. Validate parameters: + ``` - if s.CircuitBreakCondition.Float64() > 0 { - return fmt.Errorf("circuitBreakCondition should be non-positive") - } - return nil +if s.CircuitBreakLossThreshold.Float64() > 0 { + return fmt.Errorf("circuitBreakLossThreshold should be non-positive") +} +return nil ``` + Circuit break condition should be non-greater than zero. Check for circuit break before submitting orders: diff --git a/pkg/indicator/float64series.go b/pkg/indicator/float64series.go index c198e8e8d1..8035acb964 100644 --- a/pkg/indicator/float64series.go +++ b/pkg/indicator/float64series.go @@ -67,5 +67,7 @@ func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) { } } - f.Subscribe(source, c) + if source != nil { + f.Subscribe(source, c) + } } diff --git a/pkg/risk/riskcontrol/circuit_break.go b/pkg/risk/riskcontrol/circuit_break.go index db528b3a8d..a89833ed8a 100644 --- a/pkg/risk/riskcontrol/circuit_break.go +++ b/pkg/risk/riskcontrol/circuit_break.go @@ -1,40 +1,41 @@ package riskcontrol import ( + log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" - log "github.com/sirupsen/logrus" ) type CircuitBreakRiskControl struct { // Since price could be fluctuated large, // use an EWMA to smooth it in running time - price *indicator.EWMA - position *types.Position - profitStats *types.ProfitStats - breakCondition fixedpoint.Value + price *indicator.EWMAStream + position *types.Position + profitStats *types.ProfitStats + lossThreshold fixedpoint.Value } func NewCircuitBreakRiskControl( position *types.Position, - price *indicator.EWMA, - breakCondition fixedpoint.Value, + price *indicator.EWMAStream, + lossThreshold fixedpoint.Value, profitStats *types.ProfitStats) *CircuitBreakRiskControl { return &CircuitBreakRiskControl{ - price: price, - position: position, - profitStats: profitStats, - breakCondition: breakCondition, + price: price, + position: position, + profitStats: profitStats, + lossThreshold: lossThreshold, } } // IsHalted returns whether we reached the circuit break condition set for this day? func (c *CircuitBreakRiskControl) IsHalted() bool { var unrealized = c.position.UnrealizedProfit(fixedpoint.NewFromFloat(c.price.Last(0))) - log.Infof("[CircuitBreakRiskControl] Realized P&L = %v, Unrealized P&L = %v\n", - c.profitStats.TodayPnL, - unrealized) - return unrealized.Add(c.profitStats.TodayPnL).Compare(c.breakCondition) <= 0 + log.Infof("[CircuitBreakRiskControl] realized PnL = %f, unrealized PnL = %f\n", + c.profitStats.TodayPnL.Float64(), + unrealized.Float64()) + return unrealized.Add(c.profitStats.TodayPnL).Compare(c.lossThreshold) <= 0 } diff --git a/pkg/risk/riskcontrol/circuit_break_test.go b/pkg/risk/riskcontrol/circuit_break_test.go index 8d2252634d..1ee01c59b0 100644 --- a/pkg/risk/riskcontrol/circuit_break_test.go +++ b/pkg/risk/riskcontrol/circuit_break_test.go @@ -19,8 +19,8 @@ func Test_IsHalted(t *testing.T) { ) window := types.IntervalWindow{Window: 30, Interval: types.Interval1m} - priceEWMA := &indicator.EWMA{IntervalWindow: window} - priceEWMA.Update(price) + priceEWMA := indicator.EWMA2(nil, window.Window) + priceEWMA.PushAndEmit(price) cases := []struct { name string diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go index 44022c55d5..447e037ceb 100644 --- a/pkg/risk/riskcontrol/position.go +++ b/pkg/risk/riskcontrol/position.go @@ -1,10 +1,11 @@ package riskcontrol import ( + log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - log "github.com/sirupsen/logrus" ) //go:generate callbackgen -type PositionRiskControl @@ -20,23 +21,25 @@ func NewPositionRiskControl(hardLimit, quantity fixedpoint.Value, tradeCollector hardLimit: hardLimit, quantity: quantity, } - // register position update handler: check if position is over hardlimit + + // register position update handler: check if position is over the hard limit tradeCollector.OnPositionUpdate(func(position *types.Position) { if fixedpoint.Compare(position.Base, hardLimit) > 0 { - log.Infof("Position %v is over hardlimit %v, releasing:\n", position.Base, hardLimit) + log.Infof("position %f is over hardlimit %f, releasing position...", position.Base.Float64(), hardLimit.Float64()) p.EmitReleasePosition(position.Base.Sub(hardLimit), types.SideTypeSell) } else if fixedpoint.Compare(position.Base, hardLimit.Neg()) < 0 { - log.Infof("Position %v is over hardlimit %v, releasing:\n", position.Base, hardLimit) + log.Infof("position %f is over hardlimit %f, releasing position...", position.Base.Float64(), hardLimit.Float64()) p.EmitReleasePosition(position.Base.Neg().Sub(hardLimit), types.SideTypeBuy) } }) + return p } // ModifiedQuantity returns quantity controlled by position risks // For buy orders, mod quantity = min(hardlimit - position, quanity), limiting by positive position // For sell orders, mod quantity = min(hardlimit - (-position), quanity), limiting by negative position -func (p *PositionRiskControl) ModifiedQuantity(position fixedpoint.Value) (buyQuanity, sellQuantity fixedpoint.Value) { +func (p *PositionRiskControl) ModifiedQuantity(position fixedpoint.Value) (buyQuantity, sellQuantity fixedpoint.Value) { return fixedpoint.Min(p.hardLimit.Sub(position), p.quantity), fixedpoint.Min(p.hardLimit.Add(position), p.quantity) } diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 38056b630e..df1e775ccf 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -11,6 +11,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/risk/riskcontrol" "github.com/c9s/bbgo/pkg/types" ) @@ -57,6 +58,12 @@ type Strategy struct { MinProfit fixedpoint.Value `json:"minProfit"` + // risk related parameters + PositionHardLimit fixedpoint.Value `json:"positionHardLimit"` + MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"` + CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"` + CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"` + Position *types.Position `json:"position,omitempty" persistence:"position"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` @@ -71,6 +78,9 @@ type Strategy struct { ewma *indicator.EWMAStream boll *indicator.BOLLStream intensity *IntensityStream + + positionRiskControl *riskcontrol.PositionRiskControl + circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl } func (s *Strategy) ID() string { @@ -126,6 +136,36 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ProfitStats = types.NewProfitStats(s.Market) } + if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { + log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") + s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.PositionHardLimit, s.MaxPositionQuantity, s.orderExecutor.TradeCollector()) + s.positionRiskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Market: s.Market, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, + }) + + if err != nil { + log.WithError(err).Errorf("failed to submit orders") + return + } + + log.Infof("created position release orders: %+v", createdOrders) + }) + } + + if !s.CircuitBreakLossThreshold.IsZero() { + log.Infof("circuitBreakLossThreshold is configured, setting up CircuitBreakRiskControl...") + s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( + s.Position, + session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), + s.CircuitBreakLossThreshold, + s.ProfitStats) + } + scale, err := s.LiquiditySlideRule.Scale() if err != nil { return err @@ -283,6 +323,11 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { } func (s *Strategy) placeLiquidityOrders(ctx context.Context) { + if s.circuitBreakRiskControl != nil && s.circuitBreakRiskControl.IsHalted() { + log.Warn("circuitBreakRiskControl: trading halted") + return + } + err := s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) if logErr(err, "unable to cancel orders") { return From 0426c18757d95cb1ef362849378c36af5339f2d2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 3 Jul 2023 17:39:42 +0800 Subject: [PATCH 1106/1392] scmaker: initialize order executor before we setup risk control --- pkg/risk/riskcontrol/position.go | 4 ++-- pkg/strategy/scmaker/strategy.go | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go index 447e037ceb..46d70d7124 100644 --- a/pkg/risk/riskcontrol/position.go +++ b/pkg/risk/riskcontrol/position.go @@ -37,8 +37,8 @@ func NewPositionRiskControl(hardLimit, quantity fixedpoint.Value, tradeCollector } // ModifiedQuantity returns quantity controlled by position risks -// For buy orders, mod quantity = min(hardlimit - position, quanity), limiting by positive position -// For sell orders, mod quantity = min(hardlimit - (-position), quanity), limiting by negative position +// For buy orders, mod quantity = min(hardLimit - position, quantity), limiting by positive position +// For sell orders, mod quantity = min(hardLimit - (-position), quantity), limiting by negative position func (p *PositionRiskControl) ModifiedQuantity(position fixedpoint.Value) (buyQuantity, sellQuantity fixedpoint.Value) { return fixedpoint.Min(p.hardLimit.Sub(position), p.quantity), fixedpoint.Min(p.hardLimit.Add(position), p.quantity) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index df1e775ccf..670268aa67 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -136,6 +136,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ProfitStats = types.NewProfitStats(s.Market) } + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.Bind() + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.PositionHardLimit, s.MaxPositionQuantity, s.orderExecutor.TradeCollector()) @@ -181,14 +189,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.liquidityScale = scale - s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) - s.orderExecutor.BindEnvironment(s.Environment) - s.orderExecutor.BindProfitStats(s.ProfitStats) - s.orderExecutor.Bind() - s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { - bbgo.Sync(ctx, s) - }) - s.initializeMidPriceEMA(session) s.initializePriceRangeBollinger(session) s.initializeIntensityIndicator(session) From c8ae36ddfcb0ae533688d4c05c269e19c0831e9e Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 4 Jul 2023 21:31:47 +0800 Subject: [PATCH 1107/1392] riskcontrol: move release position order submission into the pos risk control --- pkg/bbgo/order_executor_general.go | 2 +- pkg/risk/riskcontrol/position.go | 65 +++++++++++++++++++++------ pkg/risk/riskcontrol/position_test.go | 52 +++++++++++++-------- pkg/strategy/scmaker/strategy.go | 18 +------- 4 files changed, 87 insertions(+), 50 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index adf2771120..6a0de068e9 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -62,7 +62,7 @@ func NewGeneralOrderExecutor(session *ExchangeSession, symbol, strategy, strateg tradeCollector: NewTradeCollector(symbol, position, orderStore), } - if session.Margin { + if session != nil && session.Margin { executor.startMarginAssetUpdater(context.Background()) } diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go index 46d70d7124..9a71fe6b0b 100644 --- a/pkg/risk/riskcontrol/position.go +++ b/pkg/risk/riskcontrol/position.go @@ -1,6 +1,8 @@ package riskcontrol import ( + "context" + log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" @@ -10,36 +12,71 @@ import ( //go:generate callbackgen -type PositionRiskControl type PositionRiskControl struct { + orderExecutor *bbgo.GeneralOrderExecutor + + // hardLimit is the maximum base position you can hold hardLimit fixedpoint.Value - quantity fixedpoint.Value + + // sliceQuantity is the maximum quantity of the order you want to place. + // only used in the ModifiedQuantity method + sliceQuantity fixedpoint.Value releasePositionCallbacks []func(quantity fixedpoint.Value, side types.SideType) } -func NewPositionRiskControl(hardLimit, quantity fixedpoint.Value, tradeCollector *bbgo.TradeCollector) *PositionRiskControl { - p := &PositionRiskControl{ - hardLimit: hardLimit, - quantity: quantity, +func NewPositionRiskControl(hardLimit, quantity fixedpoint.Value, orderExecutor *bbgo.GeneralOrderExecutor) *PositionRiskControl { + control := &PositionRiskControl{ + orderExecutor: orderExecutor, + hardLimit: hardLimit, + sliceQuantity: quantity, } + control.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { + pos := orderExecutor.Position() + createdOrders, err := orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ + Symbol: pos.Symbol, + Market: pos.Market, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, + }) + + if err != nil { + log.WithError(err).Errorf("failed to submit orders") + return + } + + log.Infof("created position release orders: %+v", createdOrders) + }) + // register position update handler: check if position is over the hard limit - tradeCollector.OnPositionUpdate(func(position *types.Position) { + orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { if fixedpoint.Compare(position.Base, hardLimit) > 0 { log.Infof("position %f is over hardlimit %f, releasing position...", position.Base.Float64(), hardLimit.Float64()) - p.EmitReleasePosition(position.Base.Sub(hardLimit), types.SideTypeSell) + control.EmitReleasePosition(position.Base.Sub(hardLimit), types.SideTypeSell) } else if fixedpoint.Compare(position.Base, hardLimit.Neg()) < 0 { log.Infof("position %f is over hardlimit %f, releasing position...", position.Base.Float64(), hardLimit.Float64()) - p.EmitReleasePosition(position.Base.Neg().Sub(hardLimit), types.SideTypeBuy) + control.EmitReleasePosition(position.Base.Neg().Sub(hardLimit), types.SideTypeBuy) } }) - return p + return control } -// ModifiedQuantity returns quantity controlled by position risks -// For buy orders, mod quantity = min(hardLimit - position, quantity), limiting by positive position -// For sell orders, mod quantity = min(hardLimit - (-position), quantity), limiting by negative position +// ModifiedQuantity returns sliceQuantity controlled by position risks +// For buy orders, modify sliceQuantity = min(hardLimit - position, sliceQuantity), limiting by positive position +// For sell orders, modify sliceQuantity = min(hardLimit - (-position), sliceQuantity), limiting by negative position +// +// Pass the current base position to this method, and it returns the maximum sliceQuantity for placing the orders. +// This works for both Long/Short position func (p *PositionRiskControl) ModifiedQuantity(position fixedpoint.Value) (buyQuantity, sellQuantity fixedpoint.Value) { - return fixedpoint.Min(p.hardLimit.Sub(position), p.quantity), - fixedpoint.Min(p.hardLimit.Add(position), p.quantity) + if p.sliceQuantity.IsZero() { + buyQuantity = p.hardLimit.Sub(position) + sellQuantity = p.hardLimit.Add(position) + return buyQuantity, sellQuantity + } + + buyQuantity = fixedpoint.Min(p.hardLimit.Sub(position), p.sliceQuantity) + sellQuantity = fixedpoint.Min(p.hardLimit.Add(position), p.sliceQuantity) + return buyQuantity, sellQuantity } diff --git a/pkg/risk/riskcontrol/position_test.go b/pkg/risk/riskcontrol/position_test.go index fe0aa7f14d..6f8ab483a3 100644 --- a/pkg/risk/riskcontrol/position_test.go +++ b/pkg/risk/riskcontrol/position_test.go @@ -11,8 +11,17 @@ import ( ) func Test_ModifiedQuantity(t *testing.T) { - - riskControl := NewPositionRiskControl(fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2), &bbgo.TradeCollector{}) + pos := &types.Position{ + Market: types.Market{ + Symbol: "BTCUSDT", + PricePrecision: 8, + VolumePrecision: 8, + QuoteCurrency: "USDT", + BaseCurrency: "BTC", + }, + } + orderExecutor := bbgo.NewGeneralOrderExecutor(nil, "BTCUSDT", "strategy", "strategy-1", pos) + riskControl := NewPositionRiskControl(fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2), orderExecutor) cases := []struct { name string @@ -43,19 +52,6 @@ func Test_ModifiedQuantity(t *testing.T) { } func TestReleasePositionCallbacks(t *testing.T) { - - var position fixedpoint.Value - - tradeCollector := &bbgo.TradeCollector{} - riskControl := NewPositionRiskControl(fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2), tradeCollector) - riskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { - if side == types.SideTypeBuy { - position = position.Add(quantity) - } else { - position = position.Sub(quantity) - } - }) - cases := []struct { name string position fixedpoint.Value @@ -84,9 +80,29 @@ func TestReleasePositionCallbacks(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - position = tc.position - tradeCollector.EmitPositionUpdate(&types.Position{Base: tc.position}) - assert.Equal(t, tc.resultPosition, position) + pos := &types.Position{ + Base: tc.position, + Market: types.Market{ + Symbol: "BTCUSDT", + PricePrecision: 8, + VolumePrecision: 8, + QuoteCurrency: "USDT", + BaseCurrency: "BTC", + }, + } + + orderExecutor := bbgo.NewGeneralOrderExecutor(nil, "BTCUSDT", "strategy", "strategy-1", pos) + riskControl := NewPositionRiskControl(fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2), orderExecutor) + riskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { + if side == types.SideTypeBuy { + pos.Base = pos.Base.Add(quantity) + } else { + pos.Base = pos.Base.Sub(quantity) + } + }) + + orderExecutor.TradeCollector().EmitPositionUpdate(&types.Position{Base: tc.position}) + assert.Equal(t, tc.resultPosition, pos.Base) }) } } diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 670268aa67..94b5ccc2a8 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -146,23 +146,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") - s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.PositionHardLimit, s.MaxPositionQuantity, s.orderExecutor.TradeCollector()) - s.positionRiskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { - createdOrders, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Market: s.Market, - Side: side, - Type: types.OrderTypeMarket, - Quantity: quantity, - }) - - if err != nil { - log.WithError(err).Errorf("failed to submit orders") - return - } - - log.Infof("created position release orders: %+v", createdOrders) - }) + s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.PositionHardLimit, s.MaxPositionQuantity, s.orderExecutor) } if !s.CircuitBreakLossThreshold.IsZero() { From adbb6d7f93b69a0e6e53e177d7613db5e43ccb18 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 4 Jul 2023 21:32:34 +0800 Subject: [PATCH 1108/1392] riskcontrol: move parameter order --- pkg/risk/riskcontrol/position.go | 2 +- pkg/risk/riskcontrol/position_test.go | 4 ++-- pkg/strategy/scmaker/strategy.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go index 9a71fe6b0b..53b65e012f 100644 --- a/pkg/risk/riskcontrol/position.go +++ b/pkg/risk/riskcontrol/position.go @@ -24,7 +24,7 @@ type PositionRiskControl struct { releasePositionCallbacks []func(quantity fixedpoint.Value, side types.SideType) } -func NewPositionRiskControl(hardLimit, quantity fixedpoint.Value, orderExecutor *bbgo.GeneralOrderExecutor) *PositionRiskControl { +func NewPositionRiskControl(orderExecutor *bbgo.GeneralOrderExecutor, hardLimit, quantity fixedpoint.Value) *PositionRiskControl { control := &PositionRiskControl{ orderExecutor: orderExecutor, hardLimit: hardLimit, diff --git a/pkg/risk/riskcontrol/position_test.go b/pkg/risk/riskcontrol/position_test.go index 6f8ab483a3..106db68251 100644 --- a/pkg/risk/riskcontrol/position_test.go +++ b/pkg/risk/riskcontrol/position_test.go @@ -21,7 +21,7 @@ func Test_ModifiedQuantity(t *testing.T) { }, } orderExecutor := bbgo.NewGeneralOrderExecutor(nil, "BTCUSDT", "strategy", "strategy-1", pos) - riskControl := NewPositionRiskControl(fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2), orderExecutor) + riskControl := NewPositionRiskControl(orderExecutor, fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2)) cases := []struct { name string @@ -92,7 +92,7 @@ func TestReleasePositionCallbacks(t *testing.T) { } orderExecutor := bbgo.NewGeneralOrderExecutor(nil, "BTCUSDT", "strategy", "strategy-1", pos) - riskControl := NewPositionRiskControl(fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2), orderExecutor) + riskControl := NewPositionRiskControl(orderExecutor, fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2)) riskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { if side == types.SideTypeBuy { pos.Base = pos.Base.Add(quantity) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 94b5ccc2a8..af10dbbaaf 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -146,7 +146,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") - s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.PositionHardLimit, s.MaxPositionQuantity, s.orderExecutor) + s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.orderExecutor, s.PositionHardLimit, s.MaxPositionQuantity) } if !s.CircuitBreakLossThreshold.IsZero() { From 1f98731636c0b6002caea8d33200ce916d2e1026 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 4 Jul 2023 21:33:40 +0800 Subject: [PATCH 1109/1392] riskcontrol: add doc to PositionRiskControl --- pkg/risk/riskcontrol/position.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go index 53b65e012f..ffe2efe86a 100644 --- a/pkg/risk/riskcontrol/position.go +++ b/pkg/risk/riskcontrol/position.go @@ -10,6 +10,9 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +// PositionRiskControl controls the position with the given hard limit +// TODO: add a decorator for the order executor and move the order submission logics into the decorator +// //go:generate callbackgen -type PositionRiskControl type PositionRiskControl struct { orderExecutor *bbgo.GeneralOrderExecutor From f1828beac839705fd189335bbb15adc5d1aa5d87 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 4 Jul 2023 21:42:24 +0800 Subject: [PATCH 1110/1392] all: move trade store and order store into pkg/core --- pkg/bbgo/activeorderbook.go | 4 +++- pkg/bbgo/order_executor_general.go | 7 ++++--- pkg/bbgo/session.go | 13 +++++++------ pkg/bbgo/tradecollector.go | 13 +++++++------ pkg/bbgo/tradecollector_test.go | 3 ++- pkg/bbgo/twap_order_executor.go | 5 +++-- pkg/cmd/backtest.go | 3 ++- pkg/{bbgo/order_store.go => core/orderstore.go} | 2 +- pkg/{bbgo/trade_store.go => core/tradestore.go} | 2 +- .../trade_store_test.go => core/tradestore_test.go} | 2 +- pkg/strategy/bollgrid/strategy.go | 5 +++-- pkg/strategy/fmaker/strategy.go | 5 +++-- pkg/strategy/grid/strategy.go | 5 +++-- pkg/strategy/grid2/strategy.go | 5 +++-- pkg/strategy/grid2/strategy_test.go | 4 ++-- pkg/strategy/wall/strategy.go | 5 +++-- pkg/strategy/xalign/strategy.go | 5 +++-- pkg/strategy/xmaker/strategy.go | 5 +++-- 18 files changed, 54 insertions(+), 39 deletions(-) rename pkg/{bbgo/order_store.go => core/orderstore.go} (99%) rename pkg/{bbgo/trade_store.go => core/tradestore.go} (99%) rename pkg/{bbgo/trade_store_test.go => core/tradestore_test.go} (98%) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 782dfd48a0..c089d69fa5 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/sigchan" "github.com/c9s/bbgo/pkg/types" ) @@ -16,6 +17,7 @@ import ( const CancelOrderWaitTime = 20 * time.Millisecond // ActiveOrderBook manages the local active order books. +// //go:generate callbackgen -type ActiveOrderBook type ActiveOrderBook struct { Symbol string @@ -209,7 +211,7 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange, continue } - openOrderStore := NewOrderStore(symbol) + openOrderStore := core.NewOrderStore(symbol) openOrderStore.Add(openOrders...) for _, o := range orders { // if it's not on the order book (open orders), we should remove it from our local side diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 6a0de068e9..fc27fea3a5 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -10,6 +10,7 @@ import ( log "github.com/sirupsen/logrus" "go.uber.org/multierr" + "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" @@ -33,7 +34,7 @@ type GeneralOrderExecutor struct { strategyInstanceID string position *types.Position activeMakerOrders *ActiveOrderBook - orderStore *OrderStore + orderStore *core.OrderStore tradeCollector *TradeCollector logger log.FieldLogger @@ -49,7 +50,7 @@ func NewGeneralOrderExecutor(session *ExchangeSession, symbol, strategy, strateg position.Strategy = strategy position.StrategyInstanceID = strategyInstanceID - orderStore := NewOrderStore(symbol) + orderStore := core.NewOrderStore(symbol) executor := &GeneralOrderExecutor{ session: session, @@ -121,7 +122,7 @@ func (e *GeneralOrderExecutor) marginAssetMaxBorrowableUpdater(ctx context.Conte } } -func (e *GeneralOrderExecutor) OrderStore() *OrderStore { +func (e *GeneralOrderExecutor) OrderStore() *core.OrderStore { return e.orderStore } diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 0fe3444378..0b9961ec61 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -15,6 +15,7 @@ import ( "github.com/spf13/viper" "github.com/c9s/bbgo/pkg/cache" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/util/templateutil" exchange2 "github.com/c9s/bbgo/pkg/exchange" @@ -110,7 +111,7 @@ type ExchangeSession struct { // indicators is the v2 api indicators indicators map[string]*IndicatorSet - orderStores map[string]*OrderStore + orderStores map[string]*core.OrderStore usedSymbols map[string]struct{} initializedSymbols map[string]struct{} @@ -140,7 +141,7 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession { marketDataStores: make(map[string]*MarketDataStore), standardIndicatorSets: make(map[string]*StandardIndicatorSet), indicators: make(map[string]*IndicatorSet), - orderStores: make(map[string]*OrderStore), + orderStores: make(map[string]*core.OrderStore), usedSymbols: make(map[string]struct{}), initializedSymbols: make(map[string]struct{}), logger: log.WithField("session", name), @@ -398,7 +399,7 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ position.BindStream(session.UserDataStream) session.positions[symbol] = position - orderStore := NewOrderStore(symbol) + orderStore := core.NewOrderStore(symbol) orderStore.AddOrderUpdate = true orderStore.BindStream(session.UserDataStream) @@ -615,12 +616,12 @@ func (session *ExchangeSession) Markets() map[string]types.Market { return session.markets } -func (session *ExchangeSession) OrderStore(symbol string) (store *OrderStore, ok bool) { +func (session *ExchangeSession) OrderStore(symbol string) (store *core.OrderStore, ok bool) { store, ok = session.orderStores[symbol] return store, ok } -func (session *ExchangeSession) OrderStores() map[string]*OrderStore { +func (session *ExchangeSession) OrderStores() map[string]*core.OrderStore { return session.orderStores } @@ -809,7 +810,7 @@ func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) err session.positions = make(map[string]*types.Position) session.standardIndicatorSets = make(map[string]*StandardIndicatorSet) session.indicators = make(map[string]*IndicatorSet) - session.orderStores = make(map[string]*OrderStore) + session.orderStores = make(map[string]*core.OrderStore) session.OrderExecutor = &ExchangeOrderExecutor{ // copy the notification system so that we can route Session: session, diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go index 8af9b38f7c..1f0bcea91a 100644 --- a/pkg/bbgo/tradecollector.go +++ b/pkg/bbgo/tradecollector.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/sigchan" "github.com/c9s/bbgo/pkg/types" @@ -17,10 +18,10 @@ type TradeCollector struct { Symbol string orderSig sigchan.Chan - tradeStore *TradeStore + tradeStore *core.TradeStore tradeC chan types.Trade position *types.Position - orderStore *OrderStore + orderStore *core.OrderStore doneTrades map[types.TradeKey]struct{} mu sync.Mutex @@ -33,13 +34,13 @@ type TradeCollector struct { profitCallbacks []func(trade types.Trade, profit *types.Profit) } -func NewTradeCollector(symbol string, position *types.Position, orderStore *OrderStore) *TradeCollector { +func NewTradeCollector(symbol string, position *types.Position, orderStore *core.OrderStore) *TradeCollector { return &TradeCollector{ Symbol: symbol, orderSig: sigchan.New(1), tradeC: make(chan types.Trade, 100), - tradeStore: NewTradeStore(), + tradeStore: core.NewTradeStore(), doneTrades: make(map[types.TradeKey]struct{}), position: position, orderStore: orderStore, @@ -47,7 +48,7 @@ func NewTradeCollector(symbol string, position *types.Position, orderStore *Orde } // OrderStore returns the order store used by the trade collector -func (c *TradeCollector) OrderStore() *OrderStore { +func (c *TradeCollector) OrderStore() *core.OrderStore { return c.orderStore } @@ -56,7 +57,7 @@ func (c *TradeCollector) Position() *types.Position { return c.position } -func (c *TradeCollector) TradeStore() *TradeStore { +func (c *TradeCollector) TradeStore() *core.TradeStore { return c.tradeStore } diff --git a/pkg/bbgo/tradecollector_test.go b/pkg/bbgo/tradecollector_test.go index 149eb49974..f627db0350 100644 --- a/pkg/bbgo/tradecollector_test.go +++ b/pkg/bbgo/tradecollector_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -12,7 +13,7 @@ import ( func TestTradeCollector_ShouldNotCountDuplicatedTrade(t *testing.T) { symbol := "BTCUSDT" position := types.NewPosition(symbol, "BTC", "USDT") - orderStore := NewOrderStore(symbol) + orderStore := core.NewOrderStore(symbol) collector := NewTradeCollector(symbol, position, orderStore) assert.NotNil(t, collector) diff --git a/pkg/bbgo/twap_order_executor.go b/pkg/bbgo/twap_order_executor.go index 1a942cd88d..05ad9e5e45 100644 --- a/pkg/bbgo/twap_order_executor.go +++ b/pkg/bbgo/twap_order_executor.go @@ -10,6 +10,7 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/time/rate" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -37,7 +38,7 @@ type TwapExecution struct { activePosition fixedpoint.Value activeMakerOrders *ActiveOrderBook - orderStore *OrderStore + orderStore *core.OrderStore position *types.Position executionCtx context.Context @@ -406,7 +407,7 @@ func (e *TwapExecution) Run(parentCtx context.Context) error { QuoteCurrency: e.market.QuoteCurrency, } - e.orderStore = NewOrderStore(e.Symbol) + e.orderStore = core.NewOrderStore(e.Symbol) e.orderStore.BindStream(e.userDataStream) e.activeMakerOrders = NewActiveOrderBook(e.Symbol) e.activeMakerOrders.OnFilled(e.handleFilledOrder) diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 6ebd73a136..587cc1f8fe 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/data/tsv" "github.com/c9s/bbgo/pkg/util" @@ -314,7 +315,7 @@ var BacktestCmd = &cobra.Command{ for usedSymbol := range exSource.Session.Positions() { market, _ := exSource.Session.Market(usedSymbol) position := types.NewPositionFromMarket(market) - orderStore := bbgo.NewOrderStore(usedSymbol) + orderStore := core.NewOrderStore(usedSymbol) orderStore.AddOrderUpdate = true tradeCollector := bbgo.NewTradeCollector(usedSymbol, position, orderStore) diff --git a/pkg/bbgo/order_store.go b/pkg/core/orderstore.go similarity index 99% rename from pkg/bbgo/order_store.go rename to pkg/core/orderstore.go index aa1b3c9fbc..8f3742ce42 100644 --- a/pkg/bbgo/order_store.go +++ b/pkg/core/orderstore.go @@ -1,4 +1,4 @@ -package bbgo +package core import ( "sync" diff --git a/pkg/bbgo/trade_store.go b/pkg/core/tradestore.go similarity index 99% rename from pkg/bbgo/trade_store.go rename to pkg/core/tradestore.go index ffe9011f9d..98ce886e2a 100644 --- a/pkg/bbgo/trade_store.go +++ b/pkg/core/tradestore.go @@ -1,4 +1,4 @@ -package bbgo +package core import ( "sync" diff --git a/pkg/bbgo/trade_store_test.go b/pkg/core/tradestore_test.go similarity index 98% rename from pkg/bbgo/trade_store_test.go rename to pkg/core/tradestore_test.go index f3a5a1ed9e..431572ab4f 100644 --- a/pkg/bbgo/trade_store_test.go +++ b/pkg/core/tradestore_test.go @@ -1,4 +1,4 @@ -package bbgo +package core import ( "testing" diff --git a/pkg/strategy/bollgrid/strategy.go b/pkg/strategy/bollgrid/strategy.go index 2474c7b9cc..9bd8f4ce0b 100644 --- a/pkg/strategy/bollgrid/strategy.go +++ b/pkg/strategy/bollgrid/strategy.go @@ -8,6 +8,7 @@ import ( "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" @@ -71,7 +72,7 @@ type Strategy struct { profitOrders *bbgo.ActiveOrderBook - orders *bbgo.OrderStore + orders *core.OrderStore // boll is the BOLLINGER indicator we used for predicting the price. boll *indicator.BOLL @@ -330,7 +331,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Window: 21, }, 2.0) - s.orders = bbgo.NewOrderStore(s.Symbol) + s.orders = core.NewOrderStore(s.Symbol) s.orders.BindStream(session.UserDataStream) // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. diff --git a/pkg/strategy/fmaker/strategy.go b/pkg/strategy/fmaker/strategy.go index d03b9bf37a..4578857d3c 100644 --- a/pkg/strategy/fmaker/strategy.go +++ b/pkg/strategy/fmaker/strategy.go @@ -10,6 +10,7 @@ import ( "gonum.org/v1/gonum/floats" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" floats2 "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" @@ -47,7 +48,7 @@ type Strategy struct { activeMakerOrders *bbgo.ActiveOrderBook // closePositionOrders *bbgo.LocalActiveOrderBook - orderStore *bbgo.OrderStore + orderStore *core.OrderStore tradeCollector *bbgo.TradeCollector session *bbgo.ExchangeSession @@ -158,7 +159,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // s.closePositionOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) // s.closePositionOrders.BindStream(session.UserDataStream) - s.orderStore = bbgo.NewOrderStore(s.Symbol) + s.orderStore = core.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) if s.Position == nil { diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index 323d1a17aa..66cef86102 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -9,6 +9,7 @@ import ( "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" @@ -89,7 +90,7 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` // orderStore is used to store all the created orders, so that we can filter the trades. - orderStore *bbgo.OrderStore + orderStore *core.OrderStore // activeOrders is the locally maintained active order book of the maker orders. activeOrders *bbgo.ActiveOrderBook @@ -562,7 +563,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.Notify("grid %s position", s.Symbol, s.State.Position) - s.orderStore = bbgo.NewOrderStore(s.Symbol) + s.orderStore = core.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6a38c5f1f7..492c404149 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -18,6 +18,7 @@ import ( "go.uber.org/multierr" "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" @@ -186,7 +187,7 @@ type Strategy struct { orderQueryService types.ExchangeOrderQueryService orderExecutor OrderExecutor - historicalTrades *bbgo.TradeStore + historicalTrades *core.TradeStore logger *logrus.Entry @@ -1858,7 +1859,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } } - s.historicalTrades = bbgo.NewTradeStore() + s.historicalTrades = core.NewTradeStore() s.historicalTrades.EnablePrune = true s.historicalTrades.BindStream(session.UserDataStream) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 483734f011..a96a9ab617 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -11,7 +11,7 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" - "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" gridmocks "github.com/c9s/bbgo/pkg/strategy/grid2/mocks" "github.com/c9s/bbgo/pkg/types" @@ -588,7 +588,7 @@ func newTestStrategy() *Strategy { UpperPrice: number(20_000), LowerPrice: number(10_000), GridNum: 11, - historicalTrades: bbgo.NewTradeStore(), + historicalTrades: core.NewTradeStore(), filledOrderIDMap: types.NewSyncOrderMap(), diff --git a/pkg/strategy/wall/strategy.go b/pkg/strategy/wall/strategy.go index 3babbcda80..1b33181971 100644 --- a/pkg/strategy/wall/strategy.go +++ b/pkg/strategy/wall/strategy.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/util" "github.com/pkg/errors" @@ -65,7 +66,7 @@ type Strategy struct { activeAdjustmentOrders *bbgo.ActiveOrderBook activeWallOrders *bbgo.ActiveOrderBook - orderStore *bbgo.OrderStore + orderStore *core.OrderStore tradeCollector *bbgo.TradeCollector groupID uint32 @@ -273,7 +274,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.activeAdjustmentOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeAdjustmentOrders.BindStream(session.UserDataStream) - s.orderStore = bbgo.NewOrderStore(s.Symbol) + s.orderStore = core.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index ff7a16f1d3..012bf56cbc 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -11,6 +11,7 @@ import ( "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -50,7 +51,7 @@ type Strategy struct { sessions map[string]*bbgo.ExchangeSession orderBooks map[string]*bbgo.ActiveOrderBook - orderStore *bbgo.OrderStore + orderStore *core.OrderStore } func (s *Strategy) ID() string { @@ -242,7 +243,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.sessions = make(map[string]*bbgo.ExchangeSession) s.orderBooks = make(map[string]*bbgo.ActiveOrderBook) - s.orderStore = bbgo.NewOrderStore("") + s.orderStore = core.NewOrderStore("") for _, sessionName := range s.PreferredSessions { session, ok := sessions[sessionName] diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index a912cfeb17..faff3615b9 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -11,6 +11,7 @@ import ( "golang.org/x/time/rate" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" @@ -103,7 +104,7 @@ type Strategy struct { hedgeErrorLimiter *rate.Limiter hedgeErrorRateReservation *rate.Reservation - orderStore *bbgo.OrderStore + orderStore *core.OrderStore tradeCollector *bbgo.TradeCollector askPriceHeartBeat, bidPriceHeartBeat types.PriceHeartBeat @@ -732,7 +733,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.activeMakerOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeMakerOrders.BindStream(s.makerSession.UserDataStream) - s.orderStore = bbgo.NewOrderStore(s.Symbol) + s.orderStore = core.NewOrderStore(s.Symbol) s.orderStore.BindStream(s.sourceSession.UserDataStream) s.orderStore.BindStream(s.makerSession.UserDataStream) From ff727ae49509c962b5f7a815db78409ae565018d Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 4 Jul 2023 22:07:31 +0800 Subject: [PATCH 1111/1392] all: use order executor extended interface to mock the risk tests --- .../mocks/mock_order_executor_extended.go | 104 ++++++++++++++++++ pkg/bbgo/order_execution.go | 8 ++ pkg/risk/riskcontrol/position.go | 4 +- pkg/risk/riskcontrol/position_test.go | 15 ++- 4 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 pkg/bbgo/mocks/mock_order_executor_extended.go diff --git a/pkg/bbgo/mocks/mock_order_executor_extended.go b/pkg/bbgo/mocks/mock_order_executor_extended.go new file mode 100644 index 0000000000..4244fe9691 --- /dev/null +++ b/pkg/bbgo/mocks/mock_order_executor_extended.go @@ -0,0 +1,104 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/c9s/bbgo/pkg/bbgo (interfaces: OrderExecutorExtended) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + bbgo "github.com/c9s/bbgo/pkg/bbgo" + types "github.com/c9s/bbgo/pkg/types" + gomock "github.com/golang/mock/gomock" +) + +// MockOrderExecutorExtended is a mock of OrderExecutorExtended interface. +type MockOrderExecutorExtended struct { + ctrl *gomock.Controller + recorder *MockOrderExecutorExtendedMockRecorder +} + +// MockOrderExecutorExtendedMockRecorder is the mock recorder for MockOrderExecutorExtended. +type MockOrderExecutorExtendedMockRecorder struct { + mock *MockOrderExecutorExtended +} + +// NewMockOrderExecutorExtended creates a new mock instance. +func NewMockOrderExecutorExtended(ctrl *gomock.Controller) *MockOrderExecutorExtended { + mock := &MockOrderExecutorExtended{ctrl: ctrl} + mock.recorder = &MockOrderExecutorExtendedMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOrderExecutorExtended) EXPECT() *MockOrderExecutorExtendedMockRecorder { + return m.recorder +} + +// CancelOrders mocks base method. +func (m *MockOrderExecutorExtended) CancelOrders(arg0 context.Context, arg1 ...types.Order) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CancelOrders", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// CancelOrders indicates an expected call of CancelOrders. +func (mr *MockOrderExecutorExtendedMockRecorder) CancelOrders(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelOrders", reflect.TypeOf((*MockOrderExecutorExtended)(nil).CancelOrders), varargs...) +} + +// Position mocks base method. +func (m *MockOrderExecutorExtended) Position() *types.Position { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Position") + ret0, _ := ret[0].(*types.Position) + return ret0 +} + +// Position indicates an expected call of Position. +func (mr *MockOrderExecutorExtendedMockRecorder) Position() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Position", reflect.TypeOf((*MockOrderExecutorExtended)(nil).Position)) +} + +// SubmitOrders mocks base method. +func (m *MockOrderExecutorExtended) SubmitOrders(arg0 context.Context, arg1 ...types.SubmitOrder) (types.OrderSlice, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SubmitOrders", varargs...) + ret0, _ := ret[0].(types.OrderSlice) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubmitOrders indicates an expected call of SubmitOrders. +func (mr *MockOrderExecutorExtendedMockRecorder) SubmitOrders(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitOrders", reflect.TypeOf((*MockOrderExecutorExtended)(nil).SubmitOrders), varargs...) +} + +// TradeCollector mocks base method. +func (m *MockOrderExecutorExtended) TradeCollector() *bbgo.TradeCollector { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TradeCollector") + ret0, _ := ret[0].(*bbgo.TradeCollector) + return ret0 +} + +// TradeCollector indicates an expected call of TradeCollector. +func (mr *MockOrderExecutorExtendedMockRecorder) TradeCollector() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TradeCollector", reflect.TypeOf((*MockOrderExecutorExtended)(nil).TradeCollector)) +} diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 095160535e..29cbf8ab02 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -28,6 +28,14 @@ type OrderExecutor interface { CancelOrders(ctx context.Context, orders ...types.Order) error } +//go:generate mockgen -destination=mocks/mock_order_executor_extended.go -package=mocks . OrderExecutorExtended +type OrderExecutorExtended interface { + SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) + CancelOrders(ctx context.Context, orders ...types.Order) error + TradeCollector() *TradeCollector + Position() *types.Position +} + type OrderExecutionRouter interface { // SubmitOrdersTo submit order to a specific exchange Session SubmitOrdersTo(ctx context.Context, session string, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go index ffe2efe86a..6a6eebb467 100644 --- a/pkg/risk/riskcontrol/position.go +++ b/pkg/risk/riskcontrol/position.go @@ -15,7 +15,7 @@ import ( // //go:generate callbackgen -type PositionRiskControl type PositionRiskControl struct { - orderExecutor *bbgo.GeneralOrderExecutor + orderExecutor bbgo.OrderExecutorExtended // hardLimit is the maximum base position you can hold hardLimit fixedpoint.Value @@ -27,7 +27,7 @@ type PositionRiskControl struct { releasePositionCallbacks []func(quantity fixedpoint.Value, side types.SideType) } -func NewPositionRiskControl(orderExecutor *bbgo.GeneralOrderExecutor, hardLimit, quantity fixedpoint.Value) *PositionRiskControl { +func NewPositionRiskControl(orderExecutor bbgo.OrderExecutorExtended, hardLimit, quantity fixedpoint.Value) *PositionRiskControl { control := &PositionRiskControl{ orderExecutor: orderExecutor, hardLimit: hardLimit, diff --git a/pkg/risk/riskcontrol/position_test.go b/pkg/risk/riskcontrol/position_test.go index 106db68251..21bb7f2ea5 100644 --- a/pkg/risk/riskcontrol/position_test.go +++ b/pkg/risk/riskcontrol/position_test.go @@ -3,9 +3,11 @@ package riskcontrol import ( "testing" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/bbgo/mocks" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -58,7 +60,7 @@ func TestReleasePositionCallbacks(t *testing.T) { resultPosition fixedpoint.Value }{ { - name: "PostivePositionWithinLimit", + name: "PositivePositionWithinLimit", position: fixedpoint.NewFromInt(8), resultPosition: fixedpoint.NewFromInt(8), }, @@ -68,7 +70,7 @@ func TestReleasePositionCallbacks(t *testing.T) { resultPosition: fixedpoint.NewFromInt(-8), }, { - name: "PostivePositionOverLimit", + name: "PositivePositionOverLimit", position: fixedpoint.NewFromInt(11), resultPosition: fixedpoint.NewFromInt(10), }, @@ -91,7 +93,14 @@ func TestReleasePositionCallbacks(t *testing.T) { }, } - orderExecutor := bbgo.NewGeneralOrderExecutor(nil, "BTCUSDT", "strategy", "strategy-1", pos) + tradeCollector := &bbgo.TradeCollector{} + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + orderExecutor := mocks.NewMockOrderExecutorExtended(mockCtrl) + orderExecutor.EXPECT().TradeCollector().Return(tradeCollector).AnyTimes() + orderExecutor.EXPECT().Position().Return(pos).AnyTimes() + orderExecutor.EXPECT().SubmitOrders(gomock.Any(), gomock.Any()).AnyTimes() + riskControl := NewPositionRiskControl(orderExecutor, fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2)) riskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { if side == types.SideTypeBuy { From 1ad10a93605c3e0cccaa62c940be763e16291af1 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 15:26:36 +0800 Subject: [PATCH 1112/1392] all: move trade collector to pkg/core --- .../mocks/mock_order_executor_extended.go | 15 +- pkg/bbgo/order_execution.go | 3 +- pkg/bbgo/order_executor_general.go | 6 +- pkg/bbgo/tradecollector.go | 237 ------------------ pkg/bbgo/tradecollector_test.go | 65 ----- pkg/cmd/backtest.go | 4 +- pkg/core/tradecollector.go | 237 ++++++++++++++++++ .../tradecollector_callbacks.go | 2 +- pkg/core/tradecollector_test.go | 65 +++++ pkg/risk/riskcontrol/position_test.go | 3 +- pkg/strategy/fmaker/strategy.go | 4 +- pkg/strategy/grid/strategy.go | 4 +- pkg/strategy/wall/strategy.go | 4 +- pkg/strategy/xmaker/strategy.go | 4 +- 14 files changed, 328 insertions(+), 325 deletions(-) create mode 100644 pkg/core/tradecollector.go rename pkg/{bbgo => core}/tradecollector_callbacks.go (99%) create mode 100644 pkg/core/tradecollector_test.go diff --git a/pkg/bbgo/mocks/mock_order_executor_extended.go b/pkg/bbgo/mocks/mock_order_executor_extended.go index 4244fe9691..32e5cb63b4 100644 --- a/pkg/bbgo/mocks/mock_order_executor_extended.go +++ b/pkg/bbgo/mocks/mock_order_executor_extended.go @@ -5,12 +5,13 @@ package mocks import ( - context "context" - reflect "reflect" + "context" + "reflect" - bbgo "github.com/c9s/bbgo/pkg/bbgo" - types "github.com/c9s/bbgo/pkg/types" - gomock "github.com/golang/mock/gomock" + "github.com/golang/mock/gomock" + + "github.com/c9s/bbgo/pkg/core" + "github.com/c9s/bbgo/pkg/types" ) // MockOrderExecutorExtended is a mock of OrderExecutorExtended interface. @@ -90,10 +91,10 @@ func (mr *MockOrderExecutorExtendedMockRecorder) SubmitOrders(arg0 interface{}, } // TradeCollector mocks base method. -func (m *MockOrderExecutorExtended) TradeCollector() *bbgo.TradeCollector { +func (m *MockOrderExecutorExtended) TradeCollector() *core.TradeCollector { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TradeCollector") - ret0, _ := ret[0].(*bbgo.TradeCollector) + ret0, _ := ret[0].(*core.TradeCollector) return ret0 } diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 29cbf8ab02..fed6ae51ec 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -10,6 +10,7 @@ import ( log "github.com/sirupsen/logrus" "go.uber.org/multierr" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" @@ -32,7 +33,7 @@ type OrderExecutor interface { type OrderExecutorExtended interface { SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) CancelOrders(ctx context.Context, orders ...types.Order) error - TradeCollector() *TradeCollector + TradeCollector() *core.TradeCollector Position() *types.Position } diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index fc27fea3a5..cee2aa5738 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -35,7 +35,7 @@ type GeneralOrderExecutor struct { position *types.Position activeMakerOrders *ActiveOrderBook orderStore *core.OrderStore - tradeCollector *TradeCollector + tradeCollector *core.TradeCollector logger log.FieldLogger @@ -60,7 +60,7 @@ func NewGeneralOrderExecutor(session *ExchangeSession, symbol, strategy, strateg position: position, activeMakerOrders: NewActiveOrderBook(symbol), orderStore: orderStore, - tradeCollector: NewTradeCollector(symbol, position, orderStore), + tradeCollector: core.NewTradeCollector(symbol, position, orderStore), } if session != nil && session.Margin { @@ -517,7 +517,7 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix return nil } -func (e *GeneralOrderExecutor) TradeCollector() *TradeCollector { +func (e *GeneralOrderExecutor) TradeCollector() *core.TradeCollector { return e.tradeCollector } diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go index 1f0bcea91a..f30d11b655 100644 --- a/pkg/bbgo/tradecollector.go +++ b/pkg/bbgo/tradecollector.go @@ -1,238 +1 @@ package bbgo - -import ( - "context" - "sync" - "time" - - log "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/core" - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/sigchan" - "github.com/c9s/bbgo/pkg/types" -) - -//go:generate callbackgen -type TradeCollector -type TradeCollector struct { - Symbol string - orderSig sigchan.Chan - - tradeStore *core.TradeStore - tradeC chan types.Trade - position *types.Position - orderStore *core.OrderStore - doneTrades map[types.TradeKey]struct{} - - mu sync.Mutex - - recoverCallbacks []func(trade types.Trade) - - tradeCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) - - positionUpdateCallbacks []func(position *types.Position) - profitCallbacks []func(trade types.Trade, profit *types.Profit) -} - -func NewTradeCollector(symbol string, position *types.Position, orderStore *core.OrderStore) *TradeCollector { - return &TradeCollector{ - Symbol: symbol, - orderSig: sigchan.New(1), - - tradeC: make(chan types.Trade, 100), - tradeStore: core.NewTradeStore(), - doneTrades: make(map[types.TradeKey]struct{}), - position: position, - orderStore: orderStore, - } -} - -// OrderStore returns the order store used by the trade collector -func (c *TradeCollector) OrderStore() *core.OrderStore { - return c.orderStore -} - -// Position returns the position used by the trade collector -func (c *TradeCollector) Position() *types.Position { - return c.position -} - -func (c *TradeCollector) TradeStore() *core.TradeStore { - return c.tradeStore -} - -func (c *TradeCollector) SetPosition(position *types.Position) { - c.position = position -} - -// QueueTrade sends the trade object to the trade channel, -// so that the goroutine can receive the trade and process in the background. -func (c *TradeCollector) QueueTrade(trade types.Trade) { - c.tradeC <- trade -} - -// BindStreamForBackground bind the stream callback for background processing -func (c *TradeCollector) BindStreamForBackground(stream types.Stream) { - stream.OnTradeUpdate(c.QueueTrade) -} - -func (c *TradeCollector) BindStream(stream types.Stream) { - stream.OnTradeUpdate(func(trade types.Trade) { - c.ProcessTrade(trade) - }) -} - -// Emit triggers the trade processing (position update) -// If you sent order, and the order store is updated, you can call this method -// so that trades will be processed in the next round of the goroutine loop -func (c *TradeCollector) Emit() { - c.orderSig.Emit() -} - -func (c *TradeCollector) Recover(ctx context.Context, ex types.ExchangeTradeHistoryService, symbol string, from time.Time) error { - trades, err := ex.QueryTrades(ctx, symbol, &types.TradeQueryOptions{ - StartTime: &from, - }) - - if err != nil { - return err - } - - for _, td := range trades { - log.Debugf("processing trade: %s", td.String()) - if c.ProcessTrade(td) { - log.Infof("recovered trade: %s", td.String()) - c.EmitRecover(td) - } - } - return nil -} - -func (c *TradeCollector) setDone(key types.TradeKey) { - c.mu.Lock() - c.doneTrades[key] = struct{}{} - c.mu.Unlock() -} - -// Process filters the received trades and see if there are orders matching the trades -// if we have the order in the order store, then the trade will be considered for the position. -// profit will also be calculated. -func (c *TradeCollector) Process() bool { - positionChanged := false - - c.tradeStore.Filter(func(trade types.Trade) bool { - key := trade.Key() - - c.mu.Lock() - defer c.mu.Unlock() - - // if it's already done, remove the trade from the trade store - if _, done := c.doneTrades[key]; done { - return true - } - - if c.orderStore.Exists(trade.OrderID) { - if c.position != nil { - profit, netProfit, madeProfit := c.position.AddTrade(trade) - if madeProfit { - p := c.position.NewProfit(trade, profit, netProfit) - c.EmitTrade(trade, profit, netProfit) - c.EmitProfit(trade, &p) - } else { - c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) - c.EmitProfit(trade, nil) - } - positionChanged = true - } else { - c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) - } - - c.doneTrades[key] = struct{}{} - return true - } - return false - }) - - if positionChanged && c.position != nil { - c.EmitPositionUpdate(c.position) - } - - return positionChanged -} - -// processTrade takes a trade and see if there is a matched order -// if the order is found, then we add the trade to the position -// return true when the given trade is added -// return false when the given trade is not added -func (c *TradeCollector) processTrade(trade types.Trade) bool { - c.mu.Lock() - defer c.mu.Unlock() - - key := trade.Key() - - // if it's already done, remove the trade from the trade store - if _, done := c.doneTrades[key]; done { - return false - } - - if c.orderStore.Exists(trade.OrderID) { - if c.position != nil { - profit, netProfit, madeProfit := c.position.AddTrade(trade) - if madeProfit { - p := c.position.NewProfit(trade, profit, netProfit) - c.EmitTrade(trade, profit, netProfit) - c.EmitProfit(trade, &p) - } else { - c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) - c.EmitProfit(trade, nil) - } - c.EmitPositionUpdate(c.position) - } else { - c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) - } - - c.doneTrades[key] = struct{}{} - return true - } - return false -} - -// return true when the given trade is added -// return false when the given trade is not added -func (c *TradeCollector) ProcessTrade(trade types.Trade) bool { - key := trade.Key() - // if it's already done, remove the trade from the trade store - c.mu.Lock() - if _, done := c.doneTrades[key]; done { - return false - } - c.mu.Unlock() - - if c.processTrade(trade) { - return true - } - - c.tradeStore.Add(trade) - return false -} - -// Run is a goroutine executed in the background -// Do not use this function if you need back-testing -func (c *TradeCollector) Run(ctx context.Context) { - var ticker = time.NewTicker(3 * time.Second) - for { - select { - case <-ctx.Done(): - return - - case <-ticker.C: - c.Process() - - case <-c.orderSig: - c.Process() - - case trade := <-c.tradeC: - c.ProcessTrade(trade) - } - } -} diff --git a/pkg/bbgo/tradecollector_test.go b/pkg/bbgo/tradecollector_test.go index f627db0350..f30d11b655 100644 --- a/pkg/bbgo/tradecollector_test.go +++ b/pkg/bbgo/tradecollector_test.go @@ -1,66 +1 @@ package bbgo - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/core" - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -func TestTradeCollector_ShouldNotCountDuplicatedTrade(t *testing.T) { - symbol := "BTCUSDT" - position := types.NewPosition(symbol, "BTC", "USDT") - orderStore := core.NewOrderStore(symbol) - collector := NewTradeCollector(symbol, position, orderStore) - assert.NotNil(t, collector) - - matched := collector.ProcessTrade(types.Trade{ - ID: 1, - OrderID: 399, - Exchange: types.ExchangeBinance, - Price: fixedpoint.NewFromInt(40000), - Quantity: fixedpoint.One, - QuoteQuantity: fixedpoint.NewFromInt(40000), - Symbol: "BTCUSDT", - Side: types.SideTypeBuy, - IsBuyer: true, - }) - assert.False(t, matched, "should be added to the trade store") - assert.Equal(t, 1, len(collector.tradeStore.Trades()), "should have one trade in the trade store") - - orderStore.Add(types.Order{ - SubmitOrder: types.SubmitOrder{ - Symbol: "BTCUSDT", - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Quantity: fixedpoint.One, - Price: fixedpoint.NewFromInt(40000), - }, - Exchange: types.ExchangeBinance, - OrderID: 399, - Status: types.OrderStatusFilled, - ExecutedQuantity: fixedpoint.One, - IsWorking: false, - }) - - matched = collector.Process() - assert.True(t, matched) - assert.Equal(t, 0, len(collector.tradeStore.Trades()), "the found trade should be removed from the trade store") - - matched = collector.ProcessTrade(types.Trade{ - ID: 1, - OrderID: 399, - Exchange: types.ExchangeBinance, - Price: fixedpoint.NewFromInt(40000), - Quantity: fixedpoint.One, - QuoteQuantity: fixedpoint.NewFromInt(40000), - Symbol: "BTCUSDT", - Side: types.SideTypeBuy, - IsBuyer: true, - }) - assert.False(t, matched, "the same trade should not match") - assert.Equal(t, 0, len(collector.tradeStore.Trades()), "the same trade should not be added to the trade store") -} diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 587cc1f8fe..025bec0209 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -308,7 +308,7 @@ var BacktestCmd = &cobra.Command{ var reportDir = outputDirectory var sessionTradeStats = make(map[string]map[string]*types.TradeStats) - var tradeCollectorList []*bbgo.TradeCollector + var tradeCollectorList []*core.TradeCollector for _, exSource := range exchangeSources { sessionName := exSource.Session.Name tradeStatsMap := make(map[string]*types.TradeStats) @@ -317,7 +317,7 @@ var BacktestCmd = &cobra.Command{ position := types.NewPositionFromMarket(market) orderStore := core.NewOrderStore(usedSymbol) orderStore.AddOrderUpdate = true - tradeCollector := bbgo.NewTradeCollector(usedSymbol, position, orderStore) + tradeCollector := core.NewTradeCollector(usedSymbol, position, orderStore) tradeStats := types.NewTradeStats(usedSymbol) tradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime)) diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go new file mode 100644 index 0000000000..e981fff56c --- /dev/null +++ b/pkg/core/tradecollector.go @@ -0,0 +1,237 @@ +package core + +import ( + "context" + "sync" + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/sigchan" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type TradeCollector +type TradeCollector struct { + Symbol string + orderSig sigchan.Chan + + tradeStore *TradeStore + tradeC chan types.Trade + position *types.Position + orderStore *OrderStore + doneTrades map[types.TradeKey]struct{} + + mu sync.Mutex + + recoverCallbacks []func(trade types.Trade) + + tradeCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) + + positionUpdateCallbacks []func(position *types.Position) + profitCallbacks []func(trade types.Trade, profit *types.Profit) +} + +func NewTradeCollector(symbol string, position *types.Position, orderStore *OrderStore) *TradeCollector { + return &TradeCollector{ + Symbol: symbol, + orderSig: sigchan.New(1), + + tradeC: make(chan types.Trade, 100), + tradeStore: NewTradeStore(), + doneTrades: make(map[types.TradeKey]struct{}), + position: position, + orderStore: orderStore, + } +} + +// OrderStore returns the order store used by the trade collector +func (c *TradeCollector) OrderStore() *OrderStore { + return c.orderStore +} + +// Position returns the position used by the trade collector +func (c *TradeCollector) Position() *types.Position { + return c.position +} + +func (c *TradeCollector) TradeStore() *TradeStore { + return c.tradeStore +} + +func (c *TradeCollector) SetPosition(position *types.Position) { + c.position = position +} + +// QueueTrade sends the trade object to the trade channel, +// so that the goroutine can receive the trade and process in the background. +func (c *TradeCollector) QueueTrade(trade types.Trade) { + c.tradeC <- trade +} + +// BindStreamForBackground bind the stream callback for background processing +func (c *TradeCollector) BindStreamForBackground(stream types.Stream) { + stream.OnTradeUpdate(c.QueueTrade) +} + +func (c *TradeCollector) BindStream(stream types.Stream) { + stream.OnTradeUpdate(func(trade types.Trade) { + c.ProcessTrade(trade) + }) +} + +// Emit triggers the trade processing (position update) +// If you sent order, and the order store is updated, you can call this method +// so that trades will be processed in the next round of the goroutine loop +func (c *TradeCollector) Emit() { + c.orderSig.Emit() +} + +func (c *TradeCollector) Recover(ctx context.Context, ex types.ExchangeTradeHistoryService, symbol string, from time.Time) error { + trades, err := ex.QueryTrades(ctx, symbol, &types.TradeQueryOptions{ + StartTime: &from, + }) + + if err != nil { + return err + } + + for _, td := range trades { + logrus.Debugf("processing trade: %s", td.String()) + if c.ProcessTrade(td) { + logrus.Infof("recovered trade: %s", td.String()) + c.EmitRecover(td) + } + } + return nil +} + +func (c *TradeCollector) setDone(key types.TradeKey) { + c.mu.Lock() + c.doneTrades[key] = struct{}{} + c.mu.Unlock() +} + +// Process filters the received trades and see if there are orders matching the trades +// if we have the order in the order store, then the trade will be considered for the position. +// profit will also be calculated. +func (c *TradeCollector) Process() bool { + positionChanged := false + + c.tradeStore.Filter(func(trade types.Trade) bool { + key := trade.Key() + + c.mu.Lock() + defer c.mu.Unlock() + + // if it's already done, remove the trade from the trade store + if _, done := c.doneTrades[key]; done { + return true + } + + if c.orderStore.Exists(trade.OrderID) { + if c.position != nil { + profit, netProfit, madeProfit := c.position.AddTrade(trade) + if madeProfit { + p := c.position.NewProfit(trade, profit, netProfit) + c.EmitTrade(trade, profit, netProfit) + c.EmitProfit(trade, &p) + } else { + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + c.EmitProfit(trade, nil) + } + positionChanged = true + } else { + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + } + + c.doneTrades[key] = struct{}{} + return true + } + return false + }) + + if positionChanged && c.position != nil { + c.EmitPositionUpdate(c.position) + } + + return positionChanged +} + +// processTrade takes a trade and see if there is a matched order +// if the order is found, then we add the trade to the position +// return true when the given trade is added +// return false when the given trade is not added +func (c *TradeCollector) processTrade(trade types.Trade) bool { + c.mu.Lock() + defer c.mu.Unlock() + + key := trade.Key() + + // if it's already done, remove the trade from the trade store + if _, done := c.doneTrades[key]; done { + return false + } + + if c.orderStore.Exists(trade.OrderID) { + if c.position != nil { + profit, netProfit, madeProfit := c.position.AddTrade(trade) + if madeProfit { + p := c.position.NewProfit(trade, profit, netProfit) + c.EmitTrade(trade, profit, netProfit) + c.EmitProfit(trade, &p) + } else { + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + c.EmitProfit(trade, nil) + } + c.EmitPositionUpdate(c.position) + } else { + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + } + + c.doneTrades[key] = struct{}{} + return true + } + return false +} + +// return true when the given trade is added +// return false when the given trade is not added +func (c *TradeCollector) ProcessTrade(trade types.Trade) bool { + key := trade.Key() + // if it's already done, remove the trade from the trade store + c.mu.Lock() + if _, done := c.doneTrades[key]; done { + return false + } + c.mu.Unlock() + + if c.processTrade(trade) { + return true + } + + c.tradeStore.Add(trade) + return false +} + +// Run is a goroutine executed in the background +// Do not use this function if you need back-testing +func (c *TradeCollector) Run(ctx context.Context) { + var ticker = time.NewTicker(3 * time.Second) + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + c.Process() + + case <-c.orderSig: + c.Process() + + case trade := <-c.tradeC: + c.ProcessTrade(trade) + } + } +} diff --git a/pkg/bbgo/tradecollector_callbacks.go b/pkg/core/tradecollector_callbacks.go similarity index 99% rename from pkg/bbgo/tradecollector_callbacks.go rename to pkg/core/tradecollector_callbacks.go index 44756224f9..e44c0f5096 100644 --- a/pkg/bbgo/tradecollector_callbacks.go +++ b/pkg/core/tradecollector_callbacks.go @@ -1,6 +1,6 @@ // Code generated by "callbackgen -type TradeCollector"; DO NOT EDIT. -package bbgo +package core import ( "github.com/c9s/bbgo/pkg/fixedpoint" diff --git a/pkg/core/tradecollector_test.go b/pkg/core/tradecollector_test.go new file mode 100644 index 0000000000..ec29a2d639 --- /dev/null +++ b/pkg/core/tradecollector_test.go @@ -0,0 +1,65 @@ +package core + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func TestTradeCollector_ShouldNotCountDuplicatedTrade(t *testing.T) { + symbol := "BTCUSDT" + position := types.NewPosition(symbol, "BTC", "USDT") + orderStore := NewOrderStore(symbol) + collector := NewTradeCollector(symbol, position, orderStore) + assert.NotNil(t, collector) + + matched := collector.ProcessTrade(types.Trade{ + ID: 1, + OrderID: 399, + Exchange: types.ExchangeBinance, + Price: fixedpoint.NewFromInt(40000), + Quantity: fixedpoint.One, + QuoteQuantity: fixedpoint.NewFromInt(40000), + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + }) + assert.False(t, matched, "should be added to the trade store") + assert.Equal(t, 1, len(collector.tradeStore.Trades()), "should have one trade in the trade store") + + orderStore.Add(types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.One, + Price: fixedpoint.NewFromInt(40000), + }, + Exchange: types.ExchangeBinance, + OrderID: 399, + Status: types.OrderStatusFilled, + ExecutedQuantity: fixedpoint.One, + IsWorking: false, + }) + + matched = collector.Process() + assert.True(t, matched) + assert.Equal(t, 0, len(collector.tradeStore.Trades()), "the found trade should be removed from the trade store") + + matched = collector.ProcessTrade(types.Trade{ + ID: 1, + OrderID: 399, + Exchange: types.ExchangeBinance, + Price: fixedpoint.NewFromInt(40000), + Quantity: fixedpoint.One, + QuoteQuantity: fixedpoint.NewFromInt(40000), + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + }) + assert.False(t, matched, "the same trade should not match") + assert.Equal(t, 0, len(collector.tradeStore.Trades()), "the same trade should not be added to the trade store") +} diff --git a/pkg/risk/riskcontrol/position_test.go b/pkg/risk/riskcontrol/position_test.go index 21bb7f2ea5..d1fc106525 100644 --- a/pkg/risk/riskcontrol/position_test.go +++ b/pkg/risk/riskcontrol/position_test.go @@ -8,6 +8,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/bbgo/mocks" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -93,7 +94,7 @@ func TestReleasePositionCallbacks(t *testing.T) { }, } - tradeCollector := &bbgo.TradeCollector{} + tradeCollector := &core.TradeCollector{} mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() orderExecutor := mocks.NewMockOrderExecutorExtended(mockCtrl) diff --git a/pkg/strategy/fmaker/strategy.go b/pkg/strategy/fmaker/strategy.go index 4578857d3c..b62d790cdc 100644 --- a/pkg/strategy/fmaker/strategy.go +++ b/pkg/strategy/fmaker/strategy.go @@ -49,7 +49,7 @@ type Strategy struct { // closePositionOrders *bbgo.LocalActiveOrderBook orderStore *core.OrderStore - tradeCollector *bbgo.TradeCollector + tradeCollector *core.TradeCollector session *bbgo.ExchangeSession @@ -174,7 +174,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position.Strategy = ID s.Position.StrategyInstanceID = instanceID - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) + s.tradeCollector = core.NewTradeCollector(s.Symbol, s.Position, s.orderStore) s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { // StrategyController if s.Status != types.StrategyStatusRunning { diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index 66cef86102..a64642ce5c 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -95,7 +95,7 @@ type Strategy struct { // activeOrders is the locally maintained active order book of the maker orders. activeOrders *bbgo.ActiveOrderBook - tradeCollector *bbgo.TradeCollector + tradeCollector *core.TradeCollector // groupID is the group ID used for the strategy instance for canceling orders groupID uint32 @@ -571,7 +571,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.activeOrders.OnFilled(s.handleFilledOrder) s.activeOrders.BindStream(session.UserDataStream) - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.State.Position, s.orderStore) + s.tradeCollector = core.NewTradeCollector(s.Symbol, s.State.Position, s.orderStore) s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { bbgo.Notify(trade) diff --git a/pkg/strategy/wall/strategy.go b/pkg/strategy/wall/strategy.go index 1b33181971..5cbb4294ff 100644 --- a/pkg/strategy/wall/strategy.go +++ b/pkg/strategy/wall/strategy.go @@ -67,7 +67,7 @@ type Strategy struct { activeAdjustmentOrders *bbgo.ActiveOrderBook activeWallOrders *bbgo.ActiveOrderBook orderStore *core.OrderStore - tradeCollector *bbgo.TradeCollector + tradeCollector *core.TradeCollector groupID uint32 @@ -277,7 +277,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderStore = core.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) + s.tradeCollector = core.NewTradeCollector(s.Symbol, s.Position, s.orderStore) s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { bbgo.Notify(trade) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index faff3615b9..2a2db7230c 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -105,7 +105,7 @@ type Strategy struct { hedgeErrorRateReservation *rate.Reservation orderStore *core.OrderStore - tradeCollector *bbgo.TradeCollector + tradeCollector *core.TradeCollector askPriceHeartBeat, bidPriceHeartBeat types.PriceHeartBeat @@ -737,7 +737,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.orderStore.BindStream(s.sourceSession.UserDataStream) s.orderStore.BindStream(s.makerSession.UserDataStream) - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) + s.tradeCollector = core.NewTradeCollector(s.Symbol, s.Position, s.orderStore) if s.NotifyTrade { s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { From fbc49c28ef0f37d59d13dbd0835f4ac40d90a534 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 15:30:08 +0800 Subject: [PATCH 1113/1392] types: add PriceVolume.Equals method --- pkg/types/price_volume_slice.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/types/price_volume_slice.go b/pkg/types/price_volume_slice.go index a7863702fe..fdaaaf7716 100644 --- a/pkg/types/price_volume_slice.go +++ b/pkg/types/price_volume_slice.go @@ -12,6 +12,10 @@ type PriceVolume struct { Price, Volume fixedpoint.Value } +func (p PriceVolume) Equals(b PriceVolume) bool { + return p.Price.Eq(b.Price) && p.Volume.Eq(b.Volume) +} + func (p PriceVolume) String() string { return fmt.Sprintf("PriceVolume{ price: %s, volume: %s }", p.Price.String(), p.Volume.String()) } @@ -139,8 +143,7 @@ func (slice *PriceVolumeSlice) UnmarshalJSON(b []byte) error { // ParsePriceVolumeSliceJSON tries to parse a 2 dimensional string array into a PriceVolumeSlice // -// [["9000", "10"], ["9900", "10"], ... ] -// +// [["9000", "10"], ["9900", "10"], ... ] func ParsePriceVolumeSliceJSON(b []byte) (slice PriceVolumeSlice, err error) { var as [][]fixedpoint.Value From 01096829aef197caa2af97eff570e58c9c7c66ed Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 15:30:15 +0800 Subject: [PATCH 1114/1392] bbgo: drop empty files --- pkg/bbgo/tradecollector.go | 1 - pkg/bbgo/tradecollector_test.go | 1 - 2 files changed, 2 deletions(-) delete mode 100644 pkg/bbgo/tradecollector.go delete mode 100644 pkg/bbgo/tradecollector_test.go diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go deleted file mode 100644 index f30d11b655..0000000000 --- a/pkg/bbgo/tradecollector.go +++ /dev/null @@ -1 +0,0 @@ -package bbgo diff --git a/pkg/bbgo/tradecollector_test.go b/pkg/bbgo/tradecollector_test.go deleted file mode 100644 index f30d11b655..0000000000 --- a/pkg/bbgo/tradecollector_test.go +++ /dev/null @@ -1 +0,0 @@ -package bbgo From e19aa8fa107d80ba7bec4b20fd7e26d32ecb6fb3 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 15:51:16 +0800 Subject: [PATCH 1115/1392] add tri strategy --- pkg/strategy/tri/market.go | 104 ++ pkg/strategy/tri/path.go | 83 ++ pkg/strategy/tri/position.go | 139 ++ pkg/strategy/tri/profit.go | 62 + pkg/strategy/tri/strategy.go | 880 ++++++++++++ pkg/strategy/tri/strategy_test.go | 220 +++ pkg/strategy/tri/symbols.go | 2212 +++++++++++++++++++++++++++++ pkg/strategy/tri/symbols.sh | 33 + pkg/strategy/tri/utils.go | 28 + 9 files changed, 3761 insertions(+) create mode 100644 pkg/strategy/tri/market.go create mode 100644 pkg/strategy/tri/path.go create mode 100644 pkg/strategy/tri/position.go create mode 100644 pkg/strategy/tri/profit.go create mode 100644 pkg/strategy/tri/strategy.go create mode 100644 pkg/strategy/tri/strategy_test.go create mode 100644 pkg/strategy/tri/symbols.go create mode 100644 pkg/strategy/tri/symbols.sh create mode 100644 pkg/strategy/tri/utils.go diff --git a/pkg/strategy/tri/market.go b/pkg/strategy/tri/market.go new file mode 100644 index 0000000000..eae5131852 --- /dev/null +++ b/pkg/strategy/tri/market.go @@ -0,0 +1,104 @@ +package tri + +import ( + "fmt" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/sigchan" + "github.com/c9s/bbgo/pkg/types" +) + +type ArbMarket struct { + Symbol string + BaseCurrency, QuoteCurrency string + market types.Market + + stream types.Stream + book *types.StreamOrderBook + bestBid, bestAsk types.PriceVolume + buyRate, sellRate float64 + sigC sigchan.Chan +} + +func (m *ArbMarket) String() string { + return m.Symbol +} + +func (m *ArbMarket) getInitialBalance(balances types.BalanceMap, dir int) (fixedpoint.Value, string) { + if dir == 1 { // sell 1 BTC -> 19000 USDT + b, ok := balances[m.BaseCurrency] + if !ok { + return fixedpoint.Zero, m.BaseCurrency + } + + return m.market.TruncateQuantity(b.Available), m.BaseCurrency + } else if dir == -1 { + b, ok := balances[m.QuoteCurrency] + if !ok { + return fixedpoint.Zero, m.QuoteCurrency + } + + return m.market.TruncateQuantity(b.Available), m.QuoteCurrency + } + + return fixedpoint.Zero, "" +} + +func (m *ArbMarket) calculateRatio(dir int) float64 { + if dir == 1 { // direct 1 = sell + if m.bestBid.Price.IsZero() || m.bestBid.Volume.Compare(m.market.MinQuantity) <= 0 { + return 0.0 + } + + return m.sellRate + } else if dir == -1 { + if m.bestAsk.Price.IsZero() || m.bestAsk.Volume.Compare(m.market.MinQuantity) <= 0 { + return 0.0 + } + + return m.buyRate + } + + return 0.0 +} + +func (m *ArbMarket) updateRate() { + m.buyRate = 1.0 / m.bestAsk.Price.Float64() + m.sellRate = m.bestBid.Price.Float64() + + if m.bestBid.Volume.Compare(m.market.MinQuantity) <= 0 && m.bestAsk.Volume.Compare(m.market.MinQuantity) <= 0 { + return + } + + m.sigC.Emit() +} + +func (m *ArbMarket) newOrder(dir int, transitingQuantity float64) (types.SubmitOrder, float64) { + if dir == 1 { // sell ETH -> BTC, sell USDT -> TWD + q, r := fitQuantityByBase(m.market.TruncateQuantity(m.bestBid.Volume).Float64(), transitingQuantity) + fq := fixedpoint.NewFromFloat(q) + return types.SubmitOrder{ + Symbol: m.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: fq, + Price: m.bestBid.Price, + Market: m.market, + }, r + } else if dir == -1 { // use 1 BTC to buy X ETH + q, r := fitQuantityByQuote(m.bestAsk.Price.Float64(), m.market.TruncateQuantity(m.bestAsk.Volume).Float64(), transitingQuantity) + fq := fixedpoint.NewFromFloat(q) + return types.SubmitOrder{ + Symbol: m.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: fq, + Price: m.bestAsk.Price, + Market: m.market, + }, r + } else { + panic(fmt.Errorf("unexpected direction: %v, valid values are (1, -1)", dir)) + } + + return types.SubmitOrder{}, 0.0 +} diff --git a/pkg/strategy/tri/path.go b/pkg/strategy/tri/path.go new file mode 100644 index 0000000000..5b15995cdb --- /dev/null +++ b/pkg/strategy/tri/path.go @@ -0,0 +1,83 @@ +package tri + +import ( + "fmt" + + "github.com/c9s/bbgo/pkg/types" +) + +type Path struct { + marketA, marketB, marketC *ArbMarket + dirA, dirB, dirC int +} + +func (p *Path) solveDirection() error { + // check if we should reverse the rate + // ETHUSDT -> ETHBTC + if p.marketA.QuoteCurrency == p.marketB.BaseCurrency || p.marketA.QuoteCurrency == p.marketB.QuoteCurrency { + p.dirA = 1 + } else if p.marketA.BaseCurrency == p.marketB.BaseCurrency || p.marketA.BaseCurrency == p.marketB.QuoteCurrency { + p.dirA = -1 + } else { + return fmt.Errorf("marketA and marketB is not related") + } + + if p.marketB.QuoteCurrency == p.marketC.BaseCurrency || p.marketB.QuoteCurrency == p.marketC.QuoteCurrency { + p.dirB = 1 + } else if p.marketB.BaseCurrency == p.marketC.BaseCurrency || p.marketB.BaseCurrency == p.marketC.QuoteCurrency { + p.dirB = -1 + } else { + return fmt.Errorf("marketB and marketC is not related") + } + + if p.marketC.QuoteCurrency == p.marketA.BaseCurrency || p.marketC.QuoteCurrency == p.marketA.QuoteCurrency { + p.dirC = 1 + } else if p.marketC.BaseCurrency == p.marketA.BaseCurrency || p.marketC.BaseCurrency == p.marketA.QuoteCurrency { + p.dirC = -1 + } else { + return fmt.Errorf("marketC and marketA is not related") + } + + return nil +} + +func (p *Path) Ready() bool { + return !(p.marketA.bestAsk.Price.IsZero() || p.marketA.bestBid.Price.IsZero() || + p.marketB.bestAsk.Price.IsZero() || p.marketB.bestBid.Price.IsZero() || + p.marketC.bestAsk.Price.IsZero() || p.marketC.bestBid.Price.IsZero()) +} + +func (p *Path) String() string { + return p.marketA.String() + " " + p.marketB.String() + " " + p.marketC.String() +} + +func (p *Path) newOrders(balances types.BalanceMap, sign int) [3]types.SubmitOrder { + var orders [3]types.SubmitOrder + var transitingQuantity float64 + + initialBalance, _ := p.marketA.getInitialBalance(balances, p.dirA*sign) + orderA, _ := p.marketA.newOrder(p.dirA*sign, initialBalance.Float64()) + orders[0] = orderA + + q, _ := orderA.Out() + transitingQuantity = q.Float64() + + // orderB + orderB, rateB := p.marketB.newOrder(p.dirB*sign, transitingQuantity) + orders = adjustOrderQuantityByRate(orders, rateB) + + q, _ = orderB.Out() + transitingQuantity = q.Float64() + orders[1] = orderB + + orderC, rateC := p.marketC.newOrder(p.dirC*sign, transitingQuantity) + orders = adjustOrderQuantityByRate(orders, rateC) + + q, _ = orderC.Out() + orders[2] = orderC + + orders[0].Quantity = p.marketA.market.TruncateQuantity(orders[0].Quantity) + orders[1].Quantity = p.marketB.market.TruncateQuantity(orders[1].Quantity) + orders[2].Quantity = p.marketC.market.TruncateQuantity(orders[2].Quantity) + return orders +} diff --git a/pkg/strategy/tri/position.go b/pkg/strategy/tri/position.go new file mode 100644 index 0000000000..5bbbe815e8 --- /dev/null +++ b/pkg/strategy/tri/position.go @@ -0,0 +1,139 @@ +package tri + +import ( + "fmt" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type MultiCurrencyPosition struct { + Currencies map[string]fixedpoint.Value `json:"currencies"` + Markets map[string]types.Market `json:"markets"` + TotalProfits map[string]fixedpoint.Value `json:"totalProfits"` + Fees map[string]fixedpoint.Value `json:"fees"` + TradePrices map[string]fixedpoint.Value `json:"prices"` +} + +func NewMultiCurrencyPosition(markets map[string]types.Market) *MultiCurrencyPosition { + p := &MultiCurrencyPosition{ + Currencies: make(map[string]fixedpoint.Value), + Markets: make(map[string]types.Market), + TotalProfits: make(map[string]fixedpoint.Value), + TradePrices: make(map[string]fixedpoint.Value), + Fees: make(map[string]fixedpoint.Value), + } + + for _, market := range markets { + p.Markets[market.Symbol] = market + p.Currencies[market.BaseCurrency] = fixedpoint.Zero + p.Currencies[market.QuoteCurrency] = fixedpoint.Zero + p.TotalProfits[market.QuoteCurrency] = fixedpoint.Zero + p.TotalProfits[market.BaseCurrency] = fixedpoint.Zero + p.Fees[market.QuoteCurrency] = fixedpoint.Zero + p.Fees[market.BaseCurrency] = fixedpoint.Zero + p.TradePrices[market.QuoteCurrency] = fixedpoint.Zero + p.TradePrices[market.BaseCurrency] = fixedpoint.Zero + } + + return p +} + +func (p *MultiCurrencyPosition) handleTrade(trade types.Trade) { + market := p.Markets[trade.Symbol] + switch trade.Side { + case types.SideTypeBuy: + p.Currencies[market.BaseCurrency] = p.Currencies[market.BaseCurrency].Add(trade.Quantity) + p.Currencies[market.QuoteCurrency] = p.Currencies[market.QuoteCurrency].Sub(trade.QuoteQuantity) + + case types.SideTypeSell: + p.Currencies[market.BaseCurrency] = p.Currencies[market.BaseCurrency].Sub(trade.Quantity) + p.Currencies[market.QuoteCurrency] = p.Currencies[market.QuoteCurrency].Add(trade.QuoteQuantity) + } + + if types.IsUSDFiatCurrency(market.QuoteCurrency) { + p.TradePrices[market.BaseCurrency] = trade.Price + } else if types.IsUSDFiatCurrency(market.BaseCurrency) { // For USDT/TWD pair, convert USDT/TWD price to TWD/USDT + p.TradePrices[market.QuoteCurrency] = one.Div(trade.Price) + } + + if !trade.Fee.IsZero() { + if f, ok := p.Fees[trade.FeeCurrency]; ok { + p.Fees[trade.FeeCurrency] = f.Add(trade.Fee) + } else { + p.Fees[trade.FeeCurrency] = trade.Fee + } + } +} + +func (p *MultiCurrencyPosition) CollectProfits() []Profit { + var profits []Profit + for currency, base := range p.Currencies { + if base.IsZero() { + continue + } + + profit := Profit{ + Asset: currency, + Profit: base, + ProfitInUSD: fixedpoint.Zero, + } + + if price, ok := p.TradePrices[currency]; ok && !price.IsZero() { + profit.ProfitInUSD = base.Mul(price) + } else if types.IsUSDFiatCurrency(currency) { + profit.ProfitInUSD = base + } + + profits = append(profits, profit) + + if total, ok := p.TotalProfits[currency]; ok { + p.TotalProfits[currency] = total.Add(base) + } else { + p.TotalProfits[currency] = base + } + } + + p.Reset() + return profits +} + +func (p *MultiCurrencyPosition) Reset() { + for currency := range p.Currencies { + p.Currencies[currency] = fixedpoint.Zero + } +} + +func (p *MultiCurrencyPosition) String() (o string) { + o += "position: \n" + + for currency, base := range p.Currencies { + if base.IsZero() { + continue + } + + o += fmt.Sprintf("- %s: %f\n", currency, base.Float64()) + } + + o += "totalProfits: \n" + + for currency, total := range p.TotalProfits { + if total.IsZero() { + continue + } + + o += fmt.Sprintf("- %s: %f\n", currency, total.Float64()) + } + + o += "fees: \n" + + for currency, fee := range p.Fees { + if fee.IsZero() { + continue + } + + o += fmt.Sprintf("- %s: %f\n", currency, fee.Float64()) + } + + return o +} diff --git a/pkg/strategy/tri/profit.go b/pkg/strategy/tri/profit.go new file mode 100644 index 0000000000..7ea0ca0ce1 --- /dev/null +++ b/pkg/strategy/tri/profit.go @@ -0,0 +1,62 @@ +package tri + +import ( + "fmt" + + "github.com/slack-go/slack" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/style" +) + +type Profit struct { + Asset string `json:"asset"` + Profit fixedpoint.Value `json:"profit"` + ProfitInUSD fixedpoint.Value `json:"profitInUSD"` +} + +func (p *Profit) PlainText() string { + var title = fmt.Sprintf("Arbitrage Profit ") + title += style.PnLEmojiSimple(p.Profit) + " " + title += style.PnLSignString(p.Profit) + " " + p.Asset + + if !p.ProfitInUSD.IsZero() { + title += " ~= " + style.PnLSignString(p.ProfitInUSD) + " USD" + } + return title +} + +func (p *Profit) SlackAttachment() slack.Attachment { + var color = style.PnLColor(p.Profit) + var title = fmt.Sprintf("Triangular PnL ") + title += style.PnLEmojiSimple(p.Profit) + " " + title += style.PnLSignString(p.Profit) + " " + p.Asset + + if !p.ProfitInUSD.IsZero() { + title += " ~= " + style.PnLSignString(p.ProfitInUSD) + " USD" + } + + var fields []slack.AttachmentField + if !p.Profit.IsZero() { + fields = append(fields, slack.AttachmentField{ + Title: "Profit", + Value: style.PnLSignString(p.Profit) + " " + p.Asset, + Short: true, + }) + } + + if !p.ProfitInUSD.IsZero() { + fields = append(fields, slack.AttachmentField{ + Title: "Profit (~= USD)", + Value: style.PnLSignString(p.ProfitInUSD) + " USD", + Short: true, + }) + } + + return slack.Attachment{ + Color: color, + Title: title, + Fields: fields, + // Footer: "", + } +} diff --git a/pkg/strategy/tri/strategy.go b/pkg/strategy/tri/strategy.go new file mode 100644 index 0000000000..ec8b13e7fe --- /dev/null +++ b/pkg/strategy/tri/strategy.go @@ -0,0 +1,880 @@ +package tri + +import ( + "context" + "errors" + "fmt" + "math" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/sirupsen/logrus" + "go.uber.org/multierr" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/sigchan" + "github.com/c9s/bbgo/pkg/style" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +//go:generate bash symbols.sh + +const ID = "tri" + +var log = logrus.WithField("strategy", ID) + +var one = fixedpoint.One +var marketOrderProtectiveRatio = fixedpoint.NewFromFloat(0.008) +var balanceBufferRatio = fixedpoint.NewFromFloat(0.005) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Side int + +const Buy Side = 1 +const Sell Side = -1 + +func (s Side) String() string { + return s.SideType().String() +} + +func (s Side) SideType() types.SideType { + if s == 1 { + return types.SideTypeBuy + } + + return types.SideTypeSell +} + +type PathRank struct { + Path *Path + Ratio float64 +} + +// backward buy -> buy -> sell +func calculateBackwardRate(p *Path) float64 { + var ratio = 1.0 + ratio *= p.marketA.calculateRatio(-p.dirA) + ratio *= p.marketB.calculateRatio(-p.dirB) + ratio *= p.marketC.calculateRatio(-p.dirC) + return ratio +} + +// calculateForwardRatio +// path: BTCUSDT (0.000044 / 22830.410000) => USDTTWD (0.033220 / 30.101000) => BTCTWD (0.000001 / 687500.000000) <= -> 0.9995899221105569 <- 1.0000373943873788 +// 1.0 * 22830 * 30.101000 / 687500.000 + +// BTCUSDT (0.000044 / 22856.910000) => USDTTWD (0.033217 / 30.104000) => BTCTWD (0.000001 / 688002.100000) +// sell -> rate * 22856 +// sell -> rate * 30.104 +// buy -> rate / 688002.1 +// 1.0000798312 +func calculateForwardRatio(p *Path) float64 { + var ratio = 1.0 + ratio *= p.marketA.calculateRatio(p.dirA) + ratio *= p.marketB.calculateRatio(p.dirB) + ratio *= p.marketC.calculateRatio(p.dirC) + return ratio +} + +func adjustOrderQuantityByRate(orders [3]types.SubmitOrder, rate float64) [3]types.SubmitOrder { + if rate == 1.0 || math.IsNaN(rate) { + return orders + } + + for i, o := range orders { + orders[i].Quantity = o.Quantity.Mul(fixedpoint.NewFromFloat(rate)) + } + + return orders +} + +type State struct { + IOCWinTimes int `json:"iocWinningTimes"` + IOCLossTimes int `json:"iocLossTimes"` + IOCWinningRatio float64 `json:"iocWinningRatio"` +} + +type Strategy struct { + Symbols []string `json:"symbols"` + Paths [][]string `json:"paths"` + MinSpreadRatio fixedpoint.Value `json:"minSpreadRatio"` + SeparateStream bool `json:"separateStream"` + Limits map[string]fixedpoint.Value `json:"limits"` + CoolingDownTime types.Duration `json:"coolingDownTime"` + NotifyTrade bool `json:"notifyTrade"` + ResetPosition bool `json:"resetPosition"` + MarketOrderProtectiveRatio fixedpoint.Value `json:"marketOrderProtectiveRatio"` + IocOrderRatio fixedpoint.Value `json:"iocOrderRatio"` + DryRun bool `json:"dryRun"` + + markets map[string]types.Market + arbMarkets map[string]*ArbMarket + paths []*Path + + session *bbgo.ExchangeSession + + activeOrders *bbgo.ActiveOrderBook + orderStore *core.OrderStore + tradeCollector *core.TradeCollector + Position *MultiCurrencyPosition `persistence:"position"` + State *State `persistence:"state"` + TradeState *types.TradeStats `persistence:"trade_stats"` + sigC sigchan.Chan +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return ID + strings.Join(s.Symbols, "-") +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + if !s.SeparateStream { + for _, symbol := range s.Symbols { + session.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{ + Depth: types.DepthLevelFull, + }) + } + } +} + +func (s *Strategy) executeOrder(ctx context.Context, order types.SubmitOrder) *types.Order { + waitTime := 100 * time.Millisecond + for maxTries := 100; maxTries >= 0; maxTries-- { + createdOrder, err := s.session.Exchange.SubmitOrder(ctx, order) + if err != nil { + log.WithError(err).Errorf("can not submit orders") + time.Sleep(waitTime) + waitTime *= 2 + continue + } + + s.orderStore.Add(*createdOrder) + s.activeOrders.Add(*createdOrder) + return createdOrder + } + + return nil +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + + if s.TradeState == nil { + s.TradeState = types.NewTradeStats("") + } + + s.Symbols = compileSymbols(s.Symbols) + + if s.MarketOrderProtectiveRatio.IsZero() { + s.MarketOrderProtectiveRatio = marketOrderProtectiveRatio + } + + if s.MinSpreadRatio.IsZero() { + s.MinSpreadRatio = fixedpoint.NewFromFloat(1.002) + } + + if s.State == nil { + s.State = &State{} + } + + s.markets = make(map[string]types.Market) + s.sigC = sigchan.New(10) + + s.session = session + s.orderStore = core.NewOrderStore("") + s.orderStore.AddOrderUpdate = true + s.orderStore.BindStream(session.UserDataStream) + + s.activeOrders = bbgo.NewActiveOrderBook("") + s.activeOrders.BindStream(session.UserDataStream) + s.tradeCollector = core.NewTradeCollector("", nil, s.orderStore) + + for _, symbol := range s.Symbols { + market, ok := session.Market(symbol) + if !ok { + return fmt.Errorf("market not found: %s", symbol) + } + s.markets[symbol] = market + } + s.optimizeMarketQuantityPrecision() + + arbMarkets, err := s.buildArbMarkets(session, s.Symbols, s.SeparateStream, s.sigC) + if err != nil { + return err + } + + s.arbMarkets = arbMarkets + + if s.Position == nil { + s.Position = NewMultiCurrencyPosition(s.markets) + } + + if s.ResetPosition { + s.Position = NewMultiCurrencyPosition(s.markets) + } + + s.tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + s.Position.handleTrade(trade) + }) + + if s.NotifyTrade { + s.tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + bbgo.Notify(trade) + }) + } + + s.tradeCollector.BindStream(session.UserDataStream) + + for _, market := range s.arbMarkets { + m := market + if s.SeparateStream { + log.Infof("connecting %s market stream...", m.Symbol) + if err := m.stream.Connect(ctx); err != nil { + return err + } + } + } + + // build paths + // rate update and check paths + for _, pathSymbols := range s.Paths { + if len(pathSymbols) != 3 { + return errors.New("a path must contains 3 symbols") + } + + p := &Path{ + marketA: s.arbMarkets[pathSymbols[0]], + marketB: s.arbMarkets[pathSymbols[1]], + marketC: s.arbMarkets[pathSymbols[2]], + } + + if p.marketA == nil { + return fmt.Errorf("market object of %s is missing", pathSymbols[0]) + } + + if p.marketB == nil { + return fmt.Errorf("market object of %s is missing", pathSymbols[1]) + } + + if p.marketC == nil { + return fmt.Errorf("market object of %s is missing", pathSymbols[2]) + } + + if err := p.solveDirection(); err != nil { + return err + } + + s.paths = append(s.paths, p) + } + + go func() { + fs := []ratioFunction{calculateForwardRatio, calculateBackwardRate} + log.Infof("waiting for market prices ready...") + wait := true + for wait { + wait = false + for _, p := range s.paths { + if !p.Ready() { + wait = true + break + } + } + } + + log.Infof("all markets ready") + + for { + select { + case <-ctx.Done(): + return + case <-s.sigC: + minRatio := s.MinSpreadRatio.Float64() + for side, f := range fs { + ranks := s.calculateRanks(minRatio, f) + if len(ranks) == 0 { + break + } + + forward := side == 0 + bestRank := ranks[0] + if forward { + log.Infof("%d paths elected, found best forward path %s profit %.5f%%", len(ranks), bestRank.Path, (bestRank.Ratio-1.0)*100.0) + } else { + log.Infof("%d paths elected, found best backward path %s profit %.5f%%", len(ranks), bestRank.Path, (bestRank.Ratio-1.0)*100.0) + } + s.executePath(ctx, session, bestRank.Path, bestRank.Ratio, forward) + } + } + } + }() + + return nil +} + +type ratioFunction func(p *Path) float64 + +func (s *Strategy) checkMinimalOrderQuantity(orders [3]types.SubmitOrder) error { + for _, order := range orders { + market := s.arbMarkets[order.Symbol] + if order.Quantity.Compare(market.market.MinQuantity) < 0 { + return fmt.Errorf("order quantity is too small: %f < %f", order.Quantity.Float64(), market.market.MinQuantity.Float64()) + } + + if order.Quantity.Mul(order.Price).Compare(market.market.MinNotional) < 0 { + return fmt.Errorf("order min notional is too small: %f < %f", order.Quantity.Mul(order.Price).Float64(), market.market.MinNotional.Float64()) + } + } + + return nil +} + +func (s *Strategy) optimizeMarketQuantityPrecision() { + var baseMarkets = make(map[string][]types.Market) + for _, m := range s.markets { + baseMarkets[m.BaseCurrency] = append(baseMarkets[m.BaseCurrency], m) + } + + for _, markets := range baseMarkets { + var prec = -1 + for _, m := range markets { + if prec == -1 || m.VolumePrecision < prec { + prec = m.VolumePrecision + } + } + + if prec == -1 { + continue + } + + for _, m := range markets { + m.VolumePrecision = prec + s.markets[m.Symbol] = m + } + } +} + +func (s *Strategy) applyBalanceMaxQuantity(balances types.BalanceMap) types.BalanceMap { + if s.Limits == nil { + return balances + } + + for c, b := range balances { + if limit, ok := s.Limits[c]; ok { + b.Available = fixedpoint.Min(b.Available, limit) + balances[c] = b + } + } + + return balances +} + +func (s *Strategy) addBalanceBuffer(balances types.BalanceMap) (out types.BalanceMap) { + out = types.BalanceMap{} + for c, b := range balances { + ab := b + ab.Available = ab.Available.Mul(one.Sub(balanceBufferRatio)) + out[c] = ab + } + + return out +} + +func (s *Strategy) toProtectiveMarketOrder(order types.SubmitOrder, ratio fixedpoint.Value) types.SubmitOrder { + sellRatio := one.Sub(ratio) + buyRatio := one.Add(ratio) + + switch order.Side { + case types.SideTypeSell: + order.Price = order.Price.Mul(sellRatio) + + case types.SideTypeBuy: + order.Price = order.Price.Mul(buyRatio) + } + + return order +} + +func (s *Strategy) toProtectiveMarketOrders(orders [3]types.SubmitOrder, ratio fixedpoint.Value) [3]types.SubmitOrder { + sellRatio := one.Sub(ratio) + buyRatio := one.Add(ratio) + for i, order := range orders { + switch order.Side { + case types.SideTypeSell: + order.Price = order.Price.Mul(sellRatio) + + case types.SideTypeBuy: + order.Price = order.Price.Mul(buyRatio) + } + + // order.Quantity = order.Market.TruncateQuantity(order.Quantity) + // order.Type = types.OrderTypeMarket + orders[i] = order + } + + return orders +} + +func (s *Strategy) executePath(ctx context.Context, session *bbgo.ExchangeSession, p *Path, ratio float64, dir bool) { + balances := session.Account.Balances() + balances = s.addBalanceBuffer(balances) + balances = s.applyBalanceMaxQuantity(balances) + + var orders [3]types.SubmitOrder + if dir { + orders = p.newOrders(balances, 1) + } else { + orders = p.newOrders(balances, -1) + } + + if err := s.checkMinimalOrderQuantity(orders); err != nil { + log.WithError(err).Warnf("order quantity too small, skip") + return + } + + if s.DryRun { + logSubmitOrders(orders) + return + } + + createdOrders, err := s.iocOrderExecution(ctx, session, orders, ratio) + if err != nil { + log.WithError(err).Errorf("order execute error") + return + } + + if len(createdOrders) == 0 { + return + } + + log.Info(s.Position.String()) + + profits := s.Position.CollectProfits() + profitInUSD := fixedpoint.Zero + for _, profit := range profits { + bbgo.Notify(&profit) + log.Info(profit.PlainText()) + + profitInUSD = profitInUSD.Add(profit.ProfitInUSD) + + // FIXME: + // s.TradeState.Add(&profit) + } + + notifyUsdPnL(profitInUSD) + + log.Info(s.TradeState.BriefString()) + + bbgo.Sync(ctx, s) + + if s.CoolingDownTime > 0 { + log.Infof("cooling down for %s", s.CoolingDownTime.Duration().String()) + time.Sleep(s.CoolingDownTime.Duration()) + } +} + +func notifyUsdPnL(profit fixedpoint.Value) { + var title = fmt.Sprintf("Triangular Sum PnL ~= ") + title += style.PnLEmojiSimple(profit) + " " + title += style.PnLSignString(profit) + " USD" + bbgo.Notify(title) +} + +func (s *Strategy) iocOrderExecution(ctx context.Context, session *bbgo.ExchangeSession, orders [3]types.SubmitOrder, ratio float64) (types.OrderSlice, error) { + service, ok := session.Exchange.(types.ExchangeOrderQueryService) + if !ok { + return nil, errors.New("exchange does not support ExchangeOrderQueryService") + } + + var filledQuantity = fixedpoint.Zero + + // Change the first order to IOC + orders[0].Type = types.OrderTypeLimit + orders[0].TimeInForce = types.TimeInForceIOC + + var originalOrders [3]types.SubmitOrder + originalOrders[0] = orders[0] + originalOrders[1] = orders[1] + originalOrders[2] = orders[2] + logSubmitOrders(orders) + + if !s.IocOrderRatio.IsZero() { + orders[0] = s.toProtectiveMarketOrder(orders[0], s.IocOrderRatio) + } + + iocOrder := s.executeOrder(ctx, orders[0]) + if iocOrder == nil { + return nil, errors.New("ioc order submit error") + } + + iocOrderC := make(chan types.Order, 2) + defer func() { + close(iocOrderC) + }() + + go func() { + o, err := s.waitWebSocketOrderDone(ctx, iocOrder.OrderID, 300*time.Millisecond) + if err != nil { + // log.WithError(err).Errorf("ioc order wait error") + return + } else if o != nil { + select { + case iocOrderC <- *o: + default: + } + } + }() + + go func() { + o, err := waitForOrderFilled(ctx, service, *iocOrder, 3*time.Second) + if err != nil { + log.WithError(err).Errorf("ioc order restful wait error") + return + } else if o != nil { + select { + case iocOrderC <- *o: + default: + } + } + }() + + o := <-iocOrderC + + filledQuantity = o.ExecutedQuantity + + if filledQuantity.IsZero() { + s.State.IOCLossTimes++ + + // we didn't get filled + log.Infof("%s %s IOC order did not get filled, skip: %+v", o.Symbol, o.Side, o) + return nil, nil + } + + filledRatio := filledQuantity.Div(iocOrder.Quantity) + bbgo.Notify("%s %s IOC order got filled %f/%f (%s)", iocOrder.Symbol, iocOrder.Side, filledQuantity.Float64(), iocOrder.Quantity.Float64(), filledRatio.Percentage()) + log.Infof("%s %s IOC order got filled %f/%f", iocOrder.Symbol, iocOrder.Side, filledQuantity.Float64(), iocOrder.Quantity.Float64()) + + orders[1].Quantity = orders[1].Quantity.Mul(filledRatio) + orders[2].Quantity = orders[2].Quantity.Mul(filledRatio) + + if orders[1].Quantity.Compare(orders[1].Market.MinQuantity) <= 0 { + log.Warnf("order #2 quantity %f is less than min quantity %f, skip", orders[1].Quantity.Float64(), orders[1].Market.MinQuantity.Float64()) + return nil, nil + } + + if orders[2].Quantity.Compare(orders[2].Market.MinQuantity) <= 0 { + log.Warnf("order #3 quantity %f is less than min quantity %f, skip", orders[2].Quantity.Float64(), orders[2].Market.MinQuantity.Float64()) + return nil, nil + } + + orders[1] = s.toProtectiveMarketOrder(orders[1], s.MarketOrderProtectiveRatio) + orders[2] = s.toProtectiveMarketOrder(orders[2], s.MarketOrderProtectiveRatio) + + var orderC = make(chan types.Order, 2) + var wg sync.WaitGroup + wg.Add(2) + + go func() { + o := s.executeOrder(ctx, orders[1]) + orderC <- *o + wg.Done() + }() + + go func() { + o := s.executeOrder(ctx, orders[2]) + orderC <- *o + wg.Done() + }() + + wg.Wait() + + var createdOrders = make(types.OrderSlice, 3) + createdOrders[0] = *iocOrder + createdOrders[1] = <-orderC + createdOrders[2] = <-orderC + close(orderC) + + orderTrades, updatedOrders, err := s.waitOrdersAndCollectTrades(ctx, service, createdOrders) + if err != nil { + log.WithError(err).Errorf("trade collecting error") + } else { + for i, order := range updatedOrders { + trades, hasTrades := orderTrades[order.OrderID] + if !hasTrades { + continue + } + averagePrice := tradeAveragePrice(trades, order.OrderID) + updatedOrders[i].AveragePrice = averagePrice + + if market, hasMarket := s.markets[order.Symbol]; hasMarket { + updatedOrders[i].Market = market + } + + for _, originalOrder := range originalOrders { + if originalOrder.Symbol == updatedOrders[i].Symbol { + updatedOrders[i].Price = originalOrder.Price + } + } + } + s.analyzeOrders(updatedOrders) + } + + // update ioc winning ratio + s.State.IOCWinTimes++ + if s.State.IOCLossTimes == 0 { + s.State.IOCWinningRatio = 999.0 + } else { + s.State.IOCWinningRatio = float64(s.State.IOCWinTimes) / float64(s.State.IOCLossTimes) + } + + log.Infof("ioc winning ratio update: %f", s.State.IOCWinningRatio) + + return createdOrders, nil +} + +func (s *Strategy) waitWebSocketOrderDone(ctx context.Context, orderID uint64, timeoutDuration time.Duration) (*types.Order, error) { + prof := util.StartTimeProfile("waitWebSocketOrderDone") + defer prof.StopAndLog(log.Infof) + + if order, ok := s.orderStore.Get(orderID); ok { + if order.Status == types.OrderStatusFilled || order.Status == types.OrderStatusCanceled { + return &order, nil + } + } + + timeoutC := time.After(timeoutDuration) + for { + select { + + case <-ctx.Done(): + return nil, ctx.Err() + + case <-timeoutC: + return nil, fmt.Errorf("order wait time timeout %s", timeoutDuration) + + case order := <-s.orderStore.C: + if orderID == order.OrderID && (order.Status == types.OrderStatusFilled || order.Status == types.OrderStatusCanceled) { + return &order, nil + } + } + } +} + +func (s *Strategy) waitOrdersAndCollectTrades(ctx context.Context, service types.ExchangeOrderQueryService, createdOrders types.OrderSlice) (map[uint64][]types.Trade, types.OrderSlice, error) { + var err error + var orderTrades = make(map[uint64][]types.Trade) + var updatedOrders types.OrderSlice + for _, o := range createdOrders { + updatedOrder, err2 := waitForOrderFilled(ctx, service, o, time.Second) + if err2 != nil { + err = multierr.Append(err, err2) + continue + } + + trades, err3 := service.QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: o.Symbol, + OrderID: strconv.FormatUint(o.OrderID, 10), + }) + if err3 != nil { + err = multierr.Append(err, err3) + continue + } + + for _, t := range trades { + s.tradeCollector.ProcessTrade(t) + } + + orderTrades[o.OrderID] = trades + updatedOrders = append(updatedOrders, *updatedOrder) + } + + /* + */ + return orderTrades, updatedOrders, nil +} + +func (s *Strategy) analyzeOrders(orders types.OrderSlice) { + sort.Slice(orders, func(i, j int) bool { + // o1 < o2 -- earlier first + return orders[i].CreationTime.Before(orders[i].CreationTime.Time()) + }) + + log.Infof("ANALYZING ORDERS (Earlier First)") + for i, o := range orders { + in, inCurrency := o.In() + out, outCurrency := o.Out() + log.Infof("#%d %s IN %f %s -> OUT %f %s", i, o.String(), in.Float64(), inCurrency, out.Float64(), outCurrency) + } + + for _, o := range orders { + switch o.Side { + case types.SideTypeSell: + price := o.Price + priceDiff := o.AveragePrice.Sub(price) + slippage := priceDiff.Div(price) + log.Infof("%-8s %-4s %-10s AVG PRICE %f PRICE %f Q %f SLIPPAGE %.3f%%", o.Symbol, o.Side, o.Type, o.AveragePrice.Float64(), price.Float64(), o.Quantity.Float64(), slippage.Float64()*100.0) + + case types.SideTypeBuy: + price := o.Price + priceDiff := price.Sub(o.AveragePrice) + slippage := priceDiff.Div(price) + log.Infof("%-8s %-4s %-10s AVG PRICE %f PRICE %f Q %f SLIPPAGE %.3f%%", o.Symbol, o.Side, o.Type, o.AveragePrice.Float64(), price.Float64(), o.Quantity.Float64(), slippage.Float64()*100.0) + } + } +} + +func (s *Strategy) buildArbMarkets(session *bbgo.ExchangeSession, symbols []string, separateStream bool, sigC sigchan.Chan) (map[string]*ArbMarket, error) { + markets := make(map[string]*ArbMarket) + // build market object + for _, symbol := range symbols { + market, ok := s.markets[symbol] + if !ok { + return nil, fmt.Errorf("market not found: %s", symbol) + } + + m := &ArbMarket{ + Symbol: symbol, + market: market, + BaseCurrency: market.BaseCurrency, + QuoteCurrency: market.QuoteCurrency, + sigC: sigC, + } + + if separateStream { + stream := session.Exchange.NewStream() + stream.SetPublicOnly() + stream.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{ + Depth: types.DepthLevelFull, + Speed: types.SpeedHigh, + }) + + book := types.NewStreamBook(symbol) + priceUpdater := func(_ types.SliceOrderBook) { + bestAsk, bestBid, _ := book.BestBidAndAsk() + if bestAsk.Equals(m.bestAsk) && bestBid.Equals(m.bestBid) { + return + } + + m.bestBid = bestBid + m.bestAsk = bestAsk + m.updateRate() + } + book.OnUpdate(priceUpdater) + book.OnSnapshot(priceUpdater) + book.BindStream(stream) + + stream.OnDisconnect(func() { + // reset price and volume + m.bestBid = types.PriceVolume{} + m.bestAsk = types.PriceVolume{} + }) + + m.book = book + m.stream = stream + } else { + book, _ := session.OrderBook(symbol) + priceUpdater := func(_ types.SliceOrderBook) { + bestAsk, bestBid, _ := book.BestBidAndAsk() + if bestAsk.Equals(m.bestAsk) && bestBid.Equals(m.bestBid) { + return + } + + m.bestBid = bestBid + m.bestAsk = bestAsk + m.updateRate() + } + book.OnUpdate(priceUpdater) + book.OnSnapshot(priceUpdater) + + m.book = book + m.stream = session.MarketDataStream + } + + markets[symbol] = m + } + + return markets, nil +} + +func (s *Strategy) calculateRanks(minRatio float64, method func(p *Path) float64) []PathRank { + ranks := make([]PathRank, 0, len(s.paths)) + + // ranking paths here + for _, path := range s.paths { + ratio := method(path) + if ratio < minRatio { + continue + } + + p := path + ranks = append(ranks, PathRank{Path: p, Ratio: ratio}) + } + + // sort and pick up the top rank path + sort.Slice(ranks, func(i, j int) bool { + return ranks[i].Ratio > ranks[j].Ratio + }) + + return ranks +} + +func waitForOrderFilled(ctx context.Context, ex types.ExchangeOrderQueryService, order types.Order, timeout time.Duration) (*types.Order, error) { + prof := util.StartTimeProfile("waitForOrderFilled") + defer prof.StopAndLog(log.Infof) + + timeoutC := time.After(timeout) + + for { + select { + case <-timeoutC: + return nil, fmt.Errorf("order wait timeout %s", timeout) + + default: + p := util.StartTimeProfile("queryOrder") + remoteOrder, err2 := ex.QueryOrder(ctx, types.OrderQuery{ + Symbol: order.Symbol, + OrderID: strconv.FormatUint(order.OrderID, 10), + }) + p.StopAndLog(log.Infof) + + if err2 != nil { + log.WithError(err2).Errorf("order query error") + time.Sleep(100 * time.Millisecond) + continue + } + + switch remoteOrder.Status { + case types.OrderStatusFilled, types.OrderStatusCanceled: + return remoteOrder, nil + default: + log.Infof("WAITING: %s", remoteOrder.String()) + time.Sleep(5 * time.Millisecond) + } + } + } +} + +func tradeAveragePrice(trades []types.Trade, orderID uint64) fixedpoint.Value { + totalAmount := fixedpoint.Zero + totalQuantity := fixedpoint.Zero + for _, trade := range trades { + if trade.OrderID != orderID { + continue + } + + totalAmount = totalAmount.Add(trade.Price.Mul(trade.Quantity)) + totalQuantity = totalQuantity.Add(trade.Quantity) + } + + return totalAmount.Div(totalQuantity) +} diff --git a/pkg/strategy/tri/strategy_test.go b/pkg/strategy/tri/strategy_test.go new file mode 100644 index 0000000000..723240b55f --- /dev/null +++ b/pkg/strategy/tri/strategy_test.go @@ -0,0 +1,220 @@ +package tri + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/cache" + "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +var markets = make(types.MarketMap) + +func init() { + var err error + markets, err = cache.LoadExchangeMarketsWithCache(context.Background(), &binance.Exchange{}) + if err != nil { + panic(err) + } +} + +func loadMarket(symbol string) types.Market { + if market, ok := markets[symbol]; ok { + return market + } + + panic(fmt.Errorf("market %s not found", symbol)) +} + +func newArbMarket(symbol, base, quote string, askPrice, askVolume, bidPrice, bidVolume float64) *ArbMarket { + return &ArbMarket{ + Symbol: symbol, + BaseCurrency: base, + QuoteCurrency: quote, + market: loadMarket(symbol), + book: nil, + bestBid: types.PriceVolume{ + Price: fixedpoint.NewFromFloat(bidPrice), + Volume: fixedpoint.NewFromFloat(bidVolume), + }, + bestAsk: types.PriceVolume{ + Price: fixedpoint.NewFromFloat(askPrice), + Volume: fixedpoint.NewFromFloat(askVolume), + }, + buyRate: 1.0 / askPrice, + sellRate: bidPrice, + } +} + +func TestPath_calculateBackwardRatio(t *testing.T) { + // BTCUSDT 22800.0 22700.0 + // ETHBTC 0.074, 0.073 + // ETHUSDT 1630.0 1620.0 + + // sell BTCUSDT @ 22700 ( 0.1 BTC => 2270 USDT) + // buy ETHUSDT @ 1630 ( 2270 USDT => 1.3926380368 ETH) + // sell ETHBTC @ 0.073 (1.3926380368 ETH => 0.1016625767 BTC) + marketA := newArbMarket("BTCUSDT", "BTC", "USDT", 22800.0, 1.0, 22700.0, 1.0) + marketB := newArbMarket("ETHBTC", "ETH", "BTC", 0.074, 2.0, 0.073, 2.0) + marketC := newArbMarket("ETHUSDT", "ETH", "USDT", 1630.0, 2.0, 1620.0, 2.0) + path := &Path{ + marketA: marketA, + marketB: marketB, + marketC: marketC, + dirA: -1, + dirB: -1, + dirC: 1, + } + ratio := calculateForwardRatio(path) + assert.Equal(t, 0.9601706970128022, ratio) + + ratio = calculateBackwardRate(path) + assert.Equal(t, 1.0166257668711656, ratio) +} + +func TestPath_CalculateForwardRatio(t *testing.T) { + // BTCUSDT 22800.0 22700.0 + // ETHBTC 0.070, 0.069 + // ETHUSDT 1630.0 1620.0 + + // buy BTCUSDT @ 22800 ( 2280 usdt => 0.1 BTC) + // buy ETHBTC @ 0.070 ( 0.1 BTC => 1.4285714286 ETH) + // sell ETHUSDT @ 1620 ( 1.4285714286 ETH => 2,314.285714332 USDT) + marketA := newArbMarket("BTCUSDT", "BTC", "USDT", 22800.0, 1.0, 22700.0, 1.0) + marketB := newArbMarket("ETHBTC", "ETH", "BTC", 0.070, 2.0, 0.069, 2.0) + marketC := newArbMarket("ETHUSDT", "ETH", "USDT", 1630.0, 2.0, 1620.0, 2.0) + path := &Path{ + marketA: marketA, + marketB: marketB, + marketC: marketC, + dirA: -1, + dirB: -1, + dirC: 1, + } + ratio := calculateForwardRatio(path) + assert.Equal(t, 1.015037593984962, ratio) + + ratio = calculateBackwardRate(path) + assert.Equal(t, 0.9609202453987732, ratio) +} + +func TestPath_newForwardOrders(t *testing.T) { + // BTCUSDT 22800.0 22700.0 + // ETHBTC 0.070, 0.069 + // ETHUSDT 1630.0 1620.0 + + // buy BTCUSDT @ 22800 ( 2280 usdt => 0.1 BTC) + // buy ETHBTC @ 0.070 ( 0.1 BTC => 1.4285714286 ETH) + // sell ETHUSDT @ 1620 ( 1.4285714286 ETH => 2,314.285714332 USDT) + marketA := newArbMarket("BTCUSDT", "BTC", "USDT", 22800.0, 1.0, 22700.0, 1.0) + marketB := newArbMarket("ETHBTC", "ETH", "BTC", 0.070, 2.0, 0.069, 2.0) + marketC := newArbMarket("ETHUSDT", "ETH", "USDT", 1630.0, 2.0, 1620.0, 2.0) + path := &Path{ + marketA: marketA, + marketB: marketB, + marketC: marketC, + dirA: -1, + dirB: -1, + dirC: 1, + } + orders := path.newOrders(types.BalanceMap{ + "USDT": { + Currency: "USDT", + Available: fixedpoint.NewFromFloat(2280.0), + Locked: fixedpoint.Zero, + Borrowed: fixedpoint.Zero, + Interest: fixedpoint.Zero, + NetAsset: fixedpoint.Zero, + }, + }, 1) + for i, order := range orders { + t.Logf("order #%d: %+v", i, order.String()) + } + + assert.InDelta(t, 2314.17, orders[2].Price.Mul(orders[2].Quantity).Float64(), 0.01) +} + +func TestPath_newForwardOrdersWithAdjustRate(t *testing.T) { + // BTCUSDT 22800.0 22700.0 + // ETHBTC 0.070, 0.069 + // ETHUSDT 1630.0 1620.0 + + // buy BTCUSDT @ 22800 (2280 usdt => 0.1 BTC) + // buy ETHBTC @ 0.070 (0.1 BTC => 1.4285714286 ETH) + + // APPLY ADJUST RATE B: 0.7 = 1 ETH / 1.4285714286 ETH + // buy BTCUSDT @ 22800 ( 1596 usdt => 0.07 BTC) + // buy ETHBTC @ 0.070 (0.07 BTC => 1 ETH) + // sell ETHUSDT @ 1620.0 (1 ETH => 1620 USDT) + + // APPLY ADJUST RATE C: 0.5 = 0.5 ETH / 1 ETH + // buy BTCUSDT @ 22800 ( 798 usdt => 0.0035 BTC) + // buy ETHBTC @ 0.070 (0.035 BTC => 0.5 ETH) + // sell ETHUSDT @ 1620.0 (0.5 ETH => 1620 USDT) + + // sell ETHUSDT @ 1620 ( 1.4285714286 ETH => 2,314.285714332 USDT) + marketA := newArbMarket("BTCUSDT", "BTC", "USDT", 22800.0, 1.0, 22700.0, 1.0) + marketB := newArbMarket("ETHBTC", "ETH", "BTC", 0.070, 1.0, 0.069, 2.0) + marketC := newArbMarket("ETHUSDT", "ETH", "USDT", 1630.0, 0.5, 1620.0, 0.5) + path := &Path{ + marketA: marketA, + marketB: marketB, + marketC: marketC, + dirA: -1, + dirB: -1, + dirC: 1, + } + orders := path.newOrders(types.BalanceMap{ + "USDT": { + Currency: "USDT", + Available: fixedpoint.NewFromFloat(2280.0), + Locked: fixedpoint.Zero, + Borrowed: fixedpoint.Zero, + Interest: fixedpoint.Zero, + NetAsset: fixedpoint.Zero, + }, + }, 1) + for i, order := range orders { + t.Logf("order #%d: %+v", i, order.String()) + } + + assert.Equal(t, "0.03499", orders[0].Quantity.String()) + assert.Equal(t, "0.5", orders[1].Quantity.String()) + assert.Equal(t, "0.5", orders[2].Quantity.String()) +} + +func Test_fitQuantityByQuote(t *testing.T) { + type args struct { + price float64 + quantity float64 + quoteBalance float64 + } + tests := []struct { + name string + args args + want float64 + }{ + { + name: "simple", + args: args{ + price: 1630.0, + quantity: 2.0, + quoteBalance: 1000, + }, + want: 0.6134969325153374, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _ := fitQuantityByQuote(tt.args.price, tt.args.quantity, tt.args.quoteBalance) + if !assert.Equal(t, got, tt.want) { + t.Errorf("fitQuantityByQuote() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/strategy/tri/symbols.go b/pkg/strategy/tri/symbols.go new file mode 100644 index 0000000000..38abba858b --- /dev/null +++ b/pkg/strategy/tri/symbols.go @@ -0,0 +1,2212 @@ +// Code generated by "bash symbols.sh"; DO NOT EDIT. +package tri +const ( + AAVEBKRW = "AAVEBKRW" + AAVEBNB = "AAVEBNB" + AAVEBRL = "AAVEBRL" + AAVEBTC = "AAVEBTC" + AAVEBUSD = "AAVEBUSD" + AAVEETH = "AAVEETH" + AAVEUSDT = "AAVEUSDT" + ACABTC = "ACABTC" + ACABUSD = "ACABUSD" + ACAUSDT = "ACAUSDT" + ACHBTC = "ACHBTC" + ACHBUSD = "ACHBUSD" + ACHTRY = "ACHTRY" + ACHUSDT = "ACHUSDT" + ACMBTC = "ACMBTC" + ACMBUSD = "ACMBUSD" + ACMUSDT = "ACMUSDT" + ADAAUD = "ADAAUD" + ADABIDR = "ADABIDR" + ADABKRW = "ADABKRW" + ADABNB = "ADABNB" + ADABRL = "ADABRL" + ADABTC = "ADABTC" + ADABUSD = "ADABUSD" + ADAETH = "ADAETH" + ADAEUR = "ADAEUR" + ADAGBP = "ADAGBP" + ADAPAX = "ADAPAX" + ADARUB = "ADARUB" + ADATRY = "ADATRY" + ADATUSD = "ADATUSD" + ADATWD = "ADATWD" + ADAUSDC = "ADAUSDC" + ADAUSDT = "ADAUSDT" + ADXBNB = "ADXBNB" + ADXBTC = "ADXBTC" + ADXBUSD = "ADXBUSD" + ADXETH = "ADXETH" + ADXUSDT = "ADXUSDT" + AEBNB = "AEBNB" + AEBTC = "AEBTC" + AEETH = "AEETH" + AERGOBTC = "AERGOBTC" + AERGOBUSD = "AERGOBUSD" + AGIBNB = "AGIBNB" + AGIBTC = "AGIBTC" + AGIETH = "AGIETH" + AGIXBTC = "AGIXBTC" + AGIXBUSD = "AGIXBUSD" + AGIXTRY = "AGIXTRY" + AGIXUSDT = "AGIXUSDT" + AGLDBNB = "AGLDBNB" + AGLDBTC = "AGLDBTC" + AGLDBUSD = "AGLDBUSD" + AGLDUSDT = "AGLDUSDT" + AIONBNB = "AIONBNB" + AIONBTC = "AIONBTC" + AIONBUSD = "AIONBUSD" + AIONETH = "AIONETH" + AIONUSDT = "AIONUSDT" + AKROBTC = "AKROBTC" + AKROBUSD = "AKROBUSD" + AKROUSDT = "AKROUSDT" + ALCXBTC = "ALCXBTC" + ALCXBUSD = "ALCXBUSD" + ALCXUSDT = "ALCXUSDT" + ALGOBIDR = "ALGOBIDR" + ALGOBNB = "ALGOBNB" + ALGOBTC = "ALGOBTC" + ALGOBUSD = "ALGOBUSD" + ALGOETH = "ALGOETH" + ALGOPAX = "ALGOPAX" + ALGORUB = "ALGORUB" + ALGOTRY = "ALGOTRY" + ALGOTUSD = "ALGOTUSD" + ALGOUSDC = "ALGOUSDC" + ALGOUSDT = "ALGOUSDT" + ALICEBIDR = "ALICEBIDR" + ALICEBNB = "ALICEBNB" + ALICEBTC = "ALICEBTC" + ALICEBUSD = "ALICEBUSD" + ALICETRY = "ALICETRY" + ALICETWD = "ALICETWD" + ALICEUSDT = "ALICEUSDT" + ALPACABNB = "ALPACABNB" + ALPACABTC = "ALPACABTC" + ALPACABUSD = "ALPACABUSD" + ALPACAUSDT = "ALPACAUSDT" + ALPHABNB = "ALPHABNB" + ALPHABTC = "ALPHABTC" + ALPHABUSD = "ALPHABUSD" + ALPHAUSDT = "ALPHAUSDT" + ALPINEBTC = "ALPINEBTC" + ALPINEBUSD = "ALPINEBUSD" + ALPINEEUR = "ALPINEEUR" + ALPINETRY = "ALPINETRY" + ALPINEUSDT = "ALPINEUSDT" + AMBBNB = "AMBBNB" + AMBBTC = "AMBBTC" + AMBBUSD = "AMBBUSD" + AMBETH = "AMBETH" + AMBUSDT = "AMBUSDT" + AMPBNB = "AMPBNB" + AMPBTC = "AMPBTC" + AMPBUSD = "AMPBUSD" + AMPUSDT = "AMPUSDT" + ANCBNB = "ANCBNB" + ANCBTC = "ANCBTC" + ANCBUSD = "ANCBUSD" + ANCUSDT = "ANCUSDT" + ANKRBNB = "ANKRBNB" + ANKRBTC = "ANKRBTC" + ANKRBUSD = "ANKRBUSD" + ANKRPAX = "ANKRPAX" + ANKRTRY = "ANKRTRY" + ANKRTUSD = "ANKRTUSD" + ANKRUSDC = "ANKRUSDC" + ANKRUSDT = "ANKRUSDT" + ANTBNB = "ANTBNB" + ANTBTC = "ANTBTC" + ANTBUSD = "ANTBUSD" + ANTUSDT = "ANTUSDT" + ANYBTC = "ANYBTC" + ANYBUSD = "ANYBUSD" + ANYUSDT = "ANYUSDT" + APEAUD = "APEAUD" + APEBNB = "APEBNB" + APEBRL = "APEBRL" + APEBTC = "APEBTC" + APEBUSD = "APEBUSD" + APEETH = "APEETH" + APEEUR = "APEEUR" + APEGBP = "APEGBP" + APETRY = "APETRY" + APETWD = "APETWD" + APEUSDT = "APEUSDT" + API3BNB = "API3BNB" + API3BTC = "API3BTC" + API3BUSD = "API3BUSD" + API3TRY = "API3TRY" + API3USDT = "API3USDT" + APPCBNB = "APPCBNB" + APPCBTC = "APPCBTC" + APPCETH = "APPCETH" + APTBRL = "APTBRL" + APTBTC = "APTBTC" + APTBUSD = "APTBUSD" + APTETH = "APTETH" + APTEUR = "APTEUR" + APTTRY = "APTTRY" + APTUSDT = "APTUSDT" + ARBBTC = "ARBBTC" + ARBEUR = "ARBEUR" + ARBNB = "ARBNB" + ARBRUB = "ARBRUB" + ARBTC = "ARBTC" + ARBTRY = "ARBTRY" + ARBTUSD = "ARBTUSD" + ARBUSD = "ARBUSD" + ARBUSDT = "ARBUSDT" + ARDRBNB = "ARDRBNB" + ARDRBTC = "ARDRBTC" + ARDRETH = "ARDRETH" + ARDRUSDT = "ARDRUSDT" + ARKBTC = "ARKBTC" + ARKBUSD = "ARKBUSD" + ARKETH = "ARKETH" + ARNBTC = "ARNBTC" + ARNETH = "ARNETH" + ARPABNB = "ARPABNB" + ARPABTC = "ARPABTC" + ARPABUSD = "ARPABUSD" + ARPAETH = "ARPAETH" + ARPARUB = "ARPARUB" + ARPATRY = "ARPATRY" + ARPAUSDT = "ARPAUSDT" + ARUSDT = "ARUSDT" + ASRBTC = "ASRBTC" + ASRBUSD = "ASRBUSD" + ASRUSDT = "ASRUSDT" + ASTBTC = "ASTBTC" + ASTETH = "ASTETH" + ASTRBTC = "ASTRBTC" + ASTRBUSD = "ASTRBUSD" + ASTRETH = "ASTRETH" + ASTRUSDT = "ASTRUSDT" + ATABNB = "ATABNB" + ATABTC = "ATABTC" + ATABUSD = "ATABUSD" + ATAUSDT = "ATAUSDT" + ATMBTC = "ATMBTC" + ATMBUSD = "ATMBUSD" + ATMUSDT = "ATMUSDT" + ATOMBIDR = "ATOMBIDR" + ATOMBNB = "ATOMBNB" + ATOMBRL = "ATOMBRL" + ATOMBTC = "ATOMBTC" + ATOMBUSD = "ATOMBUSD" + ATOMETH = "ATOMETH" + ATOMEUR = "ATOMEUR" + ATOMPAX = "ATOMPAX" + ATOMTRY = "ATOMTRY" + ATOMTUSD = "ATOMTUSD" + ATOMUSDC = "ATOMUSDC" + ATOMUSDT = "ATOMUSDT" + AUCTIONBTC = "AUCTIONBTC" + AUCTIONBUSD = "AUCTIONBUSD" + AUCTIONUSDT = "AUCTIONUSDT" + AUDBUSD = "AUDBUSD" + AUDIOBTC = "AUDIOBTC" + AUDIOBUSD = "AUDIOBUSD" + AUDIOTRY = "AUDIOTRY" + AUDIOUSDT = "AUDIOUSDT" + AUDUSDC = "AUDUSDC" + AUDUSDT = "AUDUSDT" + AUTOBTC = "AUTOBTC" + AUTOBUSD = "AUTOBUSD" + AUTOUSDT = "AUTOUSDT" + AVABNB = "AVABNB" + AVABTC = "AVABTC" + AVABUSD = "AVABUSD" + AVAUSDT = "AVAUSDT" + AVAXAUD = "AVAXAUD" + AVAXBIDR = "AVAXBIDR" + AVAXBNB = "AVAXBNB" + AVAXBRL = "AVAXBRL" + AVAXBTC = "AVAXBTC" + AVAXBUSD = "AVAXBUSD" + AVAXETH = "AVAXETH" + AVAXEUR = "AVAXEUR" + AVAXGBP = "AVAXGBP" + AVAXTRY = "AVAXTRY" + AVAXUSDT = "AVAXUSDT" + AXSAUD = "AXSAUD" + AXSBNB = "AXSBNB" + AXSBRL = "AXSBRL" + AXSBTC = "AXSBTC" + AXSBUSD = "AXSBUSD" + AXSETH = "AXSETH" + AXSTRY = "AXSTRY" + AXSUSDT = "AXSUSDT" + BADGERBTC = "BADGERBTC" + BADGERBUSD = "BADGERBUSD" + BADGERUSDT = "BADGERUSDT" + BAKEBNB = "BAKEBNB" + BAKEBTC = "BAKEBTC" + BAKEBUSD = "BAKEBUSD" + BAKEUSDT = "BAKEUSDT" + BALBNB = "BALBNB" + BALBTC = "BALBTC" + BALBUSD = "BALBUSD" + BALUSDT = "BALUSDT" + BANDBNB = "BANDBNB" + BANDBTC = "BANDBTC" + BANDBUSD = "BANDBUSD" + BANDUSDT = "BANDUSDT" + BARBTC = "BARBTC" + BARBUSD = "BARBUSD" + BARUSDT = "BARUSDT" + BATBNB = "BATBNB" + BATBTC = "BATBTC" + BATBUSD = "BATBUSD" + BATETH = "BATETH" + BATPAX = "BATPAX" + BATTUSD = "BATTUSD" + BATUSDC = "BATUSDC" + BATUSDT = "BATUSDT" + BCCBNB = "BCCBNB" + BCCBTC = "BCCBTC" + BCCETH = "BCCETH" + BCCUSDT = "BCCUSDT" + BCDBTC = "BCDBTC" + BCDETH = "BCDETH" + BCHABCBTC = "BCHABCBTC" + BCHABCBUSD = "BCHABCBUSD" + BCHABCPAX = "BCHABCPAX" + BCHABCTUSD = "BCHABCTUSD" + BCHABCUSDC = "BCHABCUSDC" + BCHABCUSDT = "BCHABCUSDT" + BCHABUSD = "BCHABUSD" + BCHBNB = "BCHBNB" + BCHBTC = "BCHBTC" + BCHBUSD = "BCHBUSD" + BCHEUR = "BCHEUR" + BCHPAX = "BCHPAX" + BCHSVBTC = "BCHSVBTC" + BCHSVPAX = "BCHSVPAX" + BCHSVTUSD = "BCHSVTUSD" + BCHSVUSDC = "BCHSVUSDC" + BCHSVUSDT = "BCHSVUSDT" + BCHTUSD = "BCHTUSD" + BCHTWD = "BCHTWD" + BCHUSDC = "BCHUSDC" + BCHUSDT = "BCHUSDT" + BCNBNB = "BCNBNB" + BCNBTC = "BCNBTC" + BCNETH = "BCNETH" + BCNTTWD = "BCNTTWD" + BCNTUSDT = "BCNTUSDT" + BCPTBNB = "BCPTBNB" + BCPTBTC = "BCPTBTC" + BCPTETH = "BCPTETH" + BCPTPAX = "BCPTPAX" + BCPTTUSD = "BCPTTUSD" + BCPTUSDC = "BCPTUSDC" + BDOTDOT = "BDOTDOT" + BEAMBNB = "BEAMBNB" + BEAMBTC = "BEAMBTC" + BEAMUSDT = "BEAMUSDT" + BEARBUSD = "BEARBUSD" + BEARUSDT = "BEARUSDT" + BELBNB = "BELBNB" + BELBTC = "BELBTC" + BELBUSD = "BELBUSD" + BELETH = "BELETH" + BELTRY = "BELTRY" + BELUSDT = "BELUSDT" + BETABNB = "BETABNB" + BETABTC = "BETABTC" + BETABUSD = "BETABUSD" + BETAETH = "BETAETH" + BETAUSDT = "BETAUSDT" + BETHBUSD = "BETHBUSD" + BETHETH = "BETHETH" + BETHUSDT = "BETHUSDT" + BGBPUSDC = "BGBPUSDC" + BICOBTC = "BICOBTC" + BICOBUSD = "BICOBUSD" + BICOUSDT = "BICOUSDT" + BIFIBNB = "BIFIBNB" + BIFIBUSD = "BIFIBUSD" + BIFIUSDT = "BIFIUSDT" + BKRWBUSD = "BKRWBUSD" + BKRWUSDT = "BKRWUSDT" + BLZBNB = "BLZBNB" + BLZBTC = "BLZBTC" + BLZBUSD = "BLZBUSD" + BLZETH = "BLZETH" + BLZUSDT = "BLZUSDT" + BNBAUD = "BNBAUD" + BNBBEARBUSD = "BNBBEARBUSD" + BNBBEARUSDT = "BNBBEARUSDT" + BNBBIDR = "BNBBIDR" + BNBBKRW = "BNBBKRW" + BNBBRL = "BNBBRL" + BNBBTC = "BNBBTC" + BNBBULLBUSD = "BNBBULLBUSD" + BNBBULLUSDT = "BNBBULLUSDT" + BNBBUSD = "BNBBUSD" + BNBDAI = "BNBDAI" + BNBETH = "BNBETH" + BNBEUR = "BNBEUR" + BNBGBP = "BNBGBP" + BNBIDRT = "BNBIDRT" + BNBNGN = "BNBNGN" + BNBPAX = "BNBPAX" + BNBRUB = "BNBRUB" + BNBTRY = "BNBTRY" + BNBTUSD = "BNBTUSD" + BNBTWD = "BNBTWD" + BNBUAH = "BNBUAH" + BNBUSDC = "BNBUSDC" + BNBUSDP = "BNBUSDP" + BNBUSDS = "BNBUSDS" + BNBUSDT = "BNBUSDT" + BNBUST = "BNBUST" + BNBZAR = "BNBZAR" + BNTBTC = "BNTBTC" + BNTBUSD = "BNTBUSD" + BNTETH = "BNTETH" + BNTUSDT = "BNTUSDT" + BNXBNB = "BNXBNB" + BNXBTC = "BNXBTC" + BNXBUSD = "BNXBUSD" + BNXUSDT = "BNXUSDT" + BONDBNB = "BONDBNB" + BONDBTC = "BONDBTC" + BONDBUSD = "BONDBUSD" + BONDETH = "BONDETH" + BONDUSDT = "BONDUSDT" + BOTBTC = "BOTBTC" + BOTBUSD = "BOTBUSD" + BQXBTC = "BQXBTC" + BQXETH = "BQXETH" + BRDBNB = "BRDBNB" + BRDBTC = "BRDBTC" + BRDETH = "BRDETH" + BSWBNB = "BSWBNB" + BSWBUSD = "BSWBUSD" + BSWETH = "BSWETH" + BSWTRY = "BSWTRY" + BSWUSDT = "BSWUSDT" + BTCAUD = "BTCAUD" + BTCBBTC = "BTCBBTC" + BTCBIDR = "BTCBIDR" + BTCBKRW = "BTCBKRW" + BTCBRL = "BTCBRL" + BTCBUSD = "BTCBUSD" + BTCDAI = "BTCDAI" + BTCEUR = "BTCEUR" + BTCGBP = "BTCGBP" + BTCIDRT = "BTCIDRT" + BTCNGN = "BTCNGN" + BTCPAX = "BTCPAX" + BTCPLN = "BTCPLN" + BTCRON = "BTCRON" + BTCRUB = "BTCRUB" + BTCSTBTC = "BTCSTBTC" + BTCSTBUSD = "BTCSTBUSD" + BTCSTUSDT = "BTCSTUSDT" + BTCTRY = "BTCTRY" + BTCTUSD = "BTCTUSD" + BTCTWD = "BTCTWD" + BTCUAH = "BTCUAH" + BTCUSDC = "BTCUSDC" + BTCUSDP = "BTCUSDP" + BTCUSDS = "BTCUSDS" + BTCUSDT = "BTCUSDT" + BTCUST = "BTCUST" + BTCVAI = "BTCVAI" + BTCZAR = "BTCZAR" + BTGBTC = "BTGBTC" + BTGBUSD = "BTGBUSD" + BTGETH = "BTGETH" + BTGUSDT = "BTGUSDT" + BTSBNB = "BTSBNB" + BTSBTC = "BTSBTC" + BTSBUSD = "BTSBUSD" + BTSETH = "BTSETH" + BTSUSDT = "BTSUSDT" + BTTBNB = "BTTBNB" + BTTBRL = "BTTBRL" + BTTBTC = "BTTBTC" + BTTBUSD = "BTTBUSD" + BTTCBUSD = "BTTCBUSD" + BTTCTRY = "BTTCTRY" + BTTCUSDC = "BTTCUSDC" + BTTCUSDT = "BTTCUSDT" + BTTEUR = "BTTEUR" + BTTPAX = "BTTPAX" + BTTTRX = "BTTTRX" + BTTTRY = "BTTTRY" + BTTTUSD = "BTTTUSD" + BTTUSDC = "BTTUSDC" + BTTUSDT = "BTTUSDT" + BULLBUSD = "BULLBUSD" + BULLUSDT = "BULLUSDT" + BURGERBNB = "BURGERBNB" + BURGERBUSD = "BURGERBUSD" + BURGERETH = "BURGERETH" + BURGERUSDT = "BURGERUSDT" + BUSDBIDR = "BUSDBIDR" + BUSDBKRW = "BUSDBKRW" + BUSDBRL = "BUSDBRL" + BUSDBVND = "BUSDBVND" + BUSDDAI = "BUSDDAI" + BUSDIDRT = "BUSDIDRT" + BUSDNGN = "BUSDNGN" + BUSDPLN = "BUSDPLN" + BUSDRON = "BUSDRON" + BUSDRUB = "BUSDRUB" + BUSDTRY = "BUSDTRY" + BUSDUAH = "BUSDUAH" + BUSDUSDT = "BUSDUSDT" + BUSDVAI = "BUSDVAI" + BUSDZAR = "BUSDZAR" + BZRXBNB = "BZRXBNB" + BZRXBTC = "BZRXBTC" + BZRXBUSD = "BZRXBUSD" + BZRXUSDT = "BZRXUSDT" + C98BNB = "C98BNB" + C98BRL = "C98BRL" + C98BTC = "C98BTC" + C98BUSD = "C98BUSD" + C98USDT = "C98USDT" + CAKEAUD = "CAKEAUD" + CAKEBNB = "CAKEBNB" + CAKEBRL = "CAKEBRL" + CAKEBTC = "CAKEBTC" + CAKEBUSD = "CAKEBUSD" + CAKEGBP = "CAKEGBP" + CAKEUSDT = "CAKEUSDT" + CDTBTC = "CDTBTC" + CDTETH = "CDTETH" + CELOBTC = "CELOBTC" + CELOBUSD = "CELOBUSD" + CELOUSDT = "CELOUSDT" + CELRBNB = "CELRBNB" + CELRBTC = "CELRBTC" + CELRBUSD = "CELRBUSD" + CELRETH = "CELRETH" + CELRUSDT = "CELRUSDT" + CFXBTC = "CFXBTC" + CFXBUSD = "CFXBUSD" + CFXTRY = "CFXTRY" + CFXUSDT = "CFXUSDT" + CHATBTC = "CHATBTC" + CHATETH = "CHATETH" + CHESSBNB = "CHESSBNB" + CHESSBTC = "CHESSBTC" + CHESSBUSD = "CHESSBUSD" + CHESSUSDT = "CHESSUSDT" + CHRBNB = "CHRBNB" + CHRBTC = "CHRBTC" + CHRBUSD = "CHRBUSD" + CHRETH = "CHRETH" + CHRUSDT = "CHRUSDT" + CHZBNB = "CHZBNB" + CHZBRL = "CHZBRL" + CHZBTC = "CHZBTC" + CHZBUSD = "CHZBUSD" + CHZEUR = "CHZEUR" + CHZGBP = "CHZGBP" + CHZTRY = "CHZTRY" + CHZUSDT = "CHZUSDT" + CITYBNB = "CITYBNB" + CITYBTC = "CITYBTC" + CITYBUSD = "CITYBUSD" + CITYUSDT = "CITYUSDT" + CKBBTC = "CKBBTC" + CKBBUSD = "CKBBUSD" + CKBUSDT = "CKBUSDT" + CLOAKBTC = "CLOAKBTC" + CLOAKETH = "CLOAKETH" + CLVBNB = "CLVBNB" + CLVBTC = "CLVBTC" + CLVBUSD = "CLVBUSD" + CLVUSDT = "CLVUSDT" + CMTBNB = "CMTBNB" + CMTBTC = "CMTBTC" + CMTETH = "CMTETH" + CNDBNB = "CNDBNB" + CNDBTC = "CNDBTC" + CNDETH = "CNDETH" + COCOSBNB = "COCOSBNB" + COCOSBTC = "COCOSBTC" + COCOSBUSD = "COCOSBUSD" + COCOSTRY = "COCOSTRY" + COCOSUSDT = "COCOSUSDT" + COMPBNB = "COMPBNB" + COMPBTC = "COMPBTC" + COMPBUSD = "COMPBUSD" + COMPTWD = "COMPTWD" + COMPUSDT = "COMPUSDT" + COSBNB = "COSBNB" + COSBTC = "COSBTC" + COSBUSD = "COSBUSD" + COSTRY = "COSTRY" + COSUSDT = "COSUSDT" + COTIBNB = "COTIBNB" + COTIBTC = "COTIBTC" + COTIBUSD = "COTIBUSD" + COTIUSDT = "COTIUSDT" + COVERBUSD = "COVERBUSD" + COVERETH = "COVERETH" + CREAMBNB = "CREAMBNB" + CREAMBUSD = "CREAMBUSD" + CRVBNB = "CRVBNB" + CRVBTC = "CRVBTC" + CRVBUSD = "CRVBUSD" + CRVETH = "CRVETH" + CRVUSDT = "CRVUSDT" + CTKBNB = "CTKBNB" + CTKBTC = "CTKBTC" + CTKBUSD = "CTKBUSD" + CTKUSDT = "CTKUSDT" + CTSIBNB = "CTSIBNB" + CTSIBTC = "CTSIBTC" + CTSIBUSD = "CTSIBUSD" + CTSIUSDT = "CTSIUSDT" + CTXCBNB = "CTXCBNB" + CTXCBTC = "CTXCBTC" + CTXCBUSD = "CTXCBUSD" + CTXCUSDT = "CTXCUSDT" + CVCBNB = "CVCBNB" + CVCBTC = "CVCBTC" + CVCBUSD = "CVCBUSD" + CVCETH = "CVCETH" + CVCUSDT = "CVCUSDT" + CVPBUSD = "CVPBUSD" + CVPETH = "CVPETH" + CVPUSDT = "CVPUSDT" + CVXBTC = "CVXBTC" + CVXBUSD = "CVXBUSD" + CVXUSDT = "CVXUSDT" + DAIBNB = "DAIBNB" + DAIBTC = "DAIBTC" + DAIBUSD = "DAIBUSD" + DAIUSDT = "DAIUSDT" + DARBNB = "DARBNB" + DARBTC = "DARBTC" + DARBUSD = "DARBUSD" + DARETH = "DARETH" + DAREUR = "DAREUR" + DARTRY = "DARTRY" + DARUSDT = "DARUSDT" + DASHBNB = "DASHBNB" + DASHBTC = "DASHBTC" + DASHBUSD = "DASHBUSD" + DASHETH = "DASHETH" + DASHUSDT = "DASHUSDT" + DATABTC = "DATABTC" + DATABUSD = "DATABUSD" + DATAETH = "DATAETH" + DATAUSDT = "DATAUSDT" + DCRBNB = "DCRBNB" + DCRBTC = "DCRBTC" + DCRBUSD = "DCRBUSD" + DCRUSDT = "DCRUSDT" + DEGOBTC = "DEGOBTC" + DEGOBUSD = "DEGOBUSD" + DEGOUSDT = "DEGOUSDT" + DENTBTC = "DENTBTC" + DENTBUSD = "DENTBUSD" + DENTETH = "DENTETH" + DENTTRY = "DENTTRY" + DENTUSDT = "DENTUSDT" + DEXEBUSD = "DEXEBUSD" + DEXEETH = "DEXEETH" + DEXEUSDT = "DEXEUSDT" + DFBUSD = "DFBUSD" + DFETH = "DFETH" + DFUSDT = "DFUSDT" + DGBBTC = "DGBBTC" + DGBBUSD = "DGBBUSD" + DGBUSDT = "DGBUSDT" + DGDBTC = "DGDBTC" + DGDETH = "DGDETH" + DIABNB = "DIABNB" + DIABTC = "DIABTC" + DIABUSD = "DIABUSD" + DIAUSDT = "DIAUSDT" + DLTBNB = "DLTBNB" + DLTBTC = "DLTBTC" + DLTETH = "DLTETH" + DNTBTC = "DNTBTC" + DNTBUSD = "DNTBUSD" + DNTETH = "DNTETH" + DNTUSDT = "DNTUSDT" + DOCKBTC = "DOCKBTC" + DOCKBUSD = "DOCKBUSD" + DOCKETH = "DOCKETH" + DOCKUSDT = "DOCKUSDT" + DODOBTC = "DODOBTC" + DODOBUSD = "DODOBUSD" + DODOUSDT = "DODOUSDT" + DOGEAUD = "DOGEAUD" + DOGEBIDR = "DOGEBIDR" + DOGEBNB = "DOGEBNB" + DOGEBRL = "DOGEBRL" + DOGEBTC = "DOGEBTC" + DOGEBUSD = "DOGEBUSD" + DOGEEUR = "DOGEEUR" + DOGEGBP = "DOGEGBP" + DOGEPAX = "DOGEPAX" + DOGERUB = "DOGERUB" + DOGETRY = "DOGETRY" + DOGETWD = "DOGETWD" + DOGEUSDC = "DOGEUSDC" + DOGEUSDT = "DOGEUSDT" + DOTAUD = "DOTAUD" + DOTBIDR = "DOTBIDR" + DOTBKRW = "DOTBKRW" + DOTBNB = "DOTBNB" + DOTBRL = "DOTBRL" + DOTBTC = "DOTBTC" + DOTBUSD = "DOTBUSD" + DOTETH = "DOTETH" + DOTEUR = "DOTEUR" + DOTGBP = "DOTGBP" + DOTNGN = "DOTNGN" + DOTRUB = "DOTRUB" + DOTTRY = "DOTTRY" + DOTTWD = "DOTTWD" + DOTUSDT = "DOTUSDT" + DREPBNB = "DREPBNB" + DREPBTC = "DREPBTC" + DREPBUSD = "DREPBUSD" + DREPUSDT = "DREPUSDT" + DUSKBNB = "DUSKBNB" + DUSKBTC = "DUSKBTC" + DUSKBUSD = "DUSKBUSD" + DUSKPAX = "DUSKPAX" + DUSKUSDC = "DUSKUSDC" + DUSKUSDT = "DUSKUSDT" + DYDXBNB = "DYDXBNB" + DYDXBTC = "DYDXBTC" + DYDXBUSD = "DYDXBUSD" + DYDXETH = "DYDXETH" + DYDXUSDT = "DYDXUSDT" + EASYBTC = "EASYBTC" + EASYETH = "EASYETH" + EDOBTC = "EDOBTC" + EDOETH = "EDOETH" + EGLDBNB = "EGLDBNB" + EGLDBTC = "EGLDBTC" + EGLDBUSD = "EGLDBUSD" + EGLDETH = "EGLDETH" + EGLDEUR = "EGLDEUR" + EGLDRON = "EGLDRON" + EGLDUSDT = "EGLDUSDT" + ELFBTC = "ELFBTC" + ELFBUSD = "ELFBUSD" + ELFETH = "ELFETH" + ELFUSDT = "ELFUSDT" + ENGBTC = "ENGBTC" + ENGETH = "ENGETH" + ENJBNB = "ENJBNB" + ENJBRL = "ENJBRL" + ENJBTC = "ENJBTC" + ENJBUSD = "ENJBUSD" + ENJETH = "ENJETH" + ENJEUR = "ENJEUR" + ENJGBP = "ENJGBP" + ENJTRY = "ENJTRY" + ENJUSDT = "ENJUSDT" + ENSBNB = "ENSBNB" + ENSBTC = "ENSBTC" + ENSBUSD = "ENSBUSD" + ENSTRY = "ENSTRY" + ENSTWD = "ENSTWD" + ENSUSDT = "ENSUSDT" + EOSAUD = "EOSAUD" + EOSBEARBUSD = "EOSBEARBUSD" + EOSBEARUSDT = "EOSBEARUSDT" + EOSBNB = "EOSBNB" + EOSBTC = "EOSBTC" + EOSBULLBUSD = "EOSBULLBUSD" + EOSBULLUSDT = "EOSBULLUSDT" + EOSBUSD = "EOSBUSD" + EOSETH = "EOSETH" + EOSEUR = "EOSEUR" + EOSPAX = "EOSPAX" + EOSTRY = "EOSTRY" + EOSTUSD = "EOSTUSD" + EOSUSDC = "EOSUSDC" + EOSUSDT = "EOSUSDT" + EPSBTC = "EPSBTC" + EPSBUSD = "EPSBUSD" + EPSUSDT = "EPSUSDT" + EPXBUSD = "EPXBUSD" + EPXUSDT = "EPXUSDT" + ERDBNB = "ERDBNB" + ERDBTC = "ERDBTC" + ERDBUSD = "ERDBUSD" + ERDPAX = "ERDPAX" + ERDUSDC = "ERDUSDC" + ERDUSDT = "ERDUSDT" + ERNBNB = "ERNBNB" + ERNBUSD = "ERNBUSD" + ERNUSDT = "ERNUSDT" + ETCBNB = "ETCBNB" + ETCBRL = "ETCBRL" + ETCBTC = "ETCBTC" + ETCBUSD = "ETCBUSD" + ETCETH = "ETCETH" + ETCEUR = "ETCEUR" + ETCGBP = "ETCGBP" + ETCPAX = "ETCPAX" + ETCTRY = "ETCTRY" + ETCTUSD = "ETCTUSD" + ETCTWD = "ETCTWD" + ETCUSDC = "ETCUSDC" + ETCUSDT = "ETCUSDT" + ETCUSDTETHBTC = "ETCUSDTETHBTC" + ETHAUD = "ETHAUD" + ETHBEARBUSD = "ETHBEARBUSD" + ETHBEARUSDT = "ETHBEARUSDT" + ETHBIDR = "ETHBIDR" + ETHBKRW = "ETHBKRW" + ETHBRL = "ETHBRL" + ETHBTC = "ETHBTC" + ETHBULLBUSD = "ETHBULLBUSD" + ETHBULLUSDT = "ETHBULLUSDT" + ETHBUSD = "ETHBUSD" + ETHDAI = "ETHDAI" + ETHEUR = "ETHEUR" + ETHGBP = "ETHGBP" + ETHNGN = "ETHNGN" + ETHPAX = "ETHPAX" + ETHPLN = "ETHPLN" + ETHRUB = "ETHRUB" + ETHTRY = "ETHTRY" + ETHTUSD = "ETHTUSD" + ETHTWD = "ETHTWD" + ETHUAH = "ETHUAH" + ETHUSDC = "ETHUSDC" + ETHUSDP = "ETHUSDP" + ETHUSDT = "ETHUSDT" + ETHUST = "ETHUST" + ETHZAR = "ETHZAR" + EURBUSD = "EURBUSD" + EURUSDT = "EURUSDT" + EVXBTC = "EVXBTC" + EVXETH = "EVXETH" + FARMBNB = "FARMBNB" + FARMBTC = "FARMBTC" + FARMBUSD = "FARMBUSD" + FARMETH = "FARMETH" + FARMUSDT = "FARMUSDT" + FETBNB = "FETBNB" + FETBTC = "FETBTC" + FETBUSD = "FETBUSD" + FETTRY = "FETTRY" + FETUSDT = "FETUSDT" + FIDABNB = "FIDABNB" + FIDABTC = "FIDABTC" + FIDABUSD = "FIDABUSD" + FIDAUSDT = "FIDAUSDT" + FILBNB = "FILBNB" + FILBTC = "FILBTC" + FILBUSD = "FILBUSD" + FILETH = "FILETH" + FILTRY = "FILTRY" + FILUSDT = "FILUSDT" + FIOBNB = "FIOBNB" + FIOBTC = "FIOBTC" + FIOBUSD = "FIOBUSD" + FIOUSDT = "FIOUSDT" + FIROBTC = "FIROBTC" + FIROBUSD = "FIROBUSD" + FIROETH = "FIROETH" + FIROUSDT = "FIROUSDT" + FISBIDR = "FISBIDR" + FISBRL = "FISBRL" + FISBTC = "FISBTC" + FISBUSD = "FISBUSD" + FISTRY = "FISTRY" + FISUSDT = "FISUSDT" + FLMBNB = "FLMBNB" + FLMBTC = "FLMBTC" + FLMBUSD = "FLMBUSD" + FLMUSDT = "FLMUSDT" + FLOWBNB = "FLOWBNB" + FLOWBTC = "FLOWBTC" + FLOWBUSD = "FLOWBUSD" + FLOWUSDT = "FLOWUSDT" + FLUXBTC = "FLUXBTC" + FLUXBUSD = "FLUXBUSD" + FLUXUSDT = "FLUXUSDT" + FORBNB = "FORBNB" + FORBTC = "FORBTC" + FORBUSD = "FORBUSD" + FORTHBTC = "FORTHBTC" + FORTHBUSD = "FORTHBUSD" + FORTHUSDT = "FORTHUSDT" + FORUSDT = "FORUSDT" + FRONTBTC = "FRONTBTC" + FRONTBUSD = "FRONTBUSD" + FRONTETH = "FRONTETH" + FRONTUSDT = "FRONTUSDT" + FTMAUD = "FTMAUD" + FTMBIDR = "FTMBIDR" + FTMBNB = "FTMBNB" + FTMBRL = "FTMBRL" + FTMBTC = "FTMBTC" + FTMBUSD = "FTMBUSD" + FTMETH = "FTMETH" + FTMEUR = "FTMEUR" + FTMPAX = "FTMPAX" + FTMRUB = "FTMRUB" + FTMTRY = "FTMTRY" + FTMTUSD = "FTMTUSD" + FTMUSDC = "FTMUSDC" + FTMUSDT = "FTMUSDT" + FTTBNB = "FTTBNB" + FTTBTC = "FTTBTC" + FTTBUSD = "FTTBUSD" + FTTETH = "FTTETH" + FTTUSDT = "FTTUSDT" + FUELBTC = "FUELBTC" + FUELETH = "FUELETH" + FUNBNB = "FUNBNB" + FUNBTC = "FUNBTC" + FUNETH = "FUNETH" + FUNUSDT = "FUNUSDT" + FXSBTC = "FXSBTC" + FXSBUSD = "FXSBUSD" + FXSUSDT = "FXSUSDT" + GALAAUD = "GALAAUD" + GALABNB = "GALABNB" + GALABRL = "GALABRL" + GALABTC = "GALABTC" + GALABUSD = "GALABUSD" + GALAETH = "GALAETH" + GALAEUR = "GALAEUR" + GALATRY = "GALATRY" + GALATWD = "GALATWD" + GALAUSDT = "GALAUSDT" + GALBNB = "GALBNB" + GALBRL = "GALBRL" + GALBTC = "GALBTC" + GALBUSD = "GALBUSD" + GALETH = "GALETH" + GALEUR = "GALEUR" + GALTRY = "GALTRY" + GALUSDT = "GALUSDT" + GASBTC = "GASBTC" + GASBUSD = "GASBUSD" + GASUSDT = "GASUSDT" + GBPBUSD = "GBPBUSD" + GBPUSDT = "GBPUSDT" + GFTBUSD = "GFTBUSD" + GHSTBUSD = "GHSTBUSD" + GHSTETH = "GHSTETH" + GHSTUSDT = "GHSTUSDT" + GLMBTC = "GLMBTC" + GLMBUSD = "GLMBUSD" + GLMETH = "GLMETH" + GLMRBNB = "GLMRBNB" + GLMRBTC = "GLMRBTC" + GLMRBUSD = "GLMRBUSD" + GLMRUSDT = "GLMRUSDT" + GLMUSDT = "GLMUSDT" + GMTAUD = "GMTAUD" + GMTBNB = "GMTBNB" + GMTBRL = "GMTBRL" + GMTBTC = "GMTBTC" + GMTBUSD = "GMTBUSD" + GMTETH = "GMTETH" + GMTEUR = "GMTEUR" + GMTGBP = "GMTGBP" + GMTTRY = "GMTTRY" + GMTTWD = "GMTTWD" + GMTUSDT = "GMTUSDT" + GMXBTC = "GMXBTC" + GMXBUSD = "GMXBUSD" + GMXUSDT = "GMXUSDT" + GNOBNB = "GNOBNB" + GNOBTC = "GNOBTC" + GNOBUSD = "GNOBUSD" + GNOUSDT = "GNOUSDT" + GNSBTC = "GNSBTC" + GNSUSDT = "GNSUSDT" + GNTBNB = "GNTBNB" + GNTBTC = "GNTBTC" + GNTETH = "GNTETH" + GOBNB = "GOBNB" + GOBTC = "GOBTC" + GRSBTC = "GRSBTC" + GRSETH = "GRSETH" + GRTBTC = "GRTBTC" + GRTBUSD = "GRTBUSD" + GRTETH = "GRTETH" + GRTEUR = "GRTEUR" + GRTTRY = "GRTTRY" + GRTTWD = "GRTTWD" + GRTUSDT = "GRTUSDT" + GSTTWD = "GSTTWD" + GTCBNB = "GTCBNB" + GTCBTC = "GTCBTC" + GTCBUSD = "GTCBUSD" + GTCUSDT = "GTCUSDT" + GTOBNB = "GTOBNB" + GTOBTC = "GTOBTC" + GTOBUSD = "GTOBUSD" + GTOETH = "GTOETH" + GTOPAX = "GTOPAX" + GTOTUSD = "GTOTUSD" + GTOUSDC = "GTOUSDC" + GTOUSDT = "GTOUSDT" + GVTBTC = "GVTBTC" + GVTETH = "GVTETH" + GXSBNB = "GXSBNB" + GXSBTC = "GXSBTC" + GXSETH = "GXSETH" + GXSUSDT = "GXSUSDT" + HARDBNB = "HARDBNB" + HARDBTC = "HARDBTC" + HARDBUSD = "HARDBUSD" + HARDUSDT = "HARDUSDT" + HBARBNB = "HBARBNB" + HBARBTC = "HBARBTC" + HBARBUSD = "HBARBUSD" + HBARUSDT = "HBARUSDT" + HCBTC = "HCBTC" + HCETH = "HCETH" + HCUSDT = "HCUSDT" + HEGICBUSD = "HEGICBUSD" + HEGICETH = "HEGICETH" + HFTBTC = "HFTBTC" + HFTBUSD = "HFTBUSD" + HFTUSDT = "HFTUSDT" + HIFIETH = "HIFIETH" + HIFIUSDT = "HIFIUSDT" + HIGHBNB = "HIGHBNB" + HIGHBTC = "HIGHBTC" + HIGHBUSD = "HIGHBUSD" + HIGHUSDT = "HIGHUSDT" + HIVEBNB = "HIVEBNB" + HIVEBTC = "HIVEBTC" + HIVEBUSD = "HIVEBUSD" + HIVEUSDT = "HIVEUSDT" + HNTBTC = "HNTBTC" + HNTBUSD = "HNTBUSD" + HNTUSDT = "HNTUSDT" + HOOKBNB = "HOOKBNB" + HOOKBTC = "HOOKBTC" + HOOKBUSD = "HOOKBUSD" + HOOKUSDT = "HOOKUSDT" + HOTBNB = "HOTBNB" + HOTBRL = "HOTBRL" + HOTBTC = "HOTBTC" + HOTBUSD = "HOTBUSD" + HOTETH = "HOTETH" + HOTEUR = "HOTEUR" + HOTTRY = "HOTTRY" + HOTUSDT = "HOTUSDT" + HSRBTC = "HSRBTC" + HSRETH = "HSRETH" + ICNBTC = "ICNBTC" + ICNETH = "ICNETH" + ICPBNB = "ICPBNB" + ICPBTC = "ICPBTC" + ICPBUSD = "ICPBUSD" + ICPETH = "ICPETH" + ICPEUR = "ICPEUR" + ICPRUB = "ICPRUB" + ICPTRY = "ICPTRY" + ICPUSDT = "ICPUSDT" + ICXBNB = "ICXBNB" + ICXBTC = "ICXBTC" + ICXBUSD = "ICXBUSD" + ICXETH = "ICXETH" + ICXUSDT = "ICXUSDT" + IDBNB = "IDBNB" + IDBTC = "IDBTC" + IDEUR = "IDEUR" + IDEXBNB = "IDEXBNB" + IDEXBTC = "IDEXBTC" + IDEXBUSD = "IDEXBUSD" + IDEXUSDT = "IDEXUSDT" + IDTRY = "IDTRY" + IDTUSD = "IDTUSD" + IDUSDT = "IDUSDT" + ILVBNB = "ILVBNB" + ILVBTC = "ILVBTC" + ILVBUSD = "ILVBUSD" + ILVUSDT = "ILVUSDT" + IMXBNB = "IMXBNB" + IMXBTC = "IMXBTC" + IMXBUSD = "IMXBUSD" + IMXUSDT = "IMXUSDT" + INJBNB = "INJBNB" + INJBTC = "INJBTC" + INJBUSD = "INJBUSD" + INJTRY = "INJTRY" + INJUSDT = "INJUSDT" + INSBTC = "INSBTC" + INSETH = "INSETH" + IOSTBTC = "IOSTBTC" + IOSTBUSD = "IOSTBUSD" + IOSTETH = "IOSTETH" + IOSTUSDT = "IOSTUSDT" + IOTABNB = "IOTABNB" + IOTABTC = "IOTABTC" + IOTABUSD = "IOTABUSD" + IOTAETH = "IOTAETH" + IOTAUSDT = "IOTAUSDT" + IOTXBTC = "IOTXBTC" + IOTXBUSD = "IOTXBUSD" + IOTXETH = "IOTXETH" + IOTXUSDT = "IOTXUSDT" + IQBNB = "IQBNB" + IQBUSD = "IQBUSD" + IRISBNB = "IRISBNB" + IRISBTC = "IRISBTC" + IRISBUSD = "IRISBUSD" + IRISUSDT = "IRISUSDT" + JASMYBNB = "JASMYBNB" + JASMYBTC = "JASMYBTC" + JASMYBUSD = "JASMYBUSD" + JASMYETH = "JASMYETH" + JASMYEUR = "JASMYEUR" + JASMYTRY = "JASMYTRY" + JASMYUSDT = "JASMYUSDT" + JOEBTC = "JOEBTC" + JOEBUSD = "JOEBUSD" + JOETRY = "JOETRY" + JOEUSDT = "JOEUSDT" + JSTBNB = "JSTBNB" + JSTBTC = "JSTBTC" + JSTBUSD = "JSTBUSD" + JSTUSDT = "JSTUSDT" + JUVBTC = "JUVBTC" + JUVBUSD = "JUVBUSD" + JUVUSDT = "JUVUSDT" + KAVABNB = "KAVABNB" + KAVABTC = "KAVABTC" + KAVABUSD = "KAVABUSD" + KAVAETH = "KAVAETH" + KAVAUSDT = "KAVAUSDT" + KDABTC = "KDABTC" + KDABUSD = "KDABUSD" + KDAUSDT = "KDAUSDT" + KEEPBNB = "KEEPBNB" + KEEPBTC = "KEEPBTC" + KEEPBUSD = "KEEPBUSD" + KEEPUSDT = "KEEPUSDT" + KEYBTC = "KEYBTC" + KEYBUSD = "KEYBUSD" + KEYETH = "KEYETH" + KEYUSDT = "KEYUSDT" + KLAYBNB = "KLAYBNB" + KLAYBTC = "KLAYBTC" + KLAYBUSD = "KLAYBUSD" + KLAYUSDT = "KLAYUSDT" + KMDBTC = "KMDBTC" + KMDBUSD = "KMDBUSD" + KMDETH = "KMDETH" + KMDUSDT = "KMDUSDT" + KNCBNB = "KNCBNB" + KNCBTC = "KNCBTC" + KNCBUSD = "KNCBUSD" + KNCETH = "KNCETH" + KNCUSDT = "KNCUSDT" + KP3RBNB = "KP3RBNB" + KP3RBUSD = "KP3RBUSD" + KP3RUSDT = "KP3RUSDT" + KSMAUD = "KSMAUD" + KSMBNB = "KSMBNB" + KSMBTC = "KSMBTC" + KSMBUSD = "KSMBUSD" + KSMETH = "KSMETH" + KSMUSDT = "KSMUSDT" + LAZIOBTC = "LAZIOBTC" + LAZIOBUSD = "LAZIOBUSD" + LAZIOEUR = "LAZIOEUR" + LAZIOTRY = "LAZIOTRY" + LAZIOUSDT = "LAZIOUSDT" + LDOBTC = "LDOBTC" + LDOBUSD = "LDOBUSD" + LDOTUSD = "LDOTUSD" + LDOUSDT = "LDOUSDT" + LENDBKRW = "LENDBKRW" + LENDBTC = "LENDBTC" + LENDBUSD = "LENDBUSD" + LENDETH = "LENDETH" + LENDUSDT = "LENDUSDT" + LEVERBUSD = "LEVERBUSD" + LEVERUSDT = "LEVERUSDT" + LINABNB = "LINABNB" + LINABTC = "LINABTC" + LINABUSD = "LINABUSD" + LINAUSDT = "LINAUSDT" + LINKAUD = "LINKAUD" + LINKBKRW = "LINKBKRW" + LINKBNB = "LINKBNB" + LINKBRL = "LINKBRL" + LINKBTC = "LINKBTC" + LINKBUSD = "LINKBUSD" + LINKETH = "LINKETH" + LINKEUR = "LINKEUR" + LINKGBP = "LINKGBP" + LINKNGN = "LINKNGN" + LINKPAX = "LINKPAX" + LINKTRY = "LINKTRY" + LINKTUSD = "LINKTUSD" + LINKTWD = "LINKTWD" + LINKUSDC = "LINKUSDC" + LINKUSDT = "LINKUSDT" + LITBTC = "LITBTC" + LITBUSD = "LITBUSD" + LITETH = "LITETH" + LITUSDT = "LITUSDT" + LOKABNB = "LOKABNB" + LOKABTC = "LOKABTC" + LOKABUSD = "LOKABUSD" + LOKAUSDT = "LOKAUSDT" + LOOKSTWD = "LOOKSTWD" + LOOMBNB = "LOOMBNB" + LOOMBTC = "LOOMBTC" + LOOMBUSD = "LOOMBUSD" + LOOMETH = "LOOMETH" + LOOMUSDT = "LOOMUSDT" + LOOTTWD = "LOOTTWD" + LOOTUSDT = "LOOTUSDT" + LPTBNB = "LPTBNB" + LPTBTC = "LPTBTC" + LPTBUSD = "LPTBUSD" + LPTUSDT = "LPTUSDT" + LQTYBTC = "LQTYBTC" + LQTYUSDT = "LQTYUSDT" + LRCBNB = "LRCBNB" + LRCBTC = "LRCBTC" + LRCBUSD = "LRCBUSD" + LRCETH = "LRCETH" + LRCTRY = "LRCTRY" + LRCUSDT = "LRCUSDT" + LSKBNB = "LSKBNB" + LSKBTC = "LSKBTC" + LSKBUSD = "LSKBUSD" + LSKETH = "LSKETH" + LSKUSDT = "LSKUSDT" + LTCBNB = "LTCBNB" + LTCBRL = "LTCBRL" + LTCBTC = "LTCBTC" + LTCBUSD = "LTCBUSD" + LTCETH = "LTCETH" + LTCEUR = "LTCEUR" + LTCGBP = "LTCGBP" + LTCNGN = "LTCNGN" + LTCPAX = "LTCPAX" + LTCRUB = "LTCRUB" + LTCTUSD = "LTCTUSD" + LTCTWD = "LTCTWD" + LTCUAH = "LTCUAH" + LTCUSDC = "LTCUSDC" + LTCUSDT = "LTCUSDT" + LTOBNB = "LTOBNB" + LTOBTC = "LTOBTC" + LTOBUSD = "LTOBUSD" + LTOUSDT = "LTOUSDT" + LUNAAUD = "LUNAAUD" + LUNABIDR = "LUNABIDR" + LUNABNB = "LUNABNB" + LUNABRL = "LUNABRL" + LUNABTC = "LUNABTC" + LUNABUSD = "LUNABUSD" + LUNAETH = "LUNAETH" + LUNAEUR = "LUNAEUR" + LUNAGBP = "LUNAGBP" + LUNATRY = "LUNATRY" + LUNAUSDT = "LUNAUSDT" + LUNAUST = "LUNAUST" + LUNBTC = "LUNBTC" + LUNCBUSD = "LUNCBUSD" + LUNCUSDT = "LUNCUSDT" + LUNETH = "LUNETH" + MAGICBTC = "MAGICBTC" + MAGICBUSD = "MAGICBUSD" + MAGICTRY = "MAGICTRY" + MAGICUSDT = "MAGICUSDT" + MANABIDR = "MANABIDR" + MANABNB = "MANABNB" + MANABRL = "MANABRL" + MANABTC = "MANABTC" + MANABUSD = "MANABUSD" + MANAETH = "MANAETH" + MANATRY = "MANATRY" + MANATWD = "MANATWD" + MANAUSDT = "MANAUSDT" + MASKBNB = "MASKBNB" + MASKBUSD = "MASKBUSD" + MASKUSDT = "MASKUSDT" + MATICAUD = "MATICAUD" + MATICBIDR = "MATICBIDR" + MATICBNB = "MATICBNB" + MATICBRL = "MATICBRL" + MATICBTC = "MATICBTC" + MATICBUSD = "MATICBUSD" + MATICETH = "MATICETH" + MATICEUR = "MATICEUR" + MATICGBP = "MATICGBP" + MATICRUB = "MATICRUB" + MATICTRY = "MATICTRY" + MATICTUSD = "MATICTUSD" + MATICTWD = "MATICTWD" + MATICUSDT = "MATICUSDT" + MAXTWD = "MAXTWD" + MAXUSDT = "MAXUSDT" + MBLBNB = "MBLBNB" + MBLBTC = "MBLBTC" + MBLBUSD = "MBLBUSD" + MBLUSDT = "MBLUSDT" + MBOXBNB = "MBOXBNB" + MBOXBTC = "MBOXBTC" + MBOXBUSD = "MBOXBUSD" + MBOXTRY = "MBOXTRY" + MBOXUSDT = "MBOXUSDT" + MCBNB = "MCBNB" + MCBTC = "MCBTC" + MCBUSD = "MCBUSD" + MCOBNB = "MCOBNB" + MCOBTC = "MCOBTC" + MCOETH = "MCOETH" + MCOUSDT = "MCOUSDT" + MCUSDT = "MCUSDT" + MDABTC = "MDABTC" + MDAETH = "MDAETH" + MDTBNB = "MDTBNB" + MDTBTC = "MDTBTC" + MDTBUSD = "MDTBUSD" + MDTUSDT = "MDTUSDT" + MDXBNB = "MDXBNB" + MDXBTC = "MDXBTC" + MDXBUSD = "MDXBUSD" + MDXUSDT = "MDXUSDT" + MFTBNB = "MFTBNB" + MFTBTC = "MFTBTC" + MFTETH = "MFTETH" + MFTUSDT = "MFTUSDT" + MINABNB = "MINABNB" + MINABTC = "MINABTC" + MINABUSD = "MINABUSD" + MINATRY = "MINATRY" + MINAUSDT = "MINAUSDT" + MIRBTC = "MIRBTC" + MIRBUSD = "MIRBUSD" + MIRUSDT = "MIRUSDT" + MITHBNB = "MITHBNB" + MITHBTC = "MITHBTC" + MITHUSDT = "MITHUSDT" + MKRBNB = "MKRBNB" + MKRBTC = "MKRBTC" + MKRBUSD = "MKRBUSD" + MKRUSDT = "MKRUSDT" + MLNBNB = "MLNBNB" + MLNBTC = "MLNBTC" + MLNBUSD = "MLNBUSD" + MLNUSDT = "MLNUSDT" + MOBBTC = "MOBBTC" + MOBBUSD = "MOBBUSD" + MOBUSDT = "MOBUSDT" + MODBTC = "MODBTC" + MODETH = "MODETH" + MOVRBNB = "MOVRBNB" + MOVRBTC = "MOVRBTC" + MOVRBUSD = "MOVRBUSD" + MOVRUSDT = "MOVRUSDT" + MTHBTC = "MTHBTC" + MTHETH = "MTHETH" + MTLBTC = "MTLBTC" + MTLBUSD = "MTLBUSD" + MTLETH = "MTLETH" + MTLUSDT = "MTLUSDT" + MULTIBTC = "MULTIBTC" + MULTIBUSD = "MULTIBUSD" + MULTIUSDT = "MULTIUSDT" + NANOBNB = "NANOBNB" + NANOBTC = "NANOBTC" + NANOBUSD = "NANOBUSD" + NANOETH = "NANOETH" + NANOUSDT = "NANOUSDT" + NASBNB = "NASBNB" + NASBTC = "NASBTC" + NASETH = "NASETH" + NAVBNB = "NAVBNB" + NAVBTC = "NAVBTC" + NAVETH = "NAVETH" + NBSBTC = "NBSBTC" + NBSUSDT = "NBSUSDT" + NCASHBNB = "NCASHBNB" + NCASHBTC = "NCASHBTC" + NCASHETH = "NCASHETH" + NEARBNB = "NEARBNB" + NEARBTC = "NEARBTC" + NEARBUSD = "NEARBUSD" + NEARETH = "NEARETH" + NEAREUR = "NEAREUR" + NEARRUB = "NEARRUB" + NEARTRY = "NEARTRY" + NEARUSDT = "NEARUSDT" + NEBLBNB = "NEBLBNB" + NEBLBTC = "NEBLBTC" + NEBLBUSD = "NEBLBUSD" + NEBLUSDT = "NEBLUSDT" + NEOBNB = "NEOBNB" + NEOBTC = "NEOBTC" + NEOBUSD = "NEOBUSD" + NEOETH = "NEOETH" + NEOPAX = "NEOPAX" + NEORUB = "NEORUB" + NEOTRY = "NEOTRY" + NEOTUSD = "NEOTUSD" + NEOUSDC = "NEOUSDC" + NEOUSDT = "NEOUSDT" + NEXOBTC = "NEXOBTC" + NEXOBUSD = "NEXOBUSD" + NEXOUSDT = "NEXOUSDT" + NKNBNB = "NKNBNB" + NKNBTC = "NKNBTC" + NKNBUSD = "NKNBUSD" + NKNUSDT = "NKNUSDT" + NMRBTC = "NMRBTC" + NMRBUSD = "NMRBUSD" + NMRUSDT = "NMRUSDT" + NPXSBTC = "NPXSBTC" + NPXSETH = "NPXSETH" + NPXSUSDC = "NPXSUSDC" + NPXSUSDT = "NPXSUSDT" + NUAUD = "NUAUD" + NUBNB = "NUBNB" + NUBTC = "NUBTC" + NUBUSD = "NUBUSD" + NULSBNB = "NULSBNB" + NULSBTC = "NULSBTC" + NULSBUSD = "NULSBUSD" + NULSETH = "NULSETH" + NULSUSDT = "NULSUSDT" + NURUB = "NURUB" + NUUSDT = "NUUSDT" + NXSBNB = "NXSBNB" + NXSBTC = "NXSBTC" + NXSETH = "NXSETH" + OAXBTC = "OAXBTC" + OAXETH = "OAXETH" + OAXUSDT = "OAXUSDT" + OCEANBNB = "OCEANBNB" + OCEANBTC = "OCEANBTC" + OCEANBUSD = "OCEANBUSD" + OCEANUSDT = "OCEANUSDT" + OGBTC = "OGBTC" + OGBUSD = "OGBUSD" + OGNBNB = "OGNBNB" + OGNBTC = "OGNBTC" + OGNBUSD = "OGNBUSD" + OGNUSDT = "OGNUSDT" + OGUSDT = "OGUSDT" + OMBTC = "OMBTC" + OMBUSD = "OMBUSD" + OMGBNB = "OMGBNB" + OMGBTC = "OMGBTC" + OMGBUSD = "OMGBUSD" + OMGETH = "OMGETH" + OMGUSDT = "OMGUSDT" + OMUSDT = "OMUSDT" + ONEBIDR = "ONEBIDR" + ONEBNB = "ONEBNB" + ONEBTC = "ONEBTC" + ONEBUSD = "ONEBUSD" + ONEETH = "ONEETH" + ONEPAX = "ONEPAX" + ONETRY = "ONETRY" + ONETUSD = "ONETUSD" + ONEUSDC = "ONEUSDC" + ONEUSDT = "ONEUSDT" + ONGBNB = "ONGBNB" + ONGBTC = "ONGBTC" + ONGUSDT = "ONGUSDT" + ONTBNB = "ONTBNB" + ONTBTC = "ONTBTC" + ONTBUSD = "ONTBUSD" + ONTETH = "ONTETH" + ONTPAX = "ONTPAX" + ONTTRY = "ONTTRY" + ONTUSDC = "ONTUSDC" + ONTUSDT = "ONTUSDT" + OOKIBNB = "OOKIBNB" + OOKIBUSD = "OOKIBUSD" + OOKIETH = "OOKIETH" + OOKIUSDT = "OOKIUSDT" + OPBNB = "OPBNB" + OPBTC = "OPBTC" + OPBUSD = "OPBUSD" + OPETH = "OPETH" + OPEUR = "OPEUR" + OPTUSD = "OPTUSD" + OPUSDT = "OPUSDT" + ORNBTC = "ORNBTC" + ORNBUSD = "ORNBUSD" + ORNUSDT = "ORNUSDT" + OSMOBTC = "OSMOBTC" + OSMOBUSD = "OSMOBUSD" + OSMOUSDT = "OSMOUSDT" + OSTBNB = "OSTBNB" + OSTBTC = "OSTBTC" + OSTETH = "OSTETH" + OXTBTC = "OXTBTC" + OXTBUSD = "OXTBUSD" + OXTUSDT = "OXTUSDT" + PAXBNB = "PAXBNB" + PAXBTC = "PAXBTC" + PAXBUSD = "PAXBUSD" + PAXETH = "PAXETH" + PAXGBNB = "PAXGBNB" + PAXGBTC = "PAXGBTC" + PAXGBUSD = "PAXGBUSD" + PAXGUSDT = "PAXGUSDT" + PAXTUSD = "PAXTUSD" + PAXUSDT = "PAXUSDT" + PEOPLEBNB = "PEOPLEBNB" + PEOPLEBTC = "PEOPLEBTC" + PEOPLEBUSD = "PEOPLEBUSD" + PEOPLEETH = "PEOPLEETH" + PEOPLEUSDT = "PEOPLEUSDT" + PERLBNB = "PERLBNB" + PERLBTC = "PERLBTC" + PERLUSDC = "PERLUSDC" + PERLUSDT = "PERLUSDT" + PERPBTC = "PERPBTC" + PERPBUSD = "PERPBUSD" + PERPUSDT = "PERPUSDT" + PHABTC = "PHABTC" + PHABUSD = "PHABUSD" + PHAUSDT = "PHAUSDT" + PHBBNB = "PHBBNB" + PHBBTC = "PHBBTC" + PHBBUSD = "PHBBUSD" + PHBPAX = "PHBPAX" + PHBTUSD = "PHBTUSD" + PHBUSDC = "PHBUSDC" + PHBUSDT = "PHBUSDT" + PHXBNB = "PHXBNB" + PHXBTC = "PHXBTC" + PHXETH = "PHXETH" + PIVXBNB = "PIVXBNB" + PIVXBTC = "PIVXBTC" + PLABNB = "PLABNB" + PLABTC = "PLABTC" + PLABUSD = "PLABUSD" + PLAUSDT = "PLAUSDT" + PNTBTC = "PNTBTC" + PNTUSDT = "PNTUSDT" + POABNB = "POABNB" + POABTC = "POABTC" + POAETH = "POAETH" + POEBTC = "POEBTC" + POEETH = "POEETH" + POLSBNB = "POLSBNB" + POLSBTC = "POLSBTC" + POLSBUSD = "POLSBUSD" + POLSUSDT = "POLSUSDT" + POLYBNB = "POLYBNB" + POLYBTC = "POLYBTC" + POLYBUSD = "POLYBUSD" + POLYUSDT = "POLYUSDT" + POLYXBTC = "POLYXBTC" + POLYXBUSD = "POLYXBUSD" + POLYXUSDT = "POLYXUSDT" + PONDBTC = "PONDBTC" + PONDBUSD = "PONDBUSD" + PONDUSDT = "PONDUSDT" + PORTOBTC = "PORTOBTC" + PORTOBUSD = "PORTOBUSD" + PORTOEUR = "PORTOEUR" + PORTOTRY = "PORTOTRY" + PORTOUSDT = "PORTOUSDT" + POWRBNB = "POWRBNB" + POWRBTC = "POWRBTC" + POWRBUSD = "POWRBUSD" + POWRETH = "POWRETH" + POWRUSDT = "POWRUSDT" + PPTBTC = "PPTBTC" + PPTETH = "PPTETH" + PROMBNB = "PROMBNB" + PROMBTC = "PROMBTC" + PROMBUSD = "PROMBUSD" + PROMUSDT = "PROMUSDT" + PROSBUSD = "PROSBUSD" + PROSETH = "PROSETH" + PROSUSDT = "PROSUSDT" + PSGBTC = "PSGBTC" + PSGBUSD = "PSGBUSD" + PSGUSDT = "PSGUSDT" + PUNDIXBUSD = "PUNDIXBUSD" + PUNDIXETH = "PUNDIXETH" + PUNDIXUSDT = "PUNDIXUSDT" + PYRBTC = "PYRBTC" + PYRBUSD = "PYRBUSD" + PYRUSDT = "PYRUSDT" + QIBNB = "QIBNB" + QIBTC = "QIBTC" + QIBUSD = "QIBUSD" + QIUSDT = "QIUSDT" + QKCBTC = "QKCBTC" + QKCBUSD = "QKCBUSD" + QKCETH = "QKCETH" + QKCUSDT = "QKCUSDT" + QLCBNB = "QLCBNB" + QLCBTC = "QLCBTC" + QLCETH = "QLCETH" + QNTBNB = "QNTBNB" + QNTBTC = "QNTBTC" + QNTBUSD = "QNTBUSD" + QNTUSDT = "QNTUSDT" + QSPBNB = "QSPBNB" + QSPBTC = "QSPBTC" + QSPETH = "QSPETH" + QTUMBNB = "QTUMBNB" + QTUMBTC = "QTUMBTC" + QTUMBUSD = "QTUMBUSD" + QTUMETH = "QTUMETH" + QTUMUSDT = "QTUMUSDT" + QUICKBNB = "QUICKBNB" + QUICKBTC = "QUICKBTC" + QUICKBUSD = "QUICKBUSD" + QUICKUSDT = "QUICKUSDT" + RADBNB = "RADBNB" + RADBTC = "RADBTC" + RADBUSD = "RADBUSD" + RADUSDT = "RADUSDT" + RAMPBTC = "RAMPBTC" + RAMPBUSD = "RAMPBUSD" + RAMPUSDT = "RAMPUSDT" + RAREBNB = "RAREBNB" + RAREBTC = "RAREBTC" + RAREBUSD = "RAREBUSD" + RAREUSDT = "RAREUSDT" + RAYBNB = "RAYBNB" + RAYBUSD = "RAYBUSD" + RAYUSDT = "RAYUSDT" + RCNBNB = "RCNBNB" + RCNBTC = "RCNBTC" + RCNETH = "RCNETH" + RDNBNB = "RDNBNB" + RDNBTC = "RDNBTC" + RDNETH = "RDNETH" + RDNTBTC = "RDNTBTC" + RDNTTUSD = "RDNTTUSD" + RDNTUSDT = "RDNTUSDT" + REEFBIDR = "REEFBIDR" + REEFBTC = "REEFBTC" + REEFBUSD = "REEFBUSD" + REEFTRY = "REEFTRY" + REEFUSDT = "REEFUSDT" + REIBNB = "REIBNB" + REIBUSD = "REIBUSD" + REIETH = "REIETH" + REIUSDT = "REIUSDT" + RENBNB = "RENBNB" + RENBTC = "RENBTC" + RENBTCBTC = "RENBTCBTC" + RENBTCETH = "RENBTCETH" + RENBUSD = "RENBUSD" + RENUSDT = "RENUSDT" + REPBNB = "REPBNB" + REPBTC = "REPBTC" + REPBUSD = "REPBUSD" + REPUSDT = "REPUSDT" + REQBTC = "REQBTC" + REQBUSD = "REQBUSD" + REQETH = "REQETH" + REQUSDT = "REQUSDT" + RGTBNB = "RGTBNB" + RGTBTC = "RGTBTC" + RGTBUSD = "RGTBUSD" + RGTUSDT = "RGTUSDT" + RIFBTC = "RIFBTC" + RIFUSDT = "RIFUSDT" + RLCBNB = "RLCBNB" + RLCBTC = "RLCBTC" + RLCBUSD = "RLCBUSD" + RLCETH = "RLCETH" + RLCUSDT = "RLCUSDT" + RLYTWD = "RLYTWD" + RLYUSDT = "RLYUSDT" + RNDRBTC = "RNDRBTC" + RNDRBUSD = "RNDRBUSD" + RNDRUSDT = "RNDRUSDT" + ROSEBNB = "ROSEBNB" + ROSEBTC = "ROSEBTC" + ROSEBUSD = "ROSEBUSD" + ROSEETH = "ROSEETH" + ROSETRY = "ROSETRY" + ROSEUSDT = "ROSEUSDT" + RPLBTC = "RPLBTC" + RPLBUSD = "RPLBUSD" + RPLUSDT = "RPLUSDT" + RPXBNB = "RPXBNB" + RPXBTC = "RPXBTC" + RPXETH = "RPXETH" + RSRBNB = "RSRBNB" + RSRBTC = "RSRBTC" + RSRBUSD = "RSRBUSD" + RSRUSDT = "RSRUSDT" + RUNEAUD = "RUNEAUD" + RUNEBNB = "RUNEBNB" + RUNEBTC = "RUNEBTC" + RUNEBUSD = "RUNEBUSD" + RUNEETH = "RUNEETH" + RUNEEUR = "RUNEEUR" + RUNEGBP = "RUNEGBP" + RUNETRY = "RUNETRY" + RUNEUSDT = "RUNEUSDT" + RVNBTC = "RVNBTC" + RVNBUSD = "RVNBUSD" + RVNTRY = "RVNTRY" + RVNUSDT = "RVNUSDT" + SALTBTC = "SALTBTC" + SALTETH = "SALTETH" + SANDAUD = "SANDAUD" + SANDBIDR = "SANDBIDR" + SANDBNB = "SANDBNB" + SANDBRL = "SANDBRL" + SANDBTC = "SANDBTC" + SANDBUSD = "SANDBUSD" + SANDETH = "SANDETH" + SANDTRY = "SANDTRY" + SANDTWD = "SANDTWD" + SANDUSDT = "SANDUSDT" + SANTOSBRL = "SANTOSBRL" + SANTOSBTC = "SANTOSBTC" + SANTOSBUSD = "SANTOSBUSD" + SANTOSTRY = "SANTOSTRY" + SANTOSUSDT = "SANTOSUSDT" + SCBTC = "SCBTC" + SCBUSD = "SCBUSD" + SCETH = "SCETH" + SCRTBTC = "SCRTBTC" + SCRTBUSD = "SCRTBUSD" + SCRTETH = "SCRTETH" + SCRTUSDT = "SCRTUSDT" + SCUSDT = "SCUSDT" + SFPBTC = "SFPBTC" + SFPBUSD = "SFPBUSD" + SFPUSDT = "SFPUSDT" + SHIBAUD = "SHIBAUD" + SHIBBRL = "SHIBBRL" + SHIBBUSD = "SHIBBUSD" + SHIBDOGE = "SHIBDOGE" + SHIBEUR = "SHIBEUR" + SHIBGBP = "SHIBGBP" + SHIBRUB = "SHIBRUB" + SHIBTRY = "SHIBTRY" + SHIBTWD = "SHIBTWD" + SHIBUAH = "SHIBUAH" + SHIBUSDT = "SHIBUSDT" + SKLBTC = "SKLBTC" + SKLBUSD = "SKLBUSD" + SKLUSDT = "SKLUSDT" + SKYBNB = "SKYBNB" + SKYBTC = "SKYBTC" + SKYETH = "SKYETH" + SLPBIDR = "SLPBIDR" + SLPBNB = "SLPBNB" + SLPBUSD = "SLPBUSD" + SLPETH = "SLPETH" + SLPTRY = "SLPTRY" + SLPUSDT = "SLPUSDT" + SNGLSBTC = "SNGLSBTC" + SNGLSETH = "SNGLSETH" + SNMBTC = "SNMBTC" + SNMBUSD = "SNMBUSD" + SNMETH = "SNMETH" + SNTBTC = "SNTBTC" + SNTBUSD = "SNTBUSD" + SNTETH = "SNTETH" + SNXBNB = "SNXBNB" + SNXBTC = "SNXBTC" + SNXBUSD = "SNXBUSD" + SNXETH = "SNXETH" + SNXUSDT = "SNXUSDT" + SOLAUD = "SOLAUD" + SOLBIDR = "SOLBIDR" + SOLBNB = "SOLBNB" + SOLBRL = "SOLBRL" + SOLBTC = "SOLBTC" + SOLBUSD = "SOLBUSD" + SOLETH = "SOLETH" + SOLEUR = "SOLEUR" + SOLGBP = "SOLGBP" + SOLRUB = "SOLRUB" + SOLTRY = "SOLTRY" + SOLTUSD = "SOLTUSD" + SOLTWD = "SOLTWD" + SOLUSDC = "SOLUSDC" + SOLUSDT = "SOLUSDT" + SPARTABNB = "SPARTABNB" + SPELLBNB = "SPELLBNB" + SPELLBTC = "SPELLBTC" + SPELLBUSD = "SPELLBUSD" + SPELLTRY = "SPELLTRY" + SPELLUSDT = "SPELLUSDT" + SRMBIDR = "SRMBIDR" + SRMBNB = "SRMBNB" + SRMBTC = "SRMBTC" + SRMBUSD = "SRMBUSD" + SRMUSDT = "SRMUSDT" + SSVBTC = "SSVBTC" + SSVBUSD = "SSVBUSD" + SSVETH = "SSVETH" + SSVTUSD = "SSVTUSD" + SSVUSDT = "SSVUSDT" + STEEMBNB = "STEEMBNB" + STEEMBTC = "STEEMBTC" + STEEMBUSD = "STEEMBUSD" + STEEMETH = "STEEMETH" + STEEMUSDT = "STEEMUSDT" + STGBTC = "STGBTC" + STGBUSD = "STGBUSD" + STGUSDT = "STGUSDT" + STMXBTC = "STMXBTC" + STMXBUSD = "STMXBUSD" + STMXETH = "STMXETH" + STMXUSDT = "STMXUSDT" + STORJBTC = "STORJBTC" + STORJBUSD = "STORJBUSD" + STORJETH = "STORJETH" + STORJTRY = "STORJTRY" + STORJUSDT = "STORJUSDT" + STORMBNB = "STORMBNB" + STORMBTC = "STORMBTC" + STORMETH = "STORMETH" + STORMUSDT = "STORMUSDT" + STPTBNB = "STPTBNB" + STPTBTC = "STPTBTC" + STPTBUSD = "STPTBUSD" + STPTUSDT = "STPTUSDT" + STRATBNB = "STRATBNB" + STRATBTC = "STRATBTC" + STRATBUSD = "STRATBUSD" + STRATETH = "STRATETH" + STRATUSDT = "STRATUSDT" + STRAXBTC = "STRAXBTC" + STRAXBUSD = "STRAXBUSD" + STRAXETH = "STRAXETH" + STRAXUSDT = "STRAXUSDT" + STXBNB = "STXBNB" + STXBTC = "STXBTC" + STXBUSD = "STXBUSD" + STXTRY = "STXTRY" + STXUSDT = "STXUSDT" + SUBBTC = "SUBBTC" + SUBETH = "SUBETH" + SUNBTC = "SUNBTC" + SUNBUSD = "SUNBUSD" + SUNUSDT = "SUNUSDT" + SUPERBTC = "SUPERBTC" + SUPERBUSD = "SUPERBUSD" + SUPERUSDT = "SUPERUSDT" + SUSDBTC = "SUSDBTC" + SUSDETH = "SUSDETH" + SUSDUSDT = "SUSDUSDT" + SUSHIBNB = "SUSHIBNB" + SUSHIBTC = "SUSHIBTC" + SUSHIBUSD = "SUSHIBUSD" + SUSHIUSDT = "SUSHIUSDT" + SWRVBNB = "SWRVBNB" + SWRVBUSD = "SWRVBUSD" + SXPAUD = "SXPAUD" + SXPBIDR = "SXPBIDR" + SXPBNB = "SXPBNB" + SXPBTC = "SXPBTC" + SXPBUSD = "SXPBUSD" + SXPEUR = "SXPEUR" + SXPGBP = "SXPGBP" + SXPTRY = "SXPTRY" + SXPUSDT = "SXPUSDT" + SYNBTC = "SYNBTC" + SYNUSDT = "SYNUSDT" + SYSBNB = "SYSBNB" + SYSBTC = "SYSBTC" + SYSBUSD = "SYSBUSD" + SYSETH = "SYSETH" + SYSUSDT = "SYSUSDT" + TBUSD = "TBUSD" + TCTBNB = "TCTBNB" + TCTBTC = "TCTBTC" + TCTUSDT = "TCTUSDT" + TFUELBNB = "TFUELBNB" + TFUELBTC = "TFUELBTC" + TFUELBUSD = "TFUELBUSD" + TFUELPAX = "TFUELPAX" + TFUELTUSD = "TFUELTUSD" + TFUELUSDC = "TFUELUSDC" + TFUELUSDT = "TFUELUSDT" + THETABNB = "THETABNB" + THETABTC = "THETABTC" + THETABUSD = "THETABUSD" + THETAETH = "THETAETH" + THETAEUR = "THETAEUR" + THETAUSDT = "THETAUSDT" + TKOBIDR = "TKOBIDR" + TKOBTC = "TKOBTC" + TKOBUSD = "TKOBUSD" + TKOUSDT = "TKOUSDT" + TLMBNB = "TLMBNB" + TLMBTC = "TLMBTC" + TLMBUSD = "TLMBUSD" + TLMTRY = "TLMTRY" + TLMUSDT = "TLMUSDT" + TNBBTC = "TNBBTC" + TNBETH = "TNBETH" + TNTBTC = "TNTBTC" + TNTETH = "TNTETH" + TOMOBNB = "TOMOBNB" + TOMOBTC = "TOMOBTC" + TOMOBUSD = "TOMOBUSD" + TOMOUSDC = "TOMOUSDC" + TOMOUSDT = "TOMOUSDT" + TORNBNB = "TORNBNB" + TORNBTC = "TORNBTC" + TORNBUSD = "TORNBUSD" + TORNUSDT = "TORNUSDT" + TRBBNB = "TRBBNB" + TRBBTC = "TRBBTC" + TRBBUSD = "TRBBUSD" + TRBUSDT = "TRBUSDT" + TRIBEBNB = "TRIBEBNB" + TRIBEBTC = "TRIBEBTC" + TRIBEBUSD = "TRIBEBUSD" + TRIBEUSDT = "TRIBEUSDT" + TRIGBNB = "TRIGBNB" + TRIGBTC = "TRIGBTC" + TRIGETH = "TRIGETH" + TROYBNB = "TROYBNB" + TROYBTC = "TROYBTC" + TROYBUSD = "TROYBUSD" + TROYUSDT = "TROYUSDT" + TRUBTC = "TRUBTC" + TRUBUSD = "TRUBUSD" + TRURUB = "TRURUB" + TRUUSDT = "TRUUSDT" + TRXAUD = "TRXAUD" + TRXBNB = "TRXBNB" + TRXBTC = "TRXBTC" + TRXBUSD = "TRXBUSD" + TRXETH = "TRXETH" + TRXEUR = "TRXEUR" + TRXNGN = "TRXNGN" + TRXPAX = "TRXPAX" + TRXTRY = "TRXTRY" + TRXTUSD = "TRXTUSD" + TRXUSDC = "TRXUSDC" + TRXUSDT = "TRXUSDT" + TRXXRP = "TRXXRP" + TUSDBNB = "TUSDBNB" + TUSDBTC = "TUSDBTC" + TUSDBTUSD = "TUSDBTUSD" + TUSDBUSD = "TUSDBUSD" + TUSDETH = "TUSDETH" + TUSDT = "TUSDT" + TUSDUSDT = "TUSDUSDT" + TVKBTC = "TVKBTC" + TVKBUSD = "TVKBUSD" + TVKUSDT = "TVKUSDT" + TWTBTC = "TWTBTC" + TWTBUSD = "TWTBUSD" + TWTTRY = "TWTTRY" + TWTUSDT = "TWTUSDT" + UFTBUSD = "UFTBUSD" + UFTETH = "UFTETH" + UFTUSDT = "UFTUSDT" + UMABTC = "UMABTC" + UMABUSD = "UMABUSD" + UMATRY = "UMATRY" + UMAUSDT = "UMAUSDT" + UNFIBNB = "UNFIBNB" + UNFIBTC = "UNFIBTC" + UNFIBUSD = "UNFIBUSD" + UNFIETH = "UNFIETH" + UNFIUSDT = "UNFIUSDT" + UNIAUD = "UNIAUD" + UNIBNB = "UNIBNB" + UNIBTC = "UNIBTC" + UNIBUSD = "UNIBUSD" + UNIETH = "UNIETH" + UNIEUR = "UNIEUR" + UNIUSDT = "UNIUSDT" + USDCBNB = "USDCBNB" + USDCBUSD = "USDCBUSD" + USDCPAX = "USDCPAX" + USDCTUSD = "USDCTUSD" + USDCTWD = "USDCTWD" + USDCUSDT = "USDCUSDT" + USDPBUSD = "USDPBUSD" + USDPUSDT = "USDPUSDT" + USDSBUSDS = "USDSBUSDS" + USDSBUSDT = "USDSBUSDT" + USDSPAX = "USDSPAX" + USDSTUSD = "USDSTUSD" + USDSUSDC = "USDSUSDC" + USDSUSDT = "USDSUSDT" + USDTBIDR = "USDTBIDR" + USDTBKRW = "USDTBKRW" + USDTBRL = "USDTBRL" + USDTBVND = "USDTBVND" + USDTDAI = "USDTDAI" + USDTIDRT = "USDTIDRT" + USDTNGN = "USDTNGN" + USDTPLN = "USDTPLN" + USDTRON = "USDTRON" + USDTRUB = "USDTRUB" + USDTTRY = "USDTTRY" + USDTTWD = "USDTTWD" + USDTUAH = "USDTUAH" + USDTZAR = "USDTZAR" + USTBTC = "USTBTC" + USTBUSD = "USTBUSD" + USTCBUSD = "USTCBUSD" + USTCUSDT = "USTCUSDT" + USTUSDT = "USTUSDT" + UTKBTC = "UTKBTC" + UTKBUSD = "UTKBUSD" + UTKUSDT = "UTKUSDT" + VENBNB = "VENBNB" + VENBTC = "VENBTC" + VENETH = "VENETH" + VENUSDT = "VENUSDT" + VETBNB = "VETBNB" + VETBTC = "VETBTC" + VETBUSD = "VETBUSD" + VETETH = "VETETH" + VETEUR = "VETEUR" + VETGBP = "VETGBP" + VETTRY = "VETTRY" + VETUSDT = "VETUSDT" + VGXBTC = "VGXBTC" + VGXETH = "VGXETH" + VGXUSDT = "VGXUSDT" + VIABNB = "VIABNB" + VIABTC = "VIABTC" + VIAETH = "VIAETH" + VIBBTC = "VIBBTC" + VIBBUSD = "VIBBUSD" + VIBEBTC = "VIBEBTC" + VIBEETH = "VIBEETH" + VIBETH = "VIBETH" + VIBUSDT = "VIBUSDT" + VIDTBTC = "VIDTBTC" + VIDTBUSD = "VIDTBUSD" + VIDTUSDT = "VIDTUSDT" + VITEBNB = "VITEBNB" + VITEBTC = "VITEBTC" + VITEBUSD = "VITEBUSD" + VITEUSDT = "VITEUSDT" + VOXELBNB = "VOXELBNB" + VOXELBTC = "VOXELBTC" + VOXELBUSD = "VOXELBUSD" + VOXELETH = "VOXELETH" + VOXELUSDT = "VOXELUSDT" + VTHOBNB = "VTHOBNB" + VTHOBUSD = "VTHOBUSD" + VTHOUSDT = "VTHOUSDT" + WABIBNB = "WABIBNB" + WABIBTC = "WABIBTC" + WABIETH = "WABIETH" + WANBNB = "WANBNB" + WANBTC = "WANBTC" + WANETH = "WANETH" + WANUSDT = "WANUSDT" + WAVESBNB = "WAVESBNB" + WAVESBTC = "WAVESBTC" + WAVESBUSD = "WAVESBUSD" + WAVESETH = "WAVESETH" + WAVESEUR = "WAVESEUR" + WAVESPAX = "WAVESPAX" + WAVESRUB = "WAVESRUB" + WAVESTRY = "WAVESTRY" + WAVESTUSD = "WAVESTUSD" + WAVESUSDC = "WAVESUSDC" + WAVESUSDT = "WAVESUSDT" + WAXPBNB = "WAXPBNB" + WAXPBTC = "WAXPBTC" + WAXPBUSD = "WAXPBUSD" + WAXPUSDT = "WAXPUSDT" + WBTCBTC = "WBTCBTC" + WBTCBUSD = "WBTCBUSD" + WBTCETH = "WBTCETH" + WINBNB = "WINBNB" + WINBRL = "WINBRL" + WINBTC = "WINBTC" + WINBUSD = "WINBUSD" + WINEUR = "WINEUR" + WINGBNB = "WINGBNB" + WINGBTC = "WINGBTC" + WINGBUSD = "WINGBUSD" + WINGETH = "WINGETH" + WINGSBTC = "WINGSBTC" + WINGSETH = "WINGSETH" + WINGUSDT = "WINGUSDT" + WINTRX = "WINTRX" + WINUSDC = "WINUSDC" + WINUSDT = "WINUSDT" + WNXMBNB = "WNXMBNB" + WNXMBTC = "WNXMBTC" + WNXMBUSD = "WNXMBUSD" + WNXMUSDT = "WNXMUSDT" + WOOBNB = "WOOBNB" + WOOBTC = "WOOBTC" + WOOBUSD = "WOOBUSD" + WOOUSDT = "WOOUSDT" + WPRBTC = "WPRBTC" + WPRETH = "WPRETH" + WRXBNB = "WRXBNB" + WRXBTC = "WRXBTC" + WRXBUSD = "WRXBUSD" + WRXEUR = "WRXEUR" + WRXUSDT = "WRXUSDT" + WTCBNB = "WTCBNB" + WTCBTC = "WTCBTC" + WTCETH = "WTCETH" + WTCUSDT = "WTCUSDT" + XECBUSD = "XECBUSD" + XECUSDT = "XECUSDT" + XEMBNB = "XEMBNB" + XEMBTC = "XEMBTC" + XEMBUSD = "XEMBUSD" + XEMETH = "XEMETH" + XEMUSDT = "XEMUSDT" + XLMBNB = "XLMBNB" + XLMBTC = "XLMBTC" + XLMBUSD = "XLMBUSD" + XLMETH = "XLMETH" + XLMEUR = "XLMEUR" + XLMPAX = "XLMPAX" + XLMTRY = "XLMTRY" + XLMTUSD = "XLMTUSD" + XLMUSDC = "XLMUSDC" + XLMUSDT = "XLMUSDT" + XMRBNB = "XMRBNB" + XMRBTC = "XMRBTC" + XMRBUSD = "XMRBUSD" + XMRETH = "XMRETH" + XMRUSDT = "XMRUSDT" + XNOBTC = "XNOBTC" + XNOBUSD = "XNOBUSD" + XNOETH = "XNOETH" + XNOUSDT = "XNOUSDT" + XRPAUD = "XRPAUD" + XRPBEARBUSD = "XRPBEARBUSD" + XRPBEARUSDT = "XRPBEARUSDT" + XRPBIDR = "XRPBIDR" + XRPBKRW = "XRPBKRW" + XRPBNB = "XRPBNB" + XRPBRL = "XRPBRL" + XRPBTC = "XRPBTC" + XRPBULLBUSD = "XRPBULLBUSD" + XRPBULLUSDT = "XRPBULLUSDT" + XRPBUSD = "XRPBUSD" + XRPETH = "XRPETH" + XRPEUR = "XRPEUR" + XRPGBP = "XRPGBP" + XRPNGN = "XRPNGN" + XRPPAX = "XRPPAX" + XRPRUB = "XRPRUB" + XRPTRY = "XRPTRY" + XRPTUSD = "XRPTUSD" + XRPTWD = "XRPTWD" + XRPUSDC = "XRPUSDC" + XRPUSDT = "XRPUSDT" + XTZBNB = "XTZBNB" + XTZBTC = "XTZBTC" + XTZBUSD = "XTZBUSD" + XTZETH = "XTZETH" + XTZTRY = "XTZTRY" + XTZTWD = "XTZTWD" + XTZUSDT = "XTZUSDT" + XVGBTC = "XVGBTC" + XVGBUSD = "XVGBUSD" + XVGETH = "XVGETH" + XVGUSDT = "XVGUSDT" + XVSBNB = "XVSBNB" + XVSBTC = "XVSBTC" + XVSBUSD = "XVSBUSD" + XVSTRY = "XVSTRY" + XVSUSDT = "XVSUSDT" + XZCBNB = "XZCBNB" + XZCBTC = "XZCBTC" + XZCETH = "XZCETH" + XZCUSDT = "XZCUSDT" + XZCXRP = "XZCXRP" + YFIBNB = "YFIBNB" + YFIBTC = "YFIBTC" + YFIBUSD = "YFIBUSD" + YFIEUR = "YFIEUR" + YFIIBNB = "YFIIBNB" + YFIIBTC = "YFIIBTC" + YFIIBUSD = "YFIIBUSD" + YFIIUSDT = "YFIIUSDT" + YFITWD = "YFITWD" + YFIUSDT = "YFIUSDT" + YGGBNB = "YGGBNB" + YGGBTC = "YGGBTC" + YGGBUSD = "YGGBUSD" + YGGUSDT = "YGGUSDT" + YOYOBNB = "YOYOBNB" + YOYOBTC = "YOYOBTC" + YOYOETH = "YOYOETH" + ZECBNB = "ZECBNB" + ZECBTC = "ZECBTC" + ZECBUSD = "ZECBUSD" + ZECETH = "ZECETH" + ZECPAX = "ZECPAX" + ZECTUSD = "ZECTUSD" + ZECUSDC = "ZECUSDC" + ZECUSDT = "ZECUSDT" + ZENBNB = "ZENBNB" + ZENBTC = "ZENBTC" + ZENBUSD = "ZENBUSD" + ZENETH = "ZENETH" + ZENUSDT = "ZENUSDT" + ZILBIDR = "ZILBIDR" + ZILBNB = "ZILBNB" + ZILBTC = "ZILBTC" + ZILBUSD = "ZILBUSD" + ZILETH = "ZILETH" + ZILEUR = "ZILEUR" + ZILTRY = "ZILTRY" + ZILUSDT = "ZILUSDT" + ZRXBNB = "ZRXBNB" + ZRXBTC = "ZRXBTC" + ZRXBUSD = "ZRXBUSD" + ZRXETH = "ZRXETH" + ZRXUSDT = "ZRXUSDT" +) +var symbols = []string{ + AAVEBKRW,AAVEBNB,AAVEBRL,AAVEBTC,AAVEBUSD,AAVEETH,AAVEUSDT,ACABTC,ACABUSD,ACAUSDT,ACHBTC,ACHBUSD,ACHTRY,ACHUSDT,ACMBTC,ACMBUSD,ACMUSDT,ADAAUD,ADABIDR,ADABKRW,ADABNB,ADABRL,ADABTC,ADABUSD,ADAETH,ADAEUR,ADAGBP,ADAPAX,ADARUB,ADATRY,ADATUSD,ADATWD,ADAUSDC,ADAUSDT,ADXBNB,ADXBTC,ADXBUSD,ADXETH,ADXUSDT,AEBNB,AEBTC,AEETH,AERGOBTC,AERGOBUSD,AGIBNB,AGIBTC,AGIETH,AGIXBTC,AGIXBUSD,AGIXTRY,AGIXUSDT,AGLDBNB,AGLDBTC,AGLDBUSD,AGLDUSDT,AIONBNB,AIONBTC,AIONBUSD,AIONETH,AIONUSDT,AKROBTC,AKROBUSD,AKROUSDT,ALCXBTC,ALCXBUSD,ALCXUSDT,ALGOBIDR,ALGOBNB,ALGOBTC,ALGOBUSD,ALGOETH,ALGOPAX,ALGORUB,ALGOTRY,ALGOTUSD,ALGOUSDC,ALGOUSDT,ALICEBIDR,ALICEBNB,ALICEBTC,ALICEBUSD,ALICETRY,ALICETWD,ALICEUSDT,ALPACABNB,ALPACABTC,ALPACABUSD,ALPACAUSDT,ALPHABNB,ALPHABTC,ALPHABUSD,ALPHAUSDT,ALPINEBTC,ALPINEBUSD,ALPINEEUR,ALPINETRY,ALPINEUSDT,AMBBNB,AMBBTC,AMBBUSD,AMBETH,AMBUSDT,AMPBNB,AMPBTC,AMPBUSD,AMPUSDT,ANCBNB,ANCBTC,ANCBUSD,ANCUSDT,ANKRBNB,ANKRBTC,ANKRBUSD,ANKRPAX,ANKRTRY,ANKRTUSD,ANKRUSDC,ANKRUSDT,ANTBNB,ANTBTC,ANTBUSD,ANTUSDT,ANYBTC,ANYBUSD,ANYUSDT,APEAUD,APEBNB,APEBRL,APEBTC,APEBUSD,APEETH,APEEUR,APEGBP,APETRY,APETWD,APEUSDT,API3BNB,API3BTC,API3BUSD,API3TRY,API3USDT,APPCBNB,APPCBTC,APPCETH,APTBRL,APTBTC,APTBUSD,APTETH,APTEUR,APTTRY,APTUSDT,ARBBTC,ARBEUR,ARBNB,ARBRUB,ARBTC,ARBTRY,ARBTUSD,ARBUSD,ARBUSDT,ARDRBNB,ARDRBTC,ARDRETH,ARDRUSDT,ARKBTC,ARKBUSD,ARKETH,ARNBTC,ARNETH,ARPABNB,ARPABTC,ARPABUSD,ARPAETH,ARPARUB,ARPATRY,ARPAUSDT,ARUSDT,ASRBTC,ASRBUSD,ASRUSDT,ASTBTC,ASTETH,ASTRBTC,ASTRBUSD,ASTRETH,ASTRUSDT,ATABNB,ATABTC,ATABUSD,ATAUSDT,ATMBTC,ATMBUSD,ATMUSDT,ATOMBIDR,ATOMBNB,ATOMBRL,ATOMBTC,ATOMBUSD,ATOMETH,ATOMEUR,ATOMPAX,ATOMTRY,ATOMTUSD,ATOMUSDC,ATOMUSDT,AUCTIONBTC,AUCTIONBUSD,AUCTIONUSDT,AUDBUSD,AUDIOBTC,AUDIOBUSD,AUDIOTRY,AUDIOUSDT,AUDUSDC,AUDUSDT,AUTOBTC,AUTOBUSD,AUTOUSDT,AVABNB,AVABTC,AVABUSD,AVAUSDT,AVAXAUD,AVAXBIDR,AVAXBNB,AVAXBRL,AVAXBTC,AVAXBUSD,AVAXETH,AVAXEUR,AVAXGBP,AVAXTRY,AVAXUSDT,AXSAUD,AXSBNB,AXSBRL,AXSBTC,AXSBUSD,AXSETH,AXSTRY,AXSUSDT,BADGERBTC,BADGERBUSD,BADGERUSDT,BAKEBNB,BAKEBTC,BAKEBUSD,BAKEUSDT,BALBNB,BALBTC,BALBUSD,BALUSDT,BANDBNB,BANDBTC,BANDBUSD,BANDUSDT,BARBTC,BARBUSD,BARUSDT,BATBNB,BATBTC,BATBUSD,BATETH,BATPAX,BATTUSD,BATUSDC,BATUSDT,BCCBNB,BCCBTC,BCCETH,BCCUSDT,BCDBTC,BCDETH,BCHABCBTC,BCHABCBUSD,BCHABCPAX,BCHABCTUSD,BCHABCUSDC,BCHABCUSDT,BCHABUSD,BCHBNB,BCHBTC,BCHBUSD,BCHEUR,BCHPAX,BCHSVBTC,BCHSVPAX,BCHSVTUSD,BCHSVUSDC,BCHSVUSDT,BCHTUSD,BCHTWD,BCHUSDC,BCHUSDT,BCNBNB,BCNBTC,BCNETH,BCNTTWD,BCNTUSDT,BCPTBNB,BCPTBTC,BCPTETH,BCPTPAX,BCPTTUSD,BCPTUSDC,BDOTDOT,BEAMBNB,BEAMBTC,BEAMUSDT,BEARBUSD,BEARUSDT,BELBNB,BELBTC,BELBUSD,BELETH,BELTRY,BELUSDT,BETABNB,BETABTC,BETABUSD,BETAETH,BETAUSDT,BETHBUSD,BETHETH,BETHUSDT,BGBPUSDC,BICOBTC,BICOBUSD,BICOUSDT,BIFIBNB,BIFIBUSD,BIFIUSDT,BKRWBUSD,BKRWUSDT,BLZBNB,BLZBTC,BLZBUSD,BLZETH,BLZUSDT,BNBAUD,BNBBEARBUSD,BNBBEARUSDT,BNBBIDR,BNBBKRW,BNBBRL,BNBBTC,BNBBULLBUSD,BNBBULLUSDT,BNBBUSD,BNBDAI,BNBETH,BNBEUR,BNBGBP,BNBIDRT,BNBNGN,BNBPAX,BNBRUB,BNBTRY,BNBTUSD,BNBTWD,BNBUAH,BNBUSDC,BNBUSDP,BNBUSDS,BNBUSDT,BNBUST,BNBZAR,BNTBTC,BNTBUSD,BNTETH,BNTUSDT,BNXBNB,BNXBTC,BNXBUSD,BNXUSDT,BONDBNB,BONDBTC,BONDBUSD,BONDETH,BONDUSDT,BOTBTC,BOTBUSD,BQXBTC,BQXETH,BRDBNB,BRDBTC,BRDETH,BSWBNB,BSWBUSD,BSWETH,BSWTRY,BSWUSDT,BTCAUD,BTCBBTC,BTCBIDR,BTCBKRW,BTCBRL,BTCBUSD,BTCDAI,BTCEUR,BTCGBP,BTCIDRT,BTCNGN,BTCPAX,BTCPLN,BTCRON,BTCRUB,BTCSTBTC,BTCSTBUSD,BTCSTUSDT,BTCTRY,BTCTUSD,BTCTWD,BTCUAH,BTCUSDC,BTCUSDP,BTCUSDS,BTCUSDT,BTCUST,BTCVAI,BTCZAR,BTGBTC,BTGBUSD,BTGETH,BTGUSDT,BTSBNB,BTSBTC,BTSBUSD,BTSETH,BTSUSDT,BTTBNB,BTTBRL,BTTBTC,BTTBUSD,BTTCBUSD,BTTCTRY,BTTCUSDC,BTTCUSDT,BTTEUR,BTTPAX,BTTTRX,BTTTRY,BTTTUSD,BTTUSDC,BTTUSDT,BULLBUSD,BULLUSDT,BURGERBNB,BURGERBUSD,BURGERETH,BURGERUSDT,BUSDBIDR,BUSDBKRW,BUSDBRL,BUSDBVND,BUSDDAI,BUSDIDRT,BUSDNGN,BUSDPLN,BUSDRON,BUSDRUB,BUSDTRY,BUSDUAH,BUSDUSDT,BUSDVAI,BUSDZAR,BZRXBNB,BZRXBTC,BZRXBUSD,BZRXUSDT,C98BNB,C98BRL,C98BTC,C98BUSD,C98USDT,CAKEAUD,CAKEBNB,CAKEBRL,CAKEBTC,CAKEBUSD,CAKEGBP,CAKEUSDT,CDTBTC,CDTETH,CELOBTC,CELOBUSD,CELOUSDT,CELRBNB,CELRBTC,CELRBUSD,CELRETH,CELRUSDT,CFXBTC,CFXBUSD,CFXTRY,CFXUSDT,CHATBTC,CHATETH,CHESSBNB,CHESSBTC,CHESSBUSD,CHESSUSDT,CHRBNB,CHRBTC,CHRBUSD,CHRETH,CHRUSDT,CHZBNB,CHZBRL,CHZBTC,CHZBUSD,CHZEUR,CHZGBP,CHZTRY,CHZUSDT,CITYBNB,CITYBTC,CITYBUSD,CITYUSDT,CKBBTC,CKBBUSD,CKBUSDT,CLOAKBTC,CLOAKETH,CLVBNB,CLVBTC,CLVBUSD,CLVUSDT,CMTBNB,CMTBTC,CMTETH,CNDBNB,CNDBTC,CNDETH,COCOSBNB,COCOSBTC,COCOSBUSD,COCOSTRY,COCOSUSDT,COMPBNB,COMPBTC,COMPBUSD,COMPTWD,COMPUSDT,COSBNB,COSBTC,COSBUSD,COSTRY,COSUSDT,COTIBNB,COTIBTC,COTIBUSD,COTIUSDT,COVERBUSD,COVERETH,CREAMBNB,CREAMBUSD,CRVBNB,CRVBTC,CRVBUSD,CRVETH,CRVUSDT,CTKBNB,CTKBTC,CTKBUSD,CTKUSDT,CTSIBNB,CTSIBTC,CTSIBUSD,CTSIUSDT,CTXCBNB,CTXCBTC,CTXCBUSD,CTXCUSDT,CVCBNB,CVCBTC,CVCBUSD,CVCETH,CVCUSDT,CVPBUSD,CVPETH,CVPUSDT,CVXBTC,CVXBUSD,CVXUSDT,DAIBNB,DAIBTC,DAIBUSD,DAIUSDT,DARBNB,DARBTC,DARBUSD,DARETH,DAREUR,DARTRY,DARUSDT,DASHBNB,DASHBTC,DASHBUSD,DASHETH,DASHUSDT,DATABTC,DATABUSD,DATAETH,DATAUSDT,DCRBNB,DCRBTC,DCRBUSD,DCRUSDT,DEGOBTC,DEGOBUSD,DEGOUSDT,DENTBTC,DENTBUSD,DENTETH,DENTTRY,DENTUSDT,DEXEBUSD,DEXEETH,DEXEUSDT,DFBUSD,DFETH,DFUSDT,DGBBTC,DGBBUSD,DGBUSDT,DGDBTC,DGDETH,DIABNB,DIABTC,DIABUSD,DIAUSDT,DLTBNB,DLTBTC,DLTETH,DNTBTC,DNTBUSD,DNTETH,DNTUSDT,DOCKBTC,DOCKBUSD,DOCKETH,DOCKUSDT,DODOBTC,DODOBUSD,DODOUSDT,DOGEAUD,DOGEBIDR,DOGEBNB,DOGEBRL,DOGEBTC,DOGEBUSD,DOGEEUR,DOGEGBP,DOGEPAX,DOGERUB,DOGETRY,DOGETWD,DOGEUSDC,DOGEUSDT,DOTAUD,DOTBIDR,DOTBKRW,DOTBNB,DOTBRL,DOTBTC,DOTBUSD,DOTETH,DOTEUR,DOTGBP,DOTNGN,DOTRUB,DOTTRY,DOTTWD,DOTUSDT,DREPBNB,DREPBTC,DREPBUSD,DREPUSDT,DUSKBNB,DUSKBTC,DUSKBUSD,DUSKPAX,DUSKUSDC,DUSKUSDT,DYDXBNB,DYDXBTC,DYDXBUSD,DYDXETH,DYDXUSDT,EASYBTC,EASYETH,EDOBTC,EDOETH,EGLDBNB,EGLDBTC,EGLDBUSD,EGLDETH,EGLDEUR,EGLDRON,EGLDUSDT,ELFBTC,ELFBUSD,ELFETH,ELFUSDT,ENGBTC,ENGETH,ENJBNB,ENJBRL,ENJBTC,ENJBUSD,ENJETH,ENJEUR,ENJGBP,ENJTRY,ENJUSDT,ENSBNB,ENSBTC,ENSBUSD,ENSTRY,ENSTWD,ENSUSDT,EOSAUD,EOSBEARBUSD,EOSBEARUSDT,EOSBNB,EOSBTC,EOSBULLBUSD,EOSBULLUSDT,EOSBUSD,EOSETH,EOSEUR,EOSPAX,EOSTRY,EOSTUSD,EOSUSDC,EOSUSDT,EPSBTC,EPSBUSD,EPSUSDT,EPXBUSD,EPXUSDT,ERDBNB,ERDBTC,ERDBUSD,ERDPAX,ERDUSDC,ERDUSDT,ERNBNB,ERNBUSD,ERNUSDT,ETCBNB,ETCBRL,ETCBTC,ETCBUSD,ETCETH,ETCEUR,ETCGBP,ETCPAX,ETCTRY,ETCTUSD,ETCTWD,ETCUSDC,ETCUSDT,ETCUSDTETHBTC,ETHAUD,ETHBEARBUSD,ETHBEARUSDT,ETHBIDR,ETHBKRW,ETHBRL,ETHBTC,ETHBULLBUSD,ETHBULLUSDT,ETHBUSD,ETHDAI,ETHEUR,ETHGBP,ETHNGN,ETHPAX,ETHPLN,ETHRUB,ETHTRY,ETHTUSD,ETHTWD,ETHUAH,ETHUSDC,ETHUSDP,ETHUSDT,ETHUST,ETHZAR,EURBUSD,EURUSDT,EVXBTC,EVXETH,FARMBNB,FARMBTC,FARMBUSD,FARMETH,FARMUSDT,FETBNB,FETBTC,FETBUSD,FETTRY,FETUSDT,FIDABNB,FIDABTC,FIDABUSD,FIDAUSDT,FILBNB,FILBTC,FILBUSD,FILETH,FILTRY,FILUSDT,FIOBNB,FIOBTC,FIOBUSD,FIOUSDT,FIROBTC,FIROBUSD,FIROETH,FIROUSDT,FISBIDR,FISBRL,FISBTC,FISBUSD,FISTRY,FISUSDT,FLMBNB,FLMBTC,FLMBUSD,FLMUSDT,FLOWBNB,FLOWBTC,FLOWBUSD,FLOWUSDT,FLUXBTC,FLUXBUSD,FLUXUSDT,FORBNB,FORBTC,FORBUSD,FORTHBTC,FORTHBUSD,FORTHUSDT,FORUSDT,FRONTBTC,FRONTBUSD,FRONTETH,FRONTUSDT,FTMAUD,FTMBIDR,FTMBNB,FTMBRL,FTMBTC,FTMBUSD,FTMETH,FTMEUR,FTMPAX,FTMRUB,FTMTRY,FTMTUSD,FTMUSDC,FTMUSDT,FTTBNB,FTTBTC,FTTBUSD,FTTETH,FTTUSDT,FUELBTC,FUELETH,FUNBNB,FUNBTC,FUNETH,FUNUSDT,FXSBTC,FXSBUSD,FXSUSDT,GALAAUD,GALABNB,GALABRL,GALABTC,GALABUSD,GALAETH,GALAEUR,GALATRY,GALATWD,GALAUSDT,GALBNB,GALBRL,GALBTC,GALBUSD,GALETH,GALEUR,GALTRY,GALUSDT,GASBTC,GASBUSD,GASUSDT,GBPBUSD,GBPUSDT,GFTBUSD,GHSTBUSD,GHSTETH,GHSTUSDT,GLMBTC,GLMBUSD,GLMETH,GLMRBNB,GLMRBTC,GLMRBUSD,GLMRUSDT,GLMUSDT,GMTAUD,GMTBNB,GMTBRL,GMTBTC,GMTBUSD,GMTETH,GMTEUR,GMTGBP,GMTTRY,GMTTWD,GMTUSDT,GMXBTC,GMXBUSD,GMXUSDT,GNOBNB,GNOBTC,GNOBUSD,GNOUSDT,GNSBTC,GNSUSDT,GNTBNB,GNTBTC,GNTETH,GOBNB,GOBTC,GRSBTC,GRSETH,GRTBTC,GRTBUSD,GRTETH,GRTEUR,GRTTRY,GRTTWD,GRTUSDT,GSTTWD,GTCBNB,GTCBTC,GTCBUSD,GTCUSDT,GTOBNB,GTOBTC,GTOBUSD,GTOETH,GTOPAX,GTOTUSD,GTOUSDC,GTOUSDT,GVTBTC,GVTETH,GXSBNB,GXSBTC,GXSETH,GXSUSDT,HARDBNB,HARDBTC,HARDBUSD,HARDUSDT,HBARBNB,HBARBTC,HBARBUSD,HBARUSDT,HCBTC,HCETH,HCUSDT,HEGICBUSD,HEGICETH,HFTBTC,HFTBUSD,HFTUSDT,HIFIETH,HIFIUSDT,HIGHBNB,HIGHBTC,HIGHBUSD,HIGHUSDT,HIVEBNB,HIVEBTC,HIVEBUSD,HIVEUSDT,HNTBTC,HNTBUSD,HNTUSDT,HOOKBNB,HOOKBTC,HOOKBUSD,HOOKUSDT,HOTBNB,HOTBRL,HOTBTC,HOTBUSD,HOTETH,HOTEUR,HOTTRY,HOTUSDT,HSRBTC,HSRETH,ICNBTC,ICNETH,ICPBNB,ICPBTC,ICPBUSD,ICPETH,ICPEUR,ICPRUB,ICPTRY,ICPUSDT,ICXBNB,ICXBTC,ICXBUSD,ICXETH,ICXUSDT,IDBNB,IDBTC,IDEUR,IDEXBNB,IDEXBTC,IDEXBUSD,IDEXUSDT,IDTRY,IDTUSD,IDUSDT,ILVBNB,ILVBTC,ILVBUSD,ILVUSDT,IMXBNB,IMXBTC,IMXBUSD,IMXUSDT,INJBNB,INJBTC,INJBUSD,INJTRY,INJUSDT,INSBTC,INSETH,IOSTBTC,IOSTBUSD,IOSTETH,IOSTUSDT,IOTABNB,IOTABTC,IOTABUSD,IOTAETH,IOTAUSDT,IOTXBTC,IOTXBUSD,IOTXETH,IOTXUSDT,IQBNB,IQBUSD,IRISBNB,IRISBTC,IRISBUSD,IRISUSDT,JASMYBNB,JASMYBTC,JASMYBUSD,JASMYETH,JASMYEUR,JASMYTRY,JASMYUSDT,JOEBTC,JOEBUSD,JOETRY,JOEUSDT,JSTBNB,JSTBTC,JSTBUSD,JSTUSDT,JUVBTC,JUVBUSD,JUVUSDT,KAVABNB,KAVABTC,KAVABUSD,KAVAETH,KAVAUSDT,KDABTC,KDABUSD,KDAUSDT,KEEPBNB,KEEPBTC,KEEPBUSD,KEEPUSDT,KEYBTC,KEYBUSD,KEYETH,KEYUSDT,KLAYBNB,KLAYBTC,KLAYBUSD,KLAYUSDT,KMDBTC,KMDBUSD,KMDETH,KMDUSDT,KNCBNB,KNCBTC,KNCBUSD,KNCETH,KNCUSDT,KP3RBNB,KP3RBUSD,KP3RUSDT,KSMAUD,KSMBNB,KSMBTC,KSMBUSD,KSMETH,KSMUSDT,LAZIOBTC,LAZIOBUSD,LAZIOEUR,LAZIOTRY,LAZIOUSDT,LDOBTC,LDOBUSD,LDOTUSD,LDOUSDT,LENDBKRW,LENDBTC,LENDBUSD,LENDETH,LENDUSDT,LEVERBUSD,LEVERUSDT,LINABNB,LINABTC,LINABUSD,LINAUSDT,LINKAUD,LINKBKRW,LINKBNB,LINKBRL,LINKBTC,LINKBUSD,LINKETH,LINKEUR,LINKGBP,LINKNGN,LINKPAX,LINKTRY,LINKTUSD,LINKTWD,LINKUSDC,LINKUSDT,LITBTC,LITBUSD,LITETH,LITUSDT,LOKABNB,LOKABTC,LOKABUSD,LOKAUSDT,LOOKSTWD,LOOMBNB,LOOMBTC,LOOMBUSD,LOOMETH,LOOMUSDT,LOOTTWD,LOOTUSDT,LPTBNB,LPTBTC,LPTBUSD,LPTUSDT,LQTYBTC,LQTYUSDT,LRCBNB,LRCBTC,LRCBUSD,LRCETH,LRCTRY,LRCUSDT,LSKBNB,LSKBTC,LSKBUSD,LSKETH,LSKUSDT,LTCBNB,LTCBRL,LTCBTC,LTCBUSD,LTCETH,LTCEUR,LTCGBP,LTCNGN,LTCPAX,LTCRUB,LTCTUSD,LTCTWD,LTCUAH,LTCUSDC,LTCUSDT,LTOBNB,LTOBTC,LTOBUSD,LTOUSDT,LUNAAUD,LUNABIDR,LUNABNB,LUNABRL,LUNABTC,LUNABUSD,LUNAETH,LUNAEUR,LUNAGBP,LUNATRY,LUNAUSDT,LUNAUST,LUNBTC,LUNCBUSD,LUNCUSDT,LUNETH,MAGICBTC,MAGICBUSD,MAGICTRY,MAGICUSDT,MANABIDR,MANABNB,MANABRL,MANABTC,MANABUSD,MANAETH,MANATRY,MANATWD,MANAUSDT,MASKBNB,MASKBUSD,MASKUSDT,MATICAUD,MATICBIDR,MATICBNB,MATICBRL,MATICBTC,MATICBUSD,MATICETH,MATICEUR,MATICGBP,MATICRUB,MATICTRY,MATICTUSD,MATICTWD,MATICUSDT,MAXTWD,MAXUSDT,MBLBNB,MBLBTC,MBLBUSD,MBLUSDT,MBOXBNB,MBOXBTC,MBOXBUSD,MBOXTRY,MBOXUSDT,MCBNB,MCBTC,MCBUSD,MCOBNB,MCOBTC,MCOETH,MCOUSDT,MCUSDT,MDABTC,MDAETH,MDTBNB,MDTBTC,MDTBUSD,MDTUSDT,MDXBNB,MDXBTC,MDXBUSD,MDXUSDT,MFTBNB,MFTBTC,MFTETH,MFTUSDT,MINABNB,MINABTC,MINABUSD,MINATRY,MINAUSDT,MIRBTC,MIRBUSD,MIRUSDT,MITHBNB,MITHBTC,MITHUSDT,MKRBNB,MKRBTC,MKRBUSD,MKRUSDT,MLNBNB,MLNBTC,MLNBUSD,MLNUSDT,MOBBTC,MOBBUSD,MOBUSDT,MODBTC,MODETH,MOVRBNB,MOVRBTC,MOVRBUSD,MOVRUSDT,MTHBTC,MTHETH,MTLBTC,MTLBUSD,MTLETH,MTLUSDT,MULTIBTC,MULTIBUSD,MULTIUSDT,NANOBNB,NANOBTC,NANOBUSD,NANOETH,NANOUSDT,NASBNB,NASBTC,NASETH,NAVBNB,NAVBTC,NAVETH,NBSBTC,NBSUSDT,NCASHBNB,NCASHBTC,NCASHETH,NEARBNB,NEARBTC,NEARBUSD,NEARETH,NEAREUR,NEARRUB,NEARTRY,NEARUSDT,NEBLBNB,NEBLBTC,NEBLBUSD,NEBLUSDT,NEOBNB,NEOBTC,NEOBUSD,NEOETH,NEOPAX,NEORUB,NEOTRY,NEOTUSD,NEOUSDC,NEOUSDT,NEXOBTC,NEXOBUSD,NEXOUSDT,NKNBNB,NKNBTC,NKNBUSD,NKNUSDT,NMRBTC,NMRBUSD,NMRUSDT,NPXSBTC,NPXSETH,NPXSUSDC,NPXSUSDT,NUAUD,NUBNB,NUBTC,NUBUSD,NULSBNB,NULSBTC,NULSBUSD,NULSETH,NULSUSDT,NURUB,NUUSDT,NXSBNB,NXSBTC,NXSETH,OAXBTC,OAXETH,OAXUSDT,OCEANBNB,OCEANBTC,OCEANBUSD,OCEANUSDT,OGBTC,OGBUSD,OGNBNB,OGNBTC,OGNBUSD,OGNUSDT,OGUSDT,OMBTC,OMBUSD,OMGBNB,OMGBTC,OMGBUSD,OMGETH,OMGUSDT,OMUSDT,ONEBIDR,ONEBNB,ONEBTC,ONEBUSD,ONEETH,ONEPAX,ONETRY,ONETUSD,ONEUSDC,ONEUSDT,ONGBNB,ONGBTC,ONGUSDT,ONTBNB,ONTBTC,ONTBUSD,ONTETH,ONTPAX,ONTTRY,ONTUSDC,ONTUSDT,OOKIBNB,OOKIBUSD,OOKIETH,OOKIUSDT,OPBNB,OPBTC,OPBUSD,OPETH,OPEUR,OPTUSD,OPUSDT,ORNBTC,ORNBUSD,ORNUSDT,OSMOBTC,OSMOBUSD,OSMOUSDT,OSTBNB,OSTBTC,OSTETH,OXTBTC,OXTBUSD,OXTUSDT,PAXBNB,PAXBTC,PAXBUSD,PAXETH,PAXGBNB,PAXGBTC,PAXGBUSD,PAXGUSDT,PAXTUSD,PAXUSDT,PEOPLEBNB,PEOPLEBTC,PEOPLEBUSD,PEOPLEETH,PEOPLEUSDT,PERLBNB,PERLBTC,PERLUSDC,PERLUSDT,PERPBTC,PERPBUSD,PERPUSDT,PHABTC,PHABUSD,PHAUSDT,PHBBNB,PHBBTC,PHBBUSD,PHBPAX,PHBTUSD,PHBUSDC,PHBUSDT,PHXBNB,PHXBTC,PHXETH,PIVXBNB,PIVXBTC,PLABNB,PLABTC,PLABUSD,PLAUSDT,PNTBTC,PNTUSDT,POABNB,POABTC,POAETH,POEBTC,POEETH,POLSBNB,POLSBTC,POLSBUSD,POLSUSDT,POLYBNB,POLYBTC,POLYBUSD,POLYUSDT,POLYXBTC,POLYXBUSD,POLYXUSDT,PONDBTC,PONDBUSD,PONDUSDT,PORTOBTC,PORTOBUSD,PORTOEUR,PORTOTRY,PORTOUSDT,POWRBNB,POWRBTC,POWRBUSD,POWRETH,POWRUSDT,PPTBTC,PPTETH,PROMBNB,PROMBTC,PROMBUSD,PROMUSDT,PROSBUSD,PROSETH,PROSUSDT,PSGBTC,PSGBUSD,PSGUSDT,PUNDIXBUSD,PUNDIXETH,PUNDIXUSDT,PYRBTC,PYRBUSD,PYRUSDT,QIBNB,QIBTC,QIBUSD,QIUSDT,QKCBTC,QKCBUSD,QKCETH,QKCUSDT,QLCBNB,QLCBTC,QLCETH,QNTBNB,QNTBTC,QNTBUSD,QNTUSDT,QSPBNB,QSPBTC,QSPETH,QTUMBNB,QTUMBTC,QTUMBUSD,QTUMETH,QTUMUSDT,QUICKBNB,QUICKBTC,QUICKBUSD,QUICKUSDT,RADBNB,RADBTC,RADBUSD,RADUSDT,RAMPBTC,RAMPBUSD,RAMPUSDT,RAREBNB,RAREBTC,RAREBUSD,RAREUSDT,RAYBNB,RAYBUSD,RAYUSDT,RCNBNB,RCNBTC,RCNETH,RDNBNB,RDNBTC,RDNETH,RDNTBTC,RDNTTUSD,RDNTUSDT,REEFBIDR,REEFBTC,REEFBUSD,REEFTRY,REEFUSDT,REIBNB,REIBUSD,REIETH,REIUSDT,RENBNB,RENBTC,RENBTCBTC,RENBTCETH,RENBUSD,RENUSDT,REPBNB,REPBTC,REPBUSD,REPUSDT,REQBTC,REQBUSD,REQETH,REQUSDT,RGTBNB,RGTBTC,RGTBUSD,RGTUSDT,RIFBTC,RIFUSDT,RLCBNB,RLCBTC,RLCBUSD,RLCETH,RLCUSDT,RLYTWD,RLYUSDT,RNDRBTC,RNDRBUSD,RNDRUSDT,ROSEBNB,ROSEBTC,ROSEBUSD,ROSEETH,ROSETRY,ROSEUSDT,RPLBTC,RPLBUSD,RPLUSDT,RPXBNB,RPXBTC,RPXETH,RSRBNB,RSRBTC,RSRBUSD,RSRUSDT,RUNEAUD,RUNEBNB,RUNEBTC,RUNEBUSD,RUNEETH,RUNEEUR,RUNEGBP,RUNETRY,RUNEUSDT,RVNBTC,RVNBUSD,RVNTRY,RVNUSDT,SALTBTC,SALTETH,SANDAUD,SANDBIDR,SANDBNB,SANDBRL,SANDBTC,SANDBUSD,SANDETH,SANDTRY,SANDTWD,SANDUSDT,SANTOSBRL,SANTOSBTC,SANTOSBUSD,SANTOSTRY,SANTOSUSDT,SCBTC,SCBUSD,SCETH,SCRTBTC,SCRTBUSD,SCRTETH,SCRTUSDT,SCUSDT,SFPBTC,SFPBUSD,SFPUSDT,SHIBAUD,SHIBBRL,SHIBBUSD,SHIBDOGE,SHIBEUR,SHIBGBP,SHIBRUB,SHIBTRY,SHIBTWD,SHIBUAH,SHIBUSDT,SKLBTC,SKLBUSD,SKLUSDT,SKYBNB,SKYBTC,SKYETH,SLPBIDR,SLPBNB,SLPBUSD,SLPETH,SLPTRY,SLPUSDT,SNGLSBTC,SNGLSETH,SNMBTC,SNMBUSD,SNMETH,SNTBTC,SNTBUSD,SNTETH,SNXBNB,SNXBTC,SNXBUSD,SNXETH,SNXUSDT,SOLAUD,SOLBIDR,SOLBNB,SOLBRL,SOLBTC,SOLBUSD,SOLETH,SOLEUR,SOLGBP,SOLRUB,SOLTRY,SOLTUSD,SOLTWD,SOLUSDC,SOLUSDT,SPARTABNB,SPELLBNB,SPELLBTC,SPELLBUSD,SPELLTRY,SPELLUSDT,SRMBIDR,SRMBNB,SRMBTC,SRMBUSD,SRMUSDT,SSVBTC,SSVBUSD,SSVETH,SSVTUSD,SSVUSDT,STEEMBNB,STEEMBTC,STEEMBUSD,STEEMETH,STEEMUSDT,STGBTC,STGBUSD,STGUSDT,STMXBTC,STMXBUSD,STMXETH,STMXUSDT,STORJBTC,STORJBUSD,STORJETH,STORJTRY,STORJUSDT,STORMBNB,STORMBTC,STORMETH,STORMUSDT,STPTBNB,STPTBTC,STPTBUSD,STPTUSDT,STRATBNB,STRATBTC,STRATBUSD,STRATETH,STRATUSDT,STRAXBTC,STRAXBUSD,STRAXETH,STRAXUSDT,STXBNB,STXBTC,STXBUSD,STXTRY,STXUSDT,SUBBTC,SUBETH,SUNBTC,SUNBUSD,SUNUSDT,SUPERBTC,SUPERBUSD,SUPERUSDT,SUSDBTC,SUSDETH,SUSDUSDT,SUSHIBNB,SUSHIBTC,SUSHIBUSD,SUSHIUSDT,SWRVBNB,SWRVBUSD,SXPAUD,SXPBIDR,SXPBNB,SXPBTC,SXPBUSD,SXPEUR,SXPGBP,SXPTRY,SXPUSDT,SYNBTC,SYNUSDT,SYSBNB,SYSBTC,SYSBUSD,SYSETH,SYSUSDT,TBUSD,TCTBNB,TCTBTC,TCTUSDT,TFUELBNB,TFUELBTC,TFUELBUSD,TFUELPAX,TFUELTUSD,TFUELUSDC,TFUELUSDT,THETABNB,THETABTC,THETABUSD,THETAETH,THETAEUR,THETAUSDT,TKOBIDR,TKOBTC,TKOBUSD,TKOUSDT,TLMBNB,TLMBTC,TLMBUSD,TLMTRY,TLMUSDT,TNBBTC,TNBETH,TNTBTC,TNTETH,TOMOBNB,TOMOBTC,TOMOBUSD,TOMOUSDC,TOMOUSDT,TORNBNB,TORNBTC,TORNBUSD,TORNUSDT,TRBBNB,TRBBTC,TRBBUSD,TRBUSDT,TRIBEBNB,TRIBEBTC,TRIBEBUSD,TRIBEUSDT,TRIGBNB,TRIGBTC,TRIGETH,TROYBNB,TROYBTC,TROYBUSD,TROYUSDT,TRUBTC,TRUBUSD,TRURUB,TRUUSDT,TRXAUD,TRXBNB,TRXBTC,TRXBUSD,TRXETH,TRXEUR,TRXNGN,TRXPAX,TRXTRY,TRXTUSD,TRXUSDC,TRXUSDT,TRXXRP,TUSDBNB,TUSDBTC,TUSDBTUSD,TUSDBUSD,TUSDETH,TUSDT,TUSDUSDT,TVKBTC,TVKBUSD,TVKUSDT,TWTBTC,TWTBUSD,TWTTRY,TWTUSDT,UFTBUSD,UFTETH,UFTUSDT,UMABTC,UMABUSD,UMATRY,UMAUSDT,UNFIBNB,UNFIBTC,UNFIBUSD,UNFIETH,UNFIUSDT,UNIAUD,UNIBNB,UNIBTC,UNIBUSD,UNIETH,UNIEUR,UNIUSDT,USDCBNB,USDCBUSD,USDCPAX,USDCTUSD,USDCTWD,USDCUSDT,USDPBUSD,USDPUSDT,USDSBUSDS,USDSBUSDT,USDSPAX,USDSTUSD,USDSUSDC,USDSUSDT,USDTBIDR,USDTBKRW,USDTBRL,USDTBVND,USDTDAI,USDTIDRT,USDTNGN,USDTPLN,USDTRON,USDTRUB,USDTTRY,USDTTWD,USDTUAH,USDTZAR,USTBTC,USTBUSD,USTCBUSD,USTCUSDT,USTUSDT,UTKBTC,UTKBUSD,UTKUSDT,VENBNB,VENBTC,VENETH,VENUSDT,VETBNB,VETBTC,VETBUSD,VETETH,VETEUR,VETGBP,VETTRY,VETUSDT,VGXBTC,VGXETH,VGXUSDT,VIABNB,VIABTC,VIAETH,VIBBTC,VIBBUSD,VIBEBTC,VIBEETH,VIBETH,VIBUSDT,VIDTBTC,VIDTBUSD,VIDTUSDT,VITEBNB,VITEBTC,VITEBUSD,VITEUSDT,VOXELBNB,VOXELBTC,VOXELBUSD,VOXELETH,VOXELUSDT,VTHOBNB,VTHOBUSD,VTHOUSDT,WABIBNB,WABIBTC,WABIETH,WANBNB,WANBTC,WANETH,WANUSDT,WAVESBNB,WAVESBTC,WAVESBUSD,WAVESETH,WAVESEUR,WAVESPAX,WAVESRUB,WAVESTRY,WAVESTUSD,WAVESUSDC,WAVESUSDT,WAXPBNB,WAXPBTC,WAXPBUSD,WAXPUSDT,WBTCBTC,WBTCBUSD,WBTCETH,WINBNB,WINBRL,WINBTC,WINBUSD,WINEUR,WINGBNB,WINGBTC,WINGBUSD,WINGETH,WINGSBTC,WINGSETH,WINGUSDT,WINTRX,WINUSDC,WINUSDT,WNXMBNB,WNXMBTC,WNXMBUSD,WNXMUSDT,WOOBNB,WOOBTC,WOOBUSD,WOOUSDT,WPRBTC,WPRETH,WRXBNB,WRXBTC,WRXBUSD,WRXEUR,WRXUSDT,WTCBNB,WTCBTC,WTCETH,WTCUSDT,XECBUSD,XECUSDT,XEMBNB,XEMBTC,XEMBUSD,XEMETH,XEMUSDT,XLMBNB,XLMBTC,XLMBUSD,XLMETH,XLMEUR,XLMPAX,XLMTRY,XLMTUSD,XLMUSDC,XLMUSDT,XMRBNB,XMRBTC,XMRBUSD,XMRETH,XMRUSDT,XNOBTC,XNOBUSD,XNOETH,XNOUSDT,XRPAUD,XRPBEARBUSD,XRPBEARUSDT,XRPBIDR,XRPBKRW,XRPBNB,XRPBRL,XRPBTC,XRPBULLBUSD,XRPBULLUSDT,XRPBUSD,XRPETH,XRPEUR,XRPGBP,XRPNGN,XRPPAX,XRPRUB,XRPTRY,XRPTUSD,XRPTWD,XRPUSDC,XRPUSDT,XTZBNB,XTZBTC,XTZBUSD,XTZETH,XTZTRY,XTZTWD,XTZUSDT,XVGBTC,XVGBUSD,XVGETH,XVGUSDT,XVSBNB,XVSBTC,XVSBUSD,XVSTRY,XVSUSDT,XZCBNB,XZCBTC,XZCETH,XZCUSDT,XZCXRP,YFIBNB,YFIBTC,YFIBUSD,YFIEUR,YFIIBNB,YFIIBTC,YFIIBUSD,YFIIUSDT,YFITWD,YFIUSDT,YGGBNB,YGGBTC,YGGBUSD,YGGUSDT,YOYOBNB,YOYOBTC,YOYOETH,ZECBNB,ZECBTC,ZECBUSD,ZECETH,ZECPAX,ZECTUSD,ZECUSDC,ZECUSDT,ZENBNB,ZENBTC,ZENBUSD,ZENETH,ZENUSDT,ZILBIDR,ZILBNB,ZILBTC,ZILBUSD,ZILETH,ZILEUR,ZILTRY,ZILUSDT,ZRXBNB,ZRXBTC,ZRXBUSD,ZRXETH,ZRXUSDT, +} + +func toSymbol(s string) string { + for _, symbol := range symbols { + if s == symbol { + return symbol + } + } + return s +} + +func compileSymbols(symbols []string) []string { + var ss = make([]string, len(symbols)) + for i, s := range symbols { + ss[i] = toSymbol(s) + } + + return ss +} + diff --git a/pkg/strategy/tri/symbols.sh b/pkg/strategy/tri/symbols.sh new file mode 100644 index 0000000000..dbf68a8314 --- /dev/null +++ b/pkg/strategy/tri/symbols.sh @@ -0,0 +1,33 @@ +#!/bin/bash +echo '// Code generated by "bash symbols.sh"; DO NOT EDIT.' > symbols.go +echo 'package tri' >> symbols.go + +max_symbols=$(curl -s https://max-api.maicoin.com/api/v2/markets | jq -r '.[].id | ascii_upcase') +binance_symbols=$(curl -s https://api.binance.com/api/v3/exchangeInfo | jq -r '.symbols[].symbol | ascii_upcase') +symbols=$(echo "$max_symbols$binance_symbols" | sort | uniq | grep -v -E '^[0-9]' | grep -v "DOWNUSDT" | grep -v "UPUSDT") +echo "$symbols" | perl -l -n -e 'BEGIN { print "const (" } END { print ")" } print qq{\t$_ = "$_"}' >> symbols.go + +cat <> symbols.go +var symbols = []string{ + $(echo -e "$symbols" | tr '\n' ',') +} + +func toSymbol(s string) string { + for _, symbol := range symbols { + if s == symbol { + return symbol + } + } + return s +} + +func compileSymbols(symbols []string) []string { + var ss = make([]string, len(symbols)) + for i, s := range symbols { + ss[i] = toSymbol(s) + } + + return ss +} + +DOC diff --git a/pkg/strategy/tri/utils.go b/pkg/strategy/tri/utils.go new file mode 100644 index 0000000000..2cb074369a --- /dev/null +++ b/pkg/strategy/tri/utils.go @@ -0,0 +1,28 @@ +package tri + +import ( + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +func fitQuantityByBase(quantity, balance float64) (float64, float64) { + q := math.Min(quantity, balance) + r := q / balance + return q, r +} + +// 1620 x 2 , quote balance = 1000 => rate = 1000/(1620*2) = 0.3086419753, quantity = 0.61728395 +func fitQuantityByQuote(price, quantity, quoteBalance float64) (float64, float64) { + quote := quantity * price + minQuote := math.Min(quote, quoteBalance) + q := minQuote / price + r := minQuote / quoteBalance + return q, r +} + +func logSubmitOrders(orders [3]types.SubmitOrder) { + for i, order := range orders { + log.Infof("SUBMIT ORDER #%d: %s", i, order.String()) + } +} From 1abb301af1522e77574439a9f543007dfe03caff Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 15:51:29 +0800 Subject: [PATCH 1116/1392] core: add order update trigger channel --- pkg/core/orderstore.go | 8 ++++++++ pkg/types/market.go | 1 + 2 files changed, 9 insertions(+) diff --git a/pkg/core/orderstore.go b/pkg/core/orderstore.go index 8f3742ce42..3af1ab81e7 100644 --- a/pkg/core/orderstore.go +++ b/pkg/core/orderstore.go @@ -15,12 +15,14 @@ type OrderStore struct { RemoveCancelled bool RemoveFilled bool AddOrderUpdate bool + C chan types.Order } func NewOrderStore(symbol string) *OrderStore { return &OrderStore{ Symbol: symbol, orders: make(map[uint64]types.Order), + C: make(chan types.Order), } } @@ -129,6 +131,7 @@ func (s *OrderStore) BindStream(stream types.Stream) { } func (s *OrderStore) HandleOrderUpdate(order types.Order) { + switch order.Status { case types.OrderStatusNew, types.OrderStatusPartiallyFilled, types.OrderStatusFilled: @@ -152,4 +155,9 @@ func (s *OrderStore) HandleOrderUpdate(order types.Order) { case types.OrderStatusRejected: s.Remove(order) } + + select { + case s.C <- order: + default: + } } diff --git a/pkg/types/market.go b/pkg/types/market.go index bfcd0ac431..a4e0a308c6 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -66,6 +66,7 @@ func (m Market) TruncateQuantity(quantity fixedpoint.Value) fixedpoint.Value { qf := math.Trunc(quantity.Float64() * pow10) qf = qf / pow10 + qs := strconv.FormatFloat(qf, 'f', prec, 64) return fixedpoint.MustNewFromString(qs) } From f06e37c44f855bb150df661b4f2ce0a1a937d8df Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 16:02:11 +0800 Subject: [PATCH 1117/1392] tri: ignore test in dnum mode --- pkg/strategy/tri/strategy_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/tri/strategy_test.go b/pkg/strategy/tri/strategy_test.go index 723240b55f..cf6e6d44b5 100644 --- a/pkg/strategy/tri/strategy_test.go +++ b/pkg/strategy/tri/strategy_test.go @@ -1,3 +1,5 @@ +//go:build !dnum + package tri import ( From 798b3c62bba5c5da5195e74cb88159c9a936c8ca Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 16:08:35 +0800 Subject: [PATCH 1118/1392] add tri strategy config --- config/tri.yaml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 config/tri.yaml diff --git a/config/tri.yaml b/config/tri.yaml new file mode 100644 index 0000000000..e59f87e039 --- /dev/null +++ b/config/tri.yaml @@ -0,0 +1,33 @@ +--- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + +sessions: + binance: + exchange: binance + envVarPrefix: binance + +exchangeStrategies: +- on: binance + tri: + minSpreadRatio: 1.0011 + separateStream: true + # resetPosition: true + limits: + BTC: 0.001 + ETH: 0.01 + USDT: 20.0 + symbols: + - BNBUSDT + - BNBBTC + - BNBETH + - BTCUSDT + - ETHUSDT + - ETHBTC + paths: + - [ BTCUSDT, ETHBTC, ETHUSDT ] + - [ BNBBTC, BNBUSDT, BTCUSDT ] + - [ BNBETH, BNBUSDT, ETHUSDT ] + From e67a7fb5532057825352953093e4bc5d12907fed Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 16:09:03 +0800 Subject: [PATCH 1119/1392] config: add description --- config/tri.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/tri.yaml b/config/tri.yaml index e59f87e039..c82180ce46 100644 --- a/config/tri.yaml +++ b/config/tri.yaml @@ -10,6 +10,8 @@ sessions: envVarPrefix: binance exchangeStrategies: + +## triangular arbitrage strategy - on: binance tri: minSpreadRatio: 1.0011 From 631203c89ec0cf2ffa3a6b759bff9922d8b54bdc Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 16:46:43 +0800 Subject: [PATCH 1120/1392] tri: update symbol file --- pkg/strategy/tri/symbols.go | 47 ++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/tri/symbols.go b/pkg/strategy/tri/symbols.go index 38abba858b..9daebbf3d4 100644 --- a/pkg/strategy/tri/symbols.go +++ b/pkg/strategy/tri/symbols.go @@ -45,6 +45,7 @@ const ( AEETH = "AEETH" AERGOBTC = "AERGOBTC" AERGOBUSD = "AERGOBUSD" + AERGOUSDT = "AERGOUSDT" AGIBNB = "AGIBNB" AGIBTC = "AGIBTC" AGIETH = "AGIETH" @@ -187,6 +188,7 @@ const ( ASTRBUSD = "ASTRBUSD" ASTRETH = "ASTRETH" ASTRUSDT = "ASTRUSDT" + ASTUSDT = "ASTUSDT" ATABNB = "ATABNB" ATABTC = "ATABTC" ATABUSD = "ATABUSD" @@ -393,6 +395,7 @@ const ( BSWETH = "BSWETH" BSWTRY = "BSWTRY" BSWUSDT = "BSWUSDT" + BTCARS = "BTCARS" BTCAUD = "BTCAUD" BTCBBTC = "BTCBBTC" BTCBIDR = "BTCBIDR" @@ -496,6 +499,7 @@ const ( CFXBTC = "CFXBTC" CFXBUSD = "CFXBUSD" CFXTRY = "CFXTRY" + CFXTUSD = "CFXTUSD" CFXUSDT = "CFXUSDT" CHATBTC = "CHATBTC" CHATETH = "CHATETH" @@ -519,6 +523,7 @@ const ( CITYBNB = "CITYBNB" CITYBTC = "CITYBTC" CITYBUSD = "CITYBUSD" + CITYTRY = "CITYTRY" CITYUSDT = "CITYUSDT" CKBBTC = "CKBBTC" CKBBUSD = "CKBBUSD" @@ -540,6 +545,9 @@ const ( COCOSBUSD = "COCOSBUSD" COCOSTRY = "COCOSTRY" COCOSUSDT = "COCOSUSDT" + COMBOBNB = "COMBOBNB" + COMBOTRY = "COMBOTRY" + COMBOUSDT = "COMBOUSDT" COMPBNB = "COMPBNB" COMPBTC = "COMPBTC" COMPBUSD = "COMPBUSD" @@ -658,6 +666,7 @@ const ( DOGEPAX = "DOGEPAX" DOGERUB = "DOGERUB" DOGETRY = "DOGETRY" + DOGETUSD = "DOGETUSD" DOGETWD = "DOGETWD" DOGEUSDC = "DOGEUSDC" DOGEUSDT = "DOGEUSDT" @@ -695,6 +704,12 @@ const ( EASYETH = "EASYETH" EDOBTC = "EDOBTC" EDOETH = "EDOETH" + EDUBNB = "EDUBNB" + EDUBTC = "EDUBTC" + EDUEUR = "EDUEUR" + EDUTRY = "EDUTRY" + EDUTUSD = "EDUTUSD" + EDUUSDT = "EDUUSDT" EGLDBNB = "EGLDBNB" EGLDBTC = "EGLDBTC" EGLDBUSD = "EGLDBUSD" @@ -796,6 +811,8 @@ const ( EURUSDT = "EURUSDT" EVXBTC = "EVXBTC" EVXETH = "EVXETH" + EZBTC = "EZBTC" + EZETH = "EZETH" FARMBNB = "FARMBNB" FARMBTC = "FARMBTC" FARMBUSD = "FARMBUSD" @@ -834,6 +851,9 @@ const ( FLMBTC = "FLMBTC" FLMBUSD = "FLMBUSD" FLMUSDT = "FLMUSDT" + FLOKITRY = "FLOKITRY" + FLOKITUSD = "FLOKITUSD" + FLOKIUSDT = "FLOKIUSDT" FLOWBNB = "FLOWBNB" FLOWBTC = "FLOWBTC" FLOWBUSD = "FLOWBUSD" @@ -1205,6 +1225,7 @@ const ( LTCNGN = "LTCNGN" LTCPAX = "LTCPAX" LTCRUB = "LTCRUB" + LTCTRY = "LTCTRY" LTCTUSD = "LTCTUSD" LTCTWD = "LTCTWD" LTCUAH = "LTCUAH" @@ -1260,6 +1281,9 @@ const ( MATICTUSD = "MATICTUSD" MATICTWD = "MATICTWD" MATICUSDT = "MATICUSDT" + MAVBTC = "MAVBTC" + MAVTUSD = "MAVTUSD" + MAVUSDT = "MAVUSDT" MAXTWD = "MAXTWD" MAXUSDT = "MAXUSDT" MBLBNB = "MBLBNB" @@ -1409,6 +1433,7 @@ const ( OGNBTC = "OGNBTC" OGNBUSD = "OGNBUSD" OGNUSDT = "OGNUSDT" + OGTRY = "OGTRY" OGUSDT = "OGUSDT" OMBTC = "OMBTC" OMBUSD = "OMBUSD" @@ -1448,6 +1473,7 @@ const ( OPBUSD = "OPBUSD" OPETH = "OPETH" OPEUR = "OPEUR" + OPTRY = "OPTRY" OPTUSD = "OPTUSD" OPUSDT = "OPUSDT" ORNBTC = "ORNBTC" @@ -1469,14 +1495,21 @@ const ( PAXGBNB = "PAXGBNB" PAXGBTC = "PAXGBTC" PAXGBUSD = "PAXGBUSD" + PAXGTRY = "PAXGTRY" PAXGUSDT = "PAXGUSDT" PAXTUSD = "PAXTUSD" PAXUSDT = "PAXUSDT" + PENDLEBTC = "PENDLEBTC" + PENDLETUSD = "PENDLETUSD" + PENDLEUSDT = "PENDLEUSDT" PEOPLEBNB = "PEOPLEBNB" PEOPLEBTC = "PEOPLEBTC" PEOPLEBUSD = "PEOPLEBUSD" PEOPLEETH = "PEOPLEETH" PEOPLEUSDT = "PEOPLEUSDT" + PEPETRY = "PEPETRY" + PEPETUSD = "PEPETUSD" + PEPEUSDT = "PEPEUSDT" PERLBNB = "PERLBNB" PERLBTC = "PERLBTC" PERLUSDC = "PERLUSDC" @@ -1582,6 +1615,7 @@ const ( RADBNB = "RADBNB" RADBTC = "RADBTC" RADBUSD = "RADBUSD" + RADTRY = "RADTRY" RADUSDT = "RADUSDT" RAMPBTC = "RAMPBTC" RAMPBUSD = "RAMPBUSD" @@ -1640,6 +1674,7 @@ const ( RLYUSDT = "RLYUSDT" RNDRBTC = "RNDRBTC" RNDRBUSD = "RNDRBUSD" + RNDRTRY = "RNDRTRY" RNDRUSDT = "RNDRUSDT" ROSEBNB = "ROSEBNB" ROSEBTC = "ROSEBTC" @@ -1729,6 +1764,7 @@ const ( SNTBTC = "SNTBTC" SNTBUSD = "SNTBUSD" SNTETH = "SNTETH" + SNTUSDT = "SNTUSDT" SNXBNB = "SNXBNB" SNXBTC = "SNXBTC" SNXBUSD = "SNXBUSD" @@ -1806,6 +1842,12 @@ const ( STXUSDT = "STXUSDT" SUBBTC = "SUBBTC" SUBETH = "SUBETH" + SUIBNB = "SUIBNB" + SUIBTC = "SUIBTC" + SUIEUR = "SUIEUR" + SUITRY = "SUITRY" + SUITUSD = "SUITUSD" + SUIUSDT = "SUIUSDT" SUNBTC = "SUNBTC" SUNBUSD = "SUNBUSD" SUNUSDT = "SUNUSDT" @@ -1955,6 +1997,7 @@ const ( USDSTUSD = "USDSTUSD" USDSUSDC = "USDSUSDC" USDSUSDT = "USDSUSDT" + USDTARS = "USDTARS" USDTBIDR = "USDTBIDR" USDTBKRW = "USDTBKRW" USDTBRL = "USDTBRL" @@ -2038,9 +2081,11 @@ const ( WAXPBTC = "WAXPBTC" WAXPBUSD = "WAXPBUSD" WAXPUSDT = "WAXPUSDT" + WBETHETH = "WBETHETH" WBTCBTC = "WBTCBTC" WBTCBUSD = "WBTCBUSD" WBTCETH = "WBTCETH" + WBTCUSDT = "WBTCUSDT" WINBNB = "WINBNB" WINBRL = "WINBRL" WINBTC = "WINBTC" @@ -2189,7 +2234,7 @@ const ( ZRXUSDT = "ZRXUSDT" ) var symbols = []string{ - AAVEBKRW,AAVEBNB,AAVEBRL,AAVEBTC,AAVEBUSD,AAVEETH,AAVEUSDT,ACABTC,ACABUSD,ACAUSDT,ACHBTC,ACHBUSD,ACHTRY,ACHUSDT,ACMBTC,ACMBUSD,ACMUSDT,ADAAUD,ADABIDR,ADABKRW,ADABNB,ADABRL,ADABTC,ADABUSD,ADAETH,ADAEUR,ADAGBP,ADAPAX,ADARUB,ADATRY,ADATUSD,ADATWD,ADAUSDC,ADAUSDT,ADXBNB,ADXBTC,ADXBUSD,ADXETH,ADXUSDT,AEBNB,AEBTC,AEETH,AERGOBTC,AERGOBUSD,AGIBNB,AGIBTC,AGIETH,AGIXBTC,AGIXBUSD,AGIXTRY,AGIXUSDT,AGLDBNB,AGLDBTC,AGLDBUSD,AGLDUSDT,AIONBNB,AIONBTC,AIONBUSD,AIONETH,AIONUSDT,AKROBTC,AKROBUSD,AKROUSDT,ALCXBTC,ALCXBUSD,ALCXUSDT,ALGOBIDR,ALGOBNB,ALGOBTC,ALGOBUSD,ALGOETH,ALGOPAX,ALGORUB,ALGOTRY,ALGOTUSD,ALGOUSDC,ALGOUSDT,ALICEBIDR,ALICEBNB,ALICEBTC,ALICEBUSD,ALICETRY,ALICETWD,ALICEUSDT,ALPACABNB,ALPACABTC,ALPACABUSD,ALPACAUSDT,ALPHABNB,ALPHABTC,ALPHABUSD,ALPHAUSDT,ALPINEBTC,ALPINEBUSD,ALPINEEUR,ALPINETRY,ALPINEUSDT,AMBBNB,AMBBTC,AMBBUSD,AMBETH,AMBUSDT,AMPBNB,AMPBTC,AMPBUSD,AMPUSDT,ANCBNB,ANCBTC,ANCBUSD,ANCUSDT,ANKRBNB,ANKRBTC,ANKRBUSD,ANKRPAX,ANKRTRY,ANKRTUSD,ANKRUSDC,ANKRUSDT,ANTBNB,ANTBTC,ANTBUSD,ANTUSDT,ANYBTC,ANYBUSD,ANYUSDT,APEAUD,APEBNB,APEBRL,APEBTC,APEBUSD,APEETH,APEEUR,APEGBP,APETRY,APETWD,APEUSDT,API3BNB,API3BTC,API3BUSD,API3TRY,API3USDT,APPCBNB,APPCBTC,APPCETH,APTBRL,APTBTC,APTBUSD,APTETH,APTEUR,APTTRY,APTUSDT,ARBBTC,ARBEUR,ARBNB,ARBRUB,ARBTC,ARBTRY,ARBTUSD,ARBUSD,ARBUSDT,ARDRBNB,ARDRBTC,ARDRETH,ARDRUSDT,ARKBTC,ARKBUSD,ARKETH,ARNBTC,ARNETH,ARPABNB,ARPABTC,ARPABUSD,ARPAETH,ARPARUB,ARPATRY,ARPAUSDT,ARUSDT,ASRBTC,ASRBUSD,ASRUSDT,ASTBTC,ASTETH,ASTRBTC,ASTRBUSD,ASTRETH,ASTRUSDT,ATABNB,ATABTC,ATABUSD,ATAUSDT,ATMBTC,ATMBUSD,ATMUSDT,ATOMBIDR,ATOMBNB,ATOMBRL,ATOMBTC,ATOMBUSD,ATOMETH,ATOMEUR,ATOMPAX,ATOMTRY,ATOMTUSD,ATOMUSDC,ATOMUSDT,AUCTIONBTC,AUCTIONBUSD,AUCTIONUSDT,AUDBUSD,AUDIOBTC,AUDIOBUSD,AUDIOTRY,AUDIOUSDT,AUDUSDC,AUDUSDT,AUTOBTC,AUTOBUSD,AUTOUSDT,AVABNB,AVABTC,AVABUSD,AVAUSDT,AVAXAUD,AVAXBIDR,AVAXBNB,AVAXBRL,AVAXBTC,AVAXBUSD,AVAXETH,AVAXEUR,AVAXGBP,AVAXTRY,AVAXUSDT,AXSAUD,AXSBNB,AXSBRL,AXSBTC,AXSBUSD,AXSETH,AXSTRY,AXSUSDT,BADGERBTC,BADGERBUSD,BADGERUSDT,BAKEBNB,BAKEBTC,BAKEBUSD,BAKEUSDT,BALBNB,BALBTC,BALBUSD,BALUSDT,BANDBNB,BANDBTC,BANDBUSD,BANDUSDT,BARBTC,BARBUSD,BARUSDT,BATBNB,BATBTC,BATBUSD,BATETH,BATPAX,BATTUSD,BATUSDC,BATUSDT,BCCBNB,BCCBTC,BCCETH,BCCUSDT,BCDBTC,BCDETH,BCHABCBTC,BCHABCBUSD,BCHABCPAX,BCHABCTUSD,BCHABCUSDC,BCHABCUSDT,BCHABUSD,BCHBNB,BCHBTC,BCHBUSD,BCHEUR,BCHPAX,BCHSVBTC,BCHSVPAX,BCHSVTUSD,BCHSVUSDC,BCHSVUSDT,BCHTUSD,BCHTWD,BCHUSDC,BCHUSDT,BCNBNB,BCNBTC,BCNETH,BCNTTWD,BCNTUSDT,BCPTBNB,BCPTBTC,BCPTETH,BCPTPAX,BCPTTUSD,BCPTUSDC,BDOTDOT,BEAMBNB,BEAMBTC,BEAMUSDT,BEARBUSD,BEARUSDT,BELBNB,BELBTC,BELBUSD,BELETH,BELTRY,BELUSDT,BETABNB,BETABTC,BETABUSD,BETAETH,BETAUSDT,BETHBUSD,BETHETH,BETHUSDT,BGBPUSDC,BICOBTC,BICOBUSD,BICOUSDT,BIFIBNB,BIFIBUSD,BIFIUSDT,BKRWBUSD,BKRWUSDT,BLZBNB,BLZBTC,BLZBUSD,BLZETH,BLZUSDT,BNBAUD,BNBBEARBUSD,BNBBEARUSDT,BNBBIDR,BNBBKRW,BNBBRL,BNBBTC,BNBBULLBUSD,BNBBULLUSDT,BNBBUSD,BNBDAI,BNBETH,BNBEUR,BNBGBP,BNBIDRT,BNBNGN,BNBPAX,BNBRUB,BNBTRY,BNBTUSD,BNBTWD,BNBUAH,BNBUSDC,BNBUSDP,BNBUSDS,BNBUSDT,BNBUST,BNBZAR,BNTBTC,BNTBUSD,BNTETH,BNTUSDT,BNXBNB,BNXBTC,BNXBUSD,BNXUSDT,BONDBNB,BONDBTC,BONDBUSD,BONDETH,BONDUSDT,BOTBTC,BOTBUSD,BQXBTC,BQXETH,BRDBNB,BRDBTC,BRDETH,BSWBNB,BSWBUSD,BSWETH,BSWTRY,BSWUSDT,BTCAUD,BTCBBTC,BTCBIDR,BTCBKRW,BTCBRL,BTCBUSD,BTCDAI,BTCEUR,BTCGBP,BTCIDRT,BTCNGN,BTCPAX,BTCPLN,BTCRON,BTCRUB,BTCSTBTC,BTCSTBUSD,BTCSTUSDT,BTCTRY,BTCTUSD,BTCTWD,BTCUAH,BTCUSDC,BTCUSDP,BTCUSDS,BTCUSDT,BTCUST,BTCVAI,BTCZAR,BTGBTC,BTGBUSD,BTGETH,BTGUSDT,BTSBNB,BTSBTC,BTSBUSD,BTSETH,BTSUSDT,BTTBNB,BTTBRL,BTTBTC,BTTBUSD,BTTCBUSD,BTTCTRY,BTTCUSDC,BTTCUSDT,BTTEUR,BTTPAX,BTTTRX,BTTTRY,BTTTUSD,BTTUSDC,BTTUSDT,BULLBUSD,BULLUSDT,BURGERBNB,BURGERBUSD,BURGERETH,BURGERUSDT,BUSDBIDR,BUSDBKRW,BUSDBRL,BUSDBVND,BUSDDAI,BUSDIDRT,BUSDNGN,BUSDPLN,BUSDRON,BUSDRUB,BUSDTRY,BUSDUAH,BUSDUSDT,BUSDVAI,BUSDZAR,BZRXBNB,BZRXBTC,BZRXBUSD,BZRXUSDT,C98BNB,C98BRL,C98BTC,C98BUSD,C98USDT,CAKEAUD,CAKEBNB,CAKEBRL,CAKEBTC,CAKEBUSD,CAKEGBP,CAKEUSDT,CDTBTC,CDTETH,CELOBTC,CELOBUSD,CELOUSDT,CELRBNB,CELRBTC,CELRBUSD,CELRETH,CELRUSDT,CFXBTC,CFXBUSD,CFXTRY,CFXUSDT,CHATBTC,CHATETH,CHESSBNB,CHESSBTC,CHESSBUSD,CHESSUSDT,CHRBNB,CHRBTC,CHRBUSD,CHRETH,CHRUSDT,CHZBNB,CHZBRL,CHZBTC,CHZBUSD,CHZEUR,CHZGBP,CHZTRY,CHZUSDT,CITYBNB,CITYBTC,CITYBUSD,CITYUSDT,CKBBTC,CKBBUSD,CKBUSDT,CLOAKBTC,CLOAKETH,CLVBNB,CLVBTC,CLVBUSD,CLVUSDT,CMTBNB,CMTBTC,CMTETH,CNDBNB,CNDBTC,CNDETH,COCOSBNB,COCOSBTC,COCOSBUSD,COCOSTRY,COCOSUSDT,COMPBNB,COMPBTC,COMPBUSD,COMPTWD,COMPUSDT,COSBNB,COSBTC,COSBUSD,COSTRY,COSUSDT,COTIBNB,COTIBTC,COTIBUSD,COTIUSDT,COVERBUSD,COVERETH,CREAMBNB,CREAMBUSD,CRVBNB,CRVBTC,CRVBUSD,CRVETH,CRVUSDT,CTKBNB,CTKBTC,CTKBUSD,CTKUSDT,CTSIBNB,CTSIBTC,CTSIBUSD,CTSIUSDT,CTXCBNB,CTXCBTC,CTXCBUSD,CTXCUSDT,CVCBNB,CVCBTC,CVCBUSD,CVCETH,CVCUSDT,CVPBUSD,CVPETH,CVPUSDT,CVXBTC,CVXBUSD,CVXUSDT,DAIBNB,DAIBTC,DAIBUSD,DAIUSDT,DARBNB,DARBTC,DARBUSD,DARETH,DAREUR,DARTRY,DARUSDT,DASHBNB,DASHBTC,DASHBUSD,DASHETH,DASHUSDT,DATABTC,DATABUSD,DATAETH,DATAUSDT,DCRBNB,DCRBTC,DCRBUSD,DCRUSDT,DEGOBTC,DEGOBUSD,DEGOUSDT,DENTBTC,DENTBUSD,DENTETH,DENTTRY,DENTUSDT,DEXEBUSD,DEXEETH,DEXEUSDT,DFBUSD,DFETH,DFUSDT,DGBBTC,DGBBUSD,DGBUSDT,DGDBTC,DGDETH,DIABNB,DIABTC,DIABUSD,DIAUSDT,DLTBNB,DLTBTC,DLTETH,DNTBTC,DNTBUSD,DNTETH,DNTUSDT,DOCKBTC,DOCKBUSD,DOCKETH,DOCKUSDT,DODOBTC,DODOBUSD,DODOUSDT,DOGEAUD,DOGEBIDR,DOGEBNB,DOGEBRL,DOGEBTC,DOGEBUSD,DOGEEUR,DOGEGBP,DOGEPAX,DOGERUB,DOGETRY,DOGETWD,DOGEUSDC,DOGEUSDT,DOTAUD,DOTBIDR,DOTBKRW,DOTBNB,DOTBRL,DOTBTC,DOTBUSD,DOTETH,DOTEUR,DOTGBP,DOTNGN,DOTRUB,DOTTRY,DOTTWD,DOTUSDT,DREPBNB,DREPBTC,DREPBUSD,DREPUSDT,DUSKBNB,DUSKBTC,DUSKBUSD,DUSKPAX,DUSKUSDC,DUSKUSDT,DYDXBNB,DYDXBTC,DYDXBUSD,DYDXETH,DYDXUSDT,EASYBTC,EASYETH,EDOBTC,EDOETH,EGLDBNB,EGLDBTC,EGLDBUSD,EGLDETH,EGLDEUR,EGLDRON,EGLDUSDT,ELFBTC,ELFBUSD,ELFETH,ELFUSDT,ENGBTC,ENGETH,ENJBNB,ENJBRL,ENJBTC,ENJBUSD,ENJETH,ENJEUR,ENJGBP,ENJTRY,ENJUSDT,ENSBNB,ENSBTC,ENSBUSD,ENSTRY,ENSTWD,ENSUSDT,EOSAUD,EOSBEARBUSD,EOSBEARUSDT,EOSBNB,EOSBTC,EOSBULLBUSD,EOSBULLUSDT,EOSBUSD,EOSETH,EOSEUR,EOSPAX,EOSTRY,EOSTUSD,EOSUSDC,EOSUSDT,EPSBTC,EPSBUSD,EPSUSDT,EPXBUSD,EPXUSDT,ERDBNB,ERDBTC,ERDBUSD,ERDPAX,ERDUSDC,ERDUSDT,ERNBNB,ERNBUSD,ERNUSDT,ETCBNB,ETCBRL,ETCBTC,ETCBUSD,ETCETH,ETCEUR,ETCGBP,ETCPAX,ETCTRY,ETCTUSD,ETCTWD,ETCUSDC,ETCUSDT,ETCUSDTETHBTC,ETHAUD,ETHBEARBUSD,ETHBEARUSDT,ETHBIDR,ETHBKRW,ETHBRL,ETHBTC,ETHBULLBUSD,ETHBULLUSDT,ETHBUSD,ETHDAI,ETHEUR,ETHGBP,ETHNGN,ETHPAX,ETHPLN,ETHRUB,ETHTRY,ETHTUSD,ETHTWD,ETHUAH,ETHUSDC,ETHUSDP,ETHUSDT,ETHUST,ETHZAR,EURBUSD,EURUSDT,EVXBTC,EVXETH,FARMBNB,FARMBTC,FARMBUSD,FARMETH,FARMUSDT,FETBNB,FETBTC,FETBUSD,FETTRY,FETUSDT,FIDABNB,FIDABTC,FIDABUSD,FIDAUSDT,FILBNB,FILBTC,FILBUSD,FILETH,FILTRY,FILUSDT,FIOBNB,FIOBTC,FIOBUSD,FIOUSDT,FIROBTC,FIROBUSD,FIROETH,FIROUSDT,FISBIDR,FISBRL,FISBTC,FISBUSD,FISTRY,FISUSDT,FLMBNB,FLMBTC,FLMBUSD,FLMUSDT,FLOWBNB,FLOWBTC,FLOWBUSD,FLOWUSDT,FLUXBTC,FLUXBUSD,FLUXUSDT,FORBNB,FORBTC,FORBUSD,FORTHBTC,FORTHBUSD,FORTHUSDT,FORUSDT,FRONTBTC,FRONTBUSD,FRONTETH,FRONTUSDT,FTMAUD,FTMBIDR,FTMBNB,FTMBRL,FTMBTC,FTMBUSD,FTMETH,FTMEUR,FTMPAX,FTMRUB,FTMTRY,FTMTUSD,FTMUSDC,FTMUSDT,FTTBNB,FTTBTC,FTTBUSD,FTTETH,FTTUSDT,FUELBTC,FUELETH,FUNBNB,FUNBTC,FUNETH,FUNUSDT,FXSBTC,FXSBUSD,FXSUSDT,GALAAUD,GALABNB,GALABRL,GALABTC,GALABUSD,GALAETH,GALAEUR,GALATRY,GALATWD,GALAUSDT,GALBNB,GALBRL,GALBTC,GALBUSD,GALETH,GALEUR,GALTRY,GALUSDT,GASBTC,GASBUSD,GASUSDT,GBPBUSD,GBPUSDT,GFTBUSD,GHSTBUSD,GHSTETH,GHSTUSDT,GLMBTC,GLMBUSD,GLMETH,GLMRBNB,GLMRBTC,GLMRBUSD,GLMRUSDT,GLMUSDT,GMTAUD,GMTBNB,GMTBRL,GMTBTC,GMTBUSD,GMTETH,GMTEUR,GMTGBP,GMTTRY,GMTTWD,GMTUSDT,GMXBTC,GMXBUSD,GMXUSDT,GNOBNB,GNOBTC,GNOBUSD,GNOUSDT,GNSBTC,GNSUSDT,GNTBNB,GNTBTC,GNTETH,GOBNB,GOBTC,GRSBTC,GRSETH,GRTBTC,GRTBUSD,GRTETH,GRTEUR,GRTTRY,GRTTWD,GRTUSDT,GSTTWD,GTCBNB,GTCBTC,GTCBUSD,GTCUSDT,GTOBNB,GTOBTC,GTOBUSD,GTOETH,GTOPAX,GTOTUSD,GTOUSDC,GTOUSDT,GVTBTC,GVTETH,GXSBNB,GXSBTC,GXSETH,GXSUSDT,HARDBNB,HARDBTC,HARDBUSD,HARDUSDT,HBARBNB,HBARBTC,HBARBUSD,HBARUSDT,HCBTC,HCETH,HCUSDT,HEGICBUSD,HEGICETH,HFTBTC,HFTBUSD,HFTUSDT,HIFIETH,HIFIUSDT,HIGHBNB,HIGHBTC,HIGHBUSD,HIGHUSDT,HIVEBNB,HIVEBTC,HIVEBUSD,HIVEUSDT,HNTBTC,HNTBUSD,HNTUSDT,HOOKBNB,HOOKBTC,HOOKBUSD,HOOKUSDT,HOTBNB,HOTBRL,HOTBTC,HOTBUSD,HOTETH,HOTEUR,HOTTRY,HOTUSDT,HSRBTC,HSRETH,ICNBTC,ICNETH,ICPBNB,ICPBTC,ICPBUSD,ICPETH,ICPEUR,ICPRUB,ICPTRY,ICPUSDT,ICXBNB,ICXBTC,ICXBUSD,ICXETH,ICXUSDT,IDBNB,IDBTC,IDEUR,IDEXBNB,IDEXBTC,IDEXBUSD,IDEXUSDT,IDTRY,IDTUSD,IDUSDT,ILVBNB,ILVBTC,ILVBUSD,ILVUSDT,IMXBNB,IMXBTC,IMXBUSD,IMXUSDT,INJBNB,INJBTC,INJBUSD,INJTRY,INJUSDT,INSBTC,INSETH,IOSTBTC,IOSTBUSD,IOSTETH,IOSTUSDT,IOTABNB,IOTABTC,IOTABUSD,IOTAETH,IOTAUSDT,IOTXBTC,IOTXBUSD,IOTXETH,IOTXUSDT,IQBNB,IQBUSD,IRISBNB,IRISBTC,IRISBUSD,IRISUSDT,JASMYBNB,JASMYBTC,JASMYBUSD,JASMYETH,JASMYEUR,JASMYTRY,JASMYUSDT,JOEBTC,JOEBUSD,JOETRY,JOEUSDT,JSTBNB,JSTBTC,JSTBUSD,JSTUSDT,JUVBTC,JUVBUSD,JUVUSDT,KAVABNB,KAVABTC,KAVABUSD,KAVAETH,KAVAUSDT,KDABTC,KDABUSD,KDAUSDT,KEEPBNB,KEEPBTC,KEEPBUSD,KEEPUSDT,KEYBTC,KEYBUSD,KEYETH,KEYUSDT,KLAYBNB,KLAYBTC,KLAYBUSD,KLAYUSDT,KMDBTC,KMDBUSD,KMDETH,KMDUSDT,KNCBNB,KNCBTC,KNCBUSD,KNCETH,KNCUSDT,KP3RBNB,KP3RBUSD,KP3RUSDT,KSMAUD,KSMBNB,KSMBTC,KSMBUSD,KSMETH,KSMUSDT,LAZIOBTC,LAZIOBUSD,LAZIOEUR,LAZIOTRY,LAZIOUSDT,LDOBTC,LDOBUSD,LDOTUSD,LDOUSDT,LENDBKRW,LENDBTC,LENDBUSD,LENDETH,LENDUSDT,LEVERBUSD,LEVERUSDT,LINABNB,LINABTC,LINABUSD,LINAUSDT,LINKAUD,LINKBKRW,LINKBNB,LINKBRL,LINKBTC,LINKBUSD,LINKETH,LINKEUR,LINKGBP,LINKNGN,LINKPAX,LINKTRY,LINKTUSD,LINKTWD,LINKUSDC,LINKUSDT,LITBTC,LITBUSD,LITETH,LITUSDT,LOKABNB,LOKABTC,LOKABUSD,LOKAUSDT,LOOKSTWD,LOOMBNB,LOOMBTC,LOOMBUSD,LOOMETH,LOOMUSDT,LOOTTWD,LOOTUSDT,LPTBNB,LPTBTC,LPTBUSD,LPTUSDT,LQTYBTC,LQTYUSDT,LRCBNB,LRCBTC,LRCBUSD,LRCETH,LRCTRY,LRCUSDT,LSKBNB,LSKBTC,LSKBUSD,LSKETH,LSKUSDT,LTCBNB,LTCBRL,LTCBTC,LTCBUSD,LTCETH,LTCEUR,LTCGBP,LTCNGN,LTCPAX,LTCRUB,LTCTUSD,LTCTWD,LTCUAH,LTCUSDC,LTCUSDT,LTOBNB,LTOBTC,LTOBUSD,LTOUSDT,LUNAAUD,LUNABIDR,LUNABNB,LUNABRL,LUNABTC,LUNABUSD,LUNAETH,LUNAEUR,LUNAGBP,LUNATRY,LUNAUSDT,LUNAUST,LUNBTC,LUNCBUSD,LUNCUSDT,LUNETH,MAGICBTC,MAGICBUSD,MAGICTRY,MAGICUSDT,MANABIDR,MANABNB,MANABRL,MANABTC,MANABUSD,MANAETH,MANATRY,MANATWD,MANAUSDT,MASKBNB,MASKBUSD,MASKUSDT,MATICAUD,MATICBIDR,MATICBNB,MATICBRL,MATICBTC,MATICBUSD,MATICETH,MATICEUR,MATICGBP,MATICRUB,MATICTRY,MATICTUSD,MATICTWD,MATICUSDT,MAXTWD,MAXUSDT,MBLBNB,MBLBTC,MBLBUSD,MBLUSDT,MBOXBNB,MBOXBTC,MBOXBUSD,MBOXTRY,MBOXUSDT,MCBNB,MCBTC,MCBUSD,MCOBNB,MCOBTC,MCOETH,MCOUSDT,MCUSDT,MDABTC,MDAETH,MDTBNB,MDTBTC,MDTBUSD,MDTUSDT,MDXBNB,MDXBTC,MDXBUSD,MDXUSDT,MFTBNB,MFTBTC,MFTETH,MFTUSDT,MINABNB,MINABTC,MINABUSD,MINATRY,MINAUSDT,MIRBTC,MIRBUSD,MIRUSDT,MITHBNB,MITHBTC,MITHUSDT,MKRBNB,MKRBTC,MKRBUSD,MKRUSDT,MLNBNB,MLNBTC,MLNBUSD,MLNUSDT,MOBBTC,MOBBUSD,MOBUSDT,MODBTC,MODETH,MOVRBNB,MOVRBTC,MOVRBUSD,MOVRUSDT,MTHBTC,MTHETH,MTLBTC,MTLBUSD,MTLETH,MTLUSDT,MULTIBTC,MULTIBUSD,MULTIUSDT,NANOBNB,NANOBTC,NANOBUSD,NANOETH,NANOUSDT,NASBNB,NASBTC,NASETH,NAVBNB,NAVBTC,NAVETH,NBSBTC,NBSUSDT,NCASHBNB,NCASHBTC,NCASHETH,NEARBNB,NEARBTC,NEARBUSD,NEARETH,NEAREUR,NEARRUB,NEARTRY,NEARUSDT,NEBLBNB,NEBLBTC,NEBLBUSD,NEBLUSDT,NEOBNB,NEOBTC,NEOBUSD,NEOETH,NEOPAX,NEORUB,NEOTRY,NEOTUSD,NEOUSDC,NEOUSDT,NEXOBTC,NEXOBUSD,NEXOUSDT,NKNBNB,NKNBTC,NKNBUSD,NKNUSDT,NMRBTC,NMRBUSD,NMRUSDT,NPXSBTC,NPXSETH,NPXSUSDC,NPXSUSDT,NUAUD,NUBNB,NUBTC,NUBUSD,NULSBNB,NULSBTC,NULSBUSD,NULSETH,NULSUSDT,NURUB,NUUSDT,NXSBNB,NXSBTC,NXSETH,OAXBTC,OAXETH,OAXUSDT,OCEANBNB,OCEANBTC,OCEANBUSD,OCEANUSDT,OGBTC,OGBUSD,OGNBNB,OGNBTC,OGNBUSD,OGNUSDT,OGUSDT,OMBTC,OMBUSD,OMGBNB,OMGBTC,OMGBUSD,OMGETH,OMGUSDT,OMUSDT,ONEBIDR,ONEBNB,ONEBTC,ONEBUSD,ONEETH,ONEPAX,ONETRY,ONETUSD,ONEUSDC,ONEUSDT,ONGBNB,ONGBTC,ONGUSDT,ONTBNB,ONTBTC,ONTBUSD,ONTETH,ONTPAX,ONTTRY,ONTUSDC,ONTUSDT,OOKIBNB,OOKIBUSD,OOKIETH,OOKIUSDT,OPBNB,OPBTC,OPBUSD,OPETH,OPEUR,OPTUSD,OPUSDT,ORNBTC,ORNBUSD,ORNUSDT,OSMOBTC,OSMOBUSD,OSMOUSDT,OSTBNB,OSTBTC,OSTETH,OXTBTC,OXTBUSD,OXTUSDT,PAXBNB,PAXBTC,PAXBUSD,PAXETH,PAXGBNB,PAXGBTC,PAXGBUSD,PAXGUSDT,PAXTUSD,PAXUSDT,PEOPLEBNB,PEOPLEBTC,PEOPLEBUSD,PEOPLEETH,PEOPLEUSDT,PERLBNB,PERLBTC,PERLUSDC,PERLUSDT,PERPBTC,PERPBUSD,PERPUSDT,PHABTC,PHABUSD,PHAUSDT,PHBBNB,PHBBTC,PHBBUSD,PHBPAX,PHBTUSD,PHBUSDC,PHBUSDT,PHXBNB,PHXBTC,PHXETH,PIVXBNB,PIVXBTC,PLABNB,PLABTC,PLABUSD,PLAUSDT,PNTBTC,PNTUSDT,POABNB,POABTC,POAETH,POEBTC,POEETH,POLSBNB,POLSBTC,POLSBUSD,POLSUSDT,POLYBNB,POLYBTC,POLYBUSD,POLYUSDT,POLYXBTC,POLYXBUSD,POLYXUSDT,PONDBTC,PONDBUSD,PONDUSDT,PORTOBTC,PORTOBUSD,PORTOEUR,PORTOTRY,PORTOUSDT,POWRBNB,POWRBTC,POWRBUSD,POWRETH,POWRUSDT,PPTBTC,PPTETH,PROMBNB,PROMBTC,PROMBUSD,PROMUSDT,PROSBUSD,PROSETH,PROSUSDT,PSGBTC,PSGBUSD,PSGUSDT,PUNDIXBUSD,PUNDIXETH,PUNDIXUSDT,PYRBTC,PYRBUSD,PYRUSDT,QIBNB,QIBTC,QIBUSD,QIUSDT,QKCBTC,QKCBUSD,QKCETH,QKCUSDT,QLCBNB,QLCBTC,QLCETH,QNTBNB,QNTBTC,QNTBUSD,QNTUSDT,QSPBNB,QSPBTC,QSPETH,QTUMBNB,QTUMBTC,QTUMBUSD,QTUMETH,QTUMUSDT,QUICKBNB,QUICKBTC,QUICKBUSD,QUICKUSDT,RADBNB,RADBTC,RADBUSD,RADUSDT,RAMPBTC,RAMPBUSD,RAMPUSDT,RAREBNB,RAREBTC,RAREBUSD,RAREUSDT,RAYBNB,RAYBUSD,RAYUSDT,RCNBNB,RCNBTC,RCNETH,RDNBNB,RDNBTC,RDNETH,RDNTBTC,RDNTTUSD,RDNTUSDT,REEFBIDR,REEFBTC,REEFBUSD,REEFTRY,REEFUSDT,REIBNB,REIBUSD,REIETH,REIUSDT,RENBNB,RENBTC,RENBTCBTC,RENBTCETH,RENBUSD,RENUSDT,REPBNB,REPBTC,REPBUSD,REPUSDT,REQBTC,REQBUSD,REQETH,REQUSDT,RGTBNB,RGTBTC,RGTBUSD,RGTUSDT,RIFBTC,RIFUSDT,RLCBNB,RLCBTC,RLCBUSD,RLCETH,RLCUSDT,RLYTWD,RLYUSDT,RNDRBTC,RNDRBUSD,RNDRUSDT,ROSEBNB,ROSEBTC,ROSEBUSD,ROSEETH,ROSETRY,ROSEUSDT,RPLBTC,RPLBUSD,RPLUSDT,RPXBNB,RPXBTC,RPXETH,RSRBNB,RSRBTC,RSRBUSD,RSRUSDT,RUNEAUD,RUNEBNB,RUNEBTC,RUNEBUSD,RUNEETH,RUNEEUR,RUNEGBP,RUNETRY,RUNEUSDT,RVNBTC,RVNBUSD,RVNTRY,RVNUSDT,SALTBTC,SALTETH,SANDAUD,SANDBIDR,SANDBNB,SANDBRL,SANDBTC,SANDBUSD,SANDETH,SANDTRY,SANDTWD,SANDUSDT,SANTOSBRL,SANTOSBTC,SANTOSBUSD,SANTOSTRY,SANTOSUSDT,SCBTC,SCBUSD,SCETH,SCRTBTC,SCRTBUSD,SCRTETH,SCRTUSDT,SCUSDT,SFPBTC,SFPBUSD,SFPUSDT,SHIBAUD,SHIBBRL,SHIBBUSD,SHIBDOGE,SHIBEUR,SHIBGBP,SHIBRUB,SHIBTRY,SHIBTWD,SHIBUAH,SHIBUSDT,SKLBTC,SKLBUSD,SKLUSDT,SKYBNB,SKYBTC,SKYETH,SLPBIDR,SLPBNB,SLPBUSD,SLPETH,SLPTRY,SLPUSDT,SNGLSBTC,SNGLSETH,SNMBTC,SNMBUSD,SNMETH,SNTBTC,SNTBUSD,SNTETH,SNXBNB,SNXBTC,SNXBUSD,SNXETH,SNXUSDT,SOLAUD,SOLBIDR,SOLBNB,SOLBRL,SOLBTC,SOLBUSD,SOLETH,SOLEUR,SOLGBP,SOLRUB,SOLTRY,SOLTUSD,SOLTWD,SOLUSDC,SOLUSDT,SPARTABNB,SPELLBNB,SPELLBTC,SPELLBUSD,SPELLTRY,SPELLUSDT,SRMBIDR,SRMBNB,SRMBTC,SRMBUSD,SRMUSDT,SSVBTC,SSVBUSD,SSVETH,SSVTUSD,SSVUSDT,STEEMBNB,STEEMBTC,STEEMBUSD,STEEMETH,STEEMUSDT,STGBTC,STGBUSD,STGUSDT,STMXBTC,STMXBUSD,STMXETH,STMXUSDT,STORJBTC,STORJBUSD,STORJETH,STORJTRY,STORJUSDT,STORMBNB,STORMBTC,STORMETH,STORMUSDT,STPTBNB,STPTBTC,STPTBUSD,STPTUSDT,STRATBNB,STRATBTC,STRATBUSD,STRATETH,STRATUSDT,STRAXBTC,STRAXBUSD,STRAXETH,STRAXUSDT,STXBNB,STXBTC,STXBUSD,STXTRY,STXUSDT,SUBBTC,SUBETH,SUNBTC,SUNBUSD,SUNUSDT,SUPERBTC,SUPERBUSD,SUPERUSDT,SUSDBTC,SUSDETH,SUSDUSDT,SUSHIBNB,SUSHIBTC,SUSHIBUSD,SUSHIUSDT,SWRVBNB,SWRVBUSD,SXPAUD,SXPBIDR,SXPBNB,SXPBTC,SXPBUSD,SXPEUR,SXPGBP,SXPTRY,SXPUSDT,SYNBTC,SYNUSDT,SYSBNB,SYSBTC,SYSBUSD,SYSETH,SYSUSDT,TBUSD,TCTBNB,TCTBTC,TCTUSDT,TFUELBNB,TFUELBTC,TFUELBUSD,TFUELPAX,TFUELTUSD,TFUELUSDC,TFUELUSDT,THETABNB,THETABTC,THETABUSD,THETAETH,THETAEUR,THETAUSDT,TKOBIDR,TKOBTC,TKOBUSD,TKOUSDT,TLMBNB,TLMBTC,TLMBUSD,TLMTRY,TLMUSDT,TNBBTC,TNBETH,TNTBTC,TNTETH,TOMOBNB,TOMOBTC,TOMOBUSD,TOMOUSDC,TOMOUSDT,TORNBNB,TORNBTC,TORNBUSD,TORNUSDT,TRBBNB,TRBBTC,TRBBUSD,TRBUSDT,TRIBEBNB,TRIBEBTC,TRIBEBUSD,TRIBEUSDT,TRIGBNB,TRIGBTC,TRIGETH,TROYBNB,TROYBTC,TROYBUSD,TROYUSDT,TRUBTC,TRUBUSD,TRURUB,TRUUSDT,TRXAUD,TRXBNB,TRXBTC,TRXBUSD,TRXETH,TRXEUR,TRXNGN,TRXPAX,TRXTRY,TRXTUSD,TRXUSDC,TRXUSDT,TRXXRP,TUSDBNB,TUSDBTC,TUSDBTUSD,TUSDBUSD,TUSDETH,TUSDT,TUSDUSDT,TVKBTC,TVKBUSD,TVKUSDT,TWTBTC,TWTBUSD,TWTTRY,TWTUSDT,UFTBUSD,UFTETH,UFTUSDT,UMABTC,UMABUSD,UMATRY,UMAUSDT,UNFIBNB,UNFIBTC,UNFIBUSD,UNFIETH,UNFIUSDT,UNIAUD,UNIBNB,UNIBTC,UNIBUSD,UNIETH,UNIEUR,UNIUSDT,USDCBNB,USDCBUSD,USDCPAX,USDCTUSD,USDCTWD,USDCUSDT,USDPBUSD,USDPUSDT,USDSBUSDS,USDSBUSDT,USDSPAX,USDSTUSD,USDSUSDC,USDSUSDT,USDTBIDR,USDTBKRW,USDTBRL,USDTBVND,USDTDAI,USDTIDRT,USDTNGN,USDTPLN,USDTRON,USDTRUB,USDTTRY,USDTTWD,USDTUAH,USDTZAR,USTBTC,USTBUSD,USTCBUSD,USTCUSDT,USTUSDT,UTKBTC,UTKBUSD,UTKUSDT,VENBNB,VENBTC,VENETH,VENUSDT,VETBNB,VETBTC,VETBUSD,VETETH,VETEUR,VETGBP,VETTRY,VETUSDT,VGXBTC,VGXETH,VGXUSDT,VIABNB,VIABTC,VIAETH,VIBBTC,VIBBUSD,VIBEBTC,VIBEETH,VIBETH,VIBUSDT,VIDTBTC,VIDTBUSD,VIDTUSDT,VITEBNB,VITEBTC,VITEBUSD,VITEUSDT,VOXELBNB,VOXELBTC,VOXELBUSD,VOXELETH,VOXELUSDT,VTHOBNB,VTHOBUSD,VTHOUSDT,WABIBNB,WABIBTC,WABIETH,WANBNB,WANBTC,WANETH,WANUSDT,WAVESBNB,WAVESBTC,WAVESBUSD,WAVESETH,WAVESEUR,WAVESPAX,WAVESRUB,WAVESTRY,WAVESTUSD,WAVESUSDC,WAVESUSDT,WAXPBNB,WAXPBTC,WAXPBUSD,WAXPUSDT,WBTCBTC,WBTCBUSD,WBTCETH,WINBNB,WINBRL,WINBTC,WINBUSD,WINEUR,WINGBNB,WINGBTC,WINGBUSD,WINGETH,WINGSBTC,WINGSETH,WINGUSDT,WINTRX,WINUSDC,WINUSDT,WNXMBNB,WNXMBTC,WNXMBUSD,WNXMUSDT,WOOBNB,WOOBTC,WOOBUSD,WOOUSDT,WPRBTC,WPRETH,WRXBNB,WRXBTC,WRXBUSD,WRXEUR,WRXUSDT,WTCBNB,WTCBTC,WTCETH,WTCUSDT,XECBUSD,XECUSDT,XEMBNB,XEMBTC,XEMBUSD,XEMETH,XEMUSDT,XLMBNB,XLMBTC,XLMBUSD,XLMETH,XLMEUR,XLMPAX,XLMTRY,XLMTUSD,XLMUSDC,XLMUSDT,XMRBNB,XMRBTC,XMRBUSD,XMRETH,XMRUSDT,XNOBTC,XNOBUSD,XNOETH,XNOUSDT,XRPAUD,XRPBEARBUSD,XRPBEARUSDT,XRPBIDR,XRPBKRW,XRPBNB,XRPBRL,XRPBTC,XRPBULLBUSD,XRPBULLUSDT,XRPBUSD,XRPETH,XRPEUR,XRPGBP,XRPNGN,XRPPAX,XRPRUB,XRPTRY,XRPTUSD,XRPTWD,XRPUSDC,XRPUSDT,XTZBNB,XTZBTC,XTZBUSD,XTZETH,XTZTRY,XTZTWD,XTZUSDT,XVGBTC,XVGBUSD,XVGETH,XVGUSDT,XVSBNB,XVSBTC,XVSBUSD,XVSTRY,XVSUSDT,XZCBNB,XZCBTC,XZCETH,XZCUSDT,XZCXRP,YFIBNB,YFIBTC,YFIBUSD,YFIEUR,YFIIBNB,YFIIBTC,YFIIBUSD,YFIIUSDT,YFITWD,YFIUSDT,YGGBNB,YGGBTC,YGGBUSD,YGGUSDT,YOYOBNB,YOYOBTC,YOYOETH,ZECBNB,ZECBTC,ZECBUSD,ZECETH,ZECPAX,ZECTUSD,ZECUSDC,ZECUSDT,ZENBNB,ZENBTC,ZENBUSD,ZENETH,ZENUSDT,ZILBIDR,ZILBNB,ZILBTC,ZILBUSD,ZILETH,ZILEUR,ZILTRY,ZILUSDT,ZRXBNB,ZRXBTC,ZRXBUSD,ZRXETH,ZRXUSDT, + AAVEBKRW,AAVEBNB,AAVEBRL,AAVEBTC,AAVEBUSD,AAVEETH,AAVEUSDT,ACABTC,ACABUSD,ACAUSDT,ACHBTC,ACHBUSD,ACHTRY,ACHUSDT,ACMBTC,ACMBUSD,ACMUSDT,ADAAUD,ADABIDR,ADABKRW,ADABNB,ADABRL,ADABTC,ADABUSD,ADAETH,ADAEUR,ADAGBP,ADAPAX,ADARUB,ADATRY,ADATUSD,ADATWD,ADAUSDC,ADAUSDT,ADXBNB,ADXBTC,ADXBUSD,ADXETH,ADXUSDT,AEBNB,AEBTC,AEETH,AERGOBTC,AERGOBUSD,AERGOUSDT,AGIBNB,AGIBTC,AGIETH,AGIXBTC,AGIXBUSD,AGIXTRY,AGIXUSDT,AGLDBNB,AGLDBTC,AGLDBUSD,AGLDUSDT,AIONBNB,AIONBTC,AIONBUSD,AIONETH,AIONUSDT,AKROBTC,AKROBUSD,AKROUSDT,ALCXBTC,ALCXBUSD,ALCXUSDT,ALGOBIDR,ALGOBNB,ALGOBTC,ALGOBUSD,ALGOETH,ALGOPAX,ALGORUB,ALGOTRY,ALGOTUSD,ALGOUSDC,ALGOUSDT,ALICEBIDR,ALICEBNB,ALICEBTC,ALICEBUSD,ALICETRY,ALICETWD,ALICEUSDT,ALPACABNB,ALPACABTC,ALPACABUSD,ALPACAUSDT,ALPHABNB,ALPHABTC,ALPHABUSD,ALPHAUSDT,ALPINEBTC,ALPINEBUSD,ALPINEEUR,ALPINETRY,ALPINEUSDT,AMBBNB,AMBBTC,AMBBUSD,AMBETH,AMBUSDT,AMPBNB,AMPBTC,AMPBUSD,AMPUSDT,ANCBNB,ANCBTC,ANCBUSD,ANCUSDT,ANKRBNB,ANKRBTC,ANKRBUSD,ANKRPAX,ANKRTRY,ANKRTUSD,ANKRUSDC,ANKRUSDT,ANTBNB,ANTBTC,ANTBUSD,ANTUSDT,ANYBTC,ANYBUSD,ANYUSDT,APEAUD,APEBNB,APEBRL,APEBTC,APEBUSD,APEETH,APEEUR,APEGBP,APETRY,APETWD,APEUSDT,API3BNB,API3BTC,API3BUSD,API3TRY,API3USDT,APPCBNB,APPCBTC,APPCETH,APTBRL,APTBTC,APTBUSD,APTETH,APTEUR,APTTRY,APTUSDT,ARBBTC,ARBEUR,ARBNB,ARBRUB,ARBTC,ARBTRY,ARBTUSD,ARBUSD,ARBUSDT,ARDRBNB,ARDRBTC,ARDRETH,ARDRUSDT,ARKBTC,ARKBUSD,ARKETH,ARNBTC,ARNETH,ARPABNB,ARPABTC,ARPABUSD,ARPAETH,ARPARUB,ARPATRY,ARPAUSDT,ARUSDT,ASRBTC,ASRBUSD,ASRUSDT,ASTBTC,ASTETH,ASTRBTC,ASTRBUSD,ASTRETH,ASTRUSDT,ASTUSDT,ATABNB,ATABTC,ATABUSD,ATAUSDT,ATMBTC,ATMBUSD,ATMUSDT,ATOMBIDR,ATOMBNB,ATOMBRL,ATOMBTC,ATOMBUSD,ATOMETH,ATOMEUR,ATOMPAX,ATOMTRY,ATOMTUSD,ATOMUSDC,ATOMUSDT,AUCTIONBTC,AUCTIONBUSD,AUCTIONUSDT,AUDBUSD,AUDIOBTC,AUDIOBUSD,AUDIOTRY,AUDIOUSDT,AUDUSDC,AUDUSDT,AUTOBTC,AUTOBUSD,AUTOUSDT,AVABNB,AVABTC,AVABUSD,AVAUSDT,AVAXAUD,AVAXBIDR,AVAXBNB,AVAXBRL,AVAXBTC,AVAXBUSD,AVAXETH,AVAXEUR,AVAXGBP,AVAXTRY,AVAXUSDT,AXSAUD,AXSBNB,AXSBRL,AXSBTC,AXSBUSD,AXSETH,AXSTRY,AXSUSDT,BADGERBTC,BADGERBUSD,BADGERUSDT,BAKEBNB,BAKEBTC,BAKEBUSD,BAKEUSDT,BALBNB,BALBTC,BALBUSD,BALUSDT,BANDBNB,BANDBTC,BANDBUSD,BANDUSDT,BARBTC,BARBUSD,BARUSDT,BATBNB,BATBTC,BATBUSD,BATETH,BATPAX,BATTUSD,BATUSDC,BATUSDT,BCCBNB,BCCBTC,BCCETH,BCCUSDT,BCDBTC,BCDETH,BCHABCBTC,BCHABCBUSD,BCHABCPAX,BCHABCTUSD,BCHABCUSDC,BCHABCUSDT,BCHABUSD,BCHBNB,BCHBTC,BCHBUSD,BCHEUR,BCHPAX,BCHSVBTC,BCHSVPAX,BCHSVTUSD,BCHSVUSDC,BCHSVUSDT,BCHTUSD,BCHTWD,BCHUSDC,BCHUSDT,BCNBNB,BCNBTC,BCNETH,BCNTTWD,BCNTUSDT,BCPTBNB,BCPTBTC,BCPTETH,BCPTPAX,BCPTTUSD,BCPTUSDC,BDOTDOT,BEAMBNB,BEAMBTC,BEAMUSDT,BEARBUSD,BEARUSDT,BELBNB,BELBTC,BELBUSD,BELETH,BELTRY,BELUSDT,BETABNB,BETABTC,BETABUSD,BETAETH,BETAUSDT,BETHBUSD,BETHETH,BETHUSDT,BGBPUSDC,BICOBTC,BICOBUSD,BICOUSDT,BIFIBNB,BIFIBUSD,BIFIUSDT,BKRWBUSD,BKRWUSDT,BLZBNB,BLZBTC,BLZBUSD,BLZETH,BLZUSDT,BNBAUD,BNBBEARBUSD,BNBBEARUSDT,BNBBIDR,BNBBKRW,BNBBRL,BNBBTC,BNBBULLBUSD,BNBBULLUSDT,BNBBUSD,BNBDAI,BNBETH,BNBEUR,BNBGBP,BNBIDRT,BNBNGN,BNBPAX,BNBRUB,BNBTRY,BNBTUSD,BNBTWD,BNBUAH,BNBUSDC,BNBUSDP,BNBUSDS,BNBUSDT,BNBUST,BNBZAR,BNTBTC,BNTBUSD,BNTETH,BNTUSDT,BNXBNB,BNXBTC,BNXBUSD,BNXUSDT,BONDBNB,BONDBTC,BONDBUSD,BONDETH,BONDUSDT,BOTBTC,BOTBUSD,BQXBTC,BQXETH,BRDBNB,BRDBTC,BRDETH,BSWBNB,BSWBUSD,BSWETH,BSWTRY,BSWUSDT,BTCARS,BTCAUD,BTCBBTC,BTCBIDR,BTCBKRW,BTCBRL,BTCBUSD,BTCDAI,BTCEUR,BTCGBP,BTCIDRT,BTCNGN,BTCPAX,BTCPLN,BTCRON,BTCRUB,BTCSTBTC,BTCSTBUSD,BTCSTUSDT,BTCTRY,BTCTUSD,BTCTWD,BTCUAH,BTCUSDC,BTCUSDP,BTCUSDS,BTCUSDT,BTCUST,BTCVAI,BTCZAR,BTGBTC,BTGBUSD,BTGETH,BTGUSDT,BTSBNB,BTSBTC,BTSBUSD,BTSETH,BTSUSDT,BTTBNB,BTTBRL,BTTBTC,BTTBUSD,BTTCBUSD,BTTCTRY,BTTCUSDC,BTTCUSDT,BTTEUR,BTTPAX,BTTTRX,BTTTRY,BTTTUSD,BTTUSDC,BTTUSDT,BULLBUSD,BULLUSDT,BURGERBNB,BURGERBUSD,BURGERETH,BURGERUSDT,BUSDBIDR,BUSDBKRW,BUSDBRL,BUSDBVND,BUSDDAI,BUSDIDRT,BUSDNGN,BUSDPLN,BUSDRON,BUSDRUB,BUSDTRY,BUSDUAH,BUSDUSDT,BUSDVAI,BUSDZAR,BZRXBNB,BZRXBTC,BZRXBUSD,BZRXUSDT,C98BNB,C98BRL,C98BTC,C98BUSD,C98USDT,CAKEAUD,CAKEBNB,CAKEBRL,CAKEBTC,CAKEBUSD,CAKEGBP,CAKEUSDT,CDTBTC,CDTETH,CELOBTC,CELOBUSD,CELOUSDT,CELRBNB,CELRBTC,CELRBUSD,CELRETH,CELRUSDT,CFXBTC,CFXBUSD,CFXTRY,CFXTUSD,CFXUSDT,CHATBTC,CHATETH,CHESSBNB,CHESSBTC,CHESSBUSD,CHESSUSDT,CHRBNB,CHRBTC,CHRBUSD,CHRETH,CHRUSDT,CHZBNB,CHZBRL,CHZBTC,CHZBUSD,CHZEUR,CHZGBP,CHZTRY,CHZUSDT,CITYBNB,CITYBTC,CITYBUSD,CITYTRY,CITYUSDT,CKBBTC,CKBBUSD,CKBUSDT,CLOAKBTC,CLOAKETH,CLVBNB,CLVBTC,CLVBUSD,CLVUSDT,CMTBNB,CMTBTC,CMTETH,CNDBNB,CNDBTC,CNDETH,COCOSBNB,COCOSBTC,COCOSBUSD,COCOSTRY,COCOSUSDT,COMBOBNB,COMBOTRY,COMBOUSDT,COMPBNB,COMPBTC,COMPBUSD,COMPTWD,COMPUSDT,COSBNB,COSBTC,COSBUSD,COSTRY,COSUSDT,COTIBNB,COTIBTC,COTIBUSD,COTIUSDT,COVERBUSD,COVERETH,CREAMBNB,CREAMBUSD,CRVBNB,CRVBTC,CRVBUSD,CRVETH,CRVUSDT,CTKBNB,CTKBTC,CTKBUSD,CTKUSDT,CTSIBNB,CTSIBTC,CTSIBUSD,CTSIUSDT,CTXCBNB,CTXCBTC,CTXCBUSD,CTXCUSDT,CVCBNB,CVCBTC,CVCBUSD,CVCETH,CVCUSDT,CVPBUSD,CVPETH,CVPUSDT,CVXBTC,CVXBUSD,CVXUSDT,DAIBNB,DAIBTC,DAIBUSD,DAIUSDT,DARBNB,DARBTC,DARBUSD,DARETH,DAREUR,DARTRY,DARUSDT,DASHBNB,DASHBTC,DASHBUSD,DASHETH,DASHUSDT,DATABTC,DATABUSD,DATAETH,DATAUSDT,DCRBNB,DCRBTC,DCRBUSD,DCRUSDT,DEGOBTC,DEGOBUSD,DEGOUSDT,DENTBTC,DENTBUSD,DENTETH,DENTTRY,DENTUSDT,DEXEBUSD,DEXEETH,DEXEUSDT,DFBUSD,DFETH,DFUSDT,DGBBTC,DGBBUSD,DGBUSDT,DGDBTC,DGDETH,DIABNB,DIABTC,DIABUSD,DIAUSDT,DLTBNB,DLTBTC,DLTETH,DNTBTC,DNTBUSD,DNTETH,DNTUSDT,DOCKBTC,DOCKBUSD,DOCKETH,DOCKUSDT,DODOBTC,DODOBUSD,DODOUSDT,DOGEAUD,DOGEBIDR,DOGEBNB,DOGEBRL,DOGEBTC,DOGEBUSD,DOGEEUR,DOGEGBP,DOGEPAX,DOGERUB,DOGETRY,DOGETUSD,DOGETWD,DOGEUSDC,DOGEUSDT,DOTAUD,DOTBIDR,DOTBKRW,DOTBNB,DOTBRL,DOTBTC,DOTBUSD,DOTETH,DOTEUR,DOTGBP,DOTNGN,DOTRUB,DOTTRY,DOTTWD,DOTUSDT,DREPBNB,DREPBTC,DREPBUSD,DREPUSDT,DUSKBNB,DUSKBTC,DUSKBUSD,DUSKPAX,DUSKUSDC,DUSKUSDT,DYDXBNB,DYDXBTC,DYDXBUSD,DYDXETH,DYDXUSDT,EASYBTC,EASYETH,EDOBTC,EDOETH,EDUBNB,EDUBTC,EDUEUR,EDUTRY,EDUTUSD,EDUUSDT,EGLDBNB,EGLDBTC,EGLDBUSD,EGLDETH,EGLDEUR,EGLDRON,EGLDUSDT,ELFBTC,ELFBUSD,ELFETH,ELFUSDT,ENGBTC,ENGETH,ENJBNB,ENJBRL,ENJBTC,ENJBUSD,ENJETH,ENJEUR,ENJGBP,ENJTRY,ENJUSDT,ENSBNB,ENSBTC,ENSBUSD,ENSTRY,ENSTWD,ENSUSDT,EOSAUD,EOSBEARBUSD,EOSBEARUSDT,EOSBNB,EOSBTC,EOSBULLBUSD,EOSBULLUSDT,EOSBUSD,EOSETH,EOSEUR,EOSPAX,EOSTRY,EOSTUSD,EOSUSDC,EOSUSDT,EPSBTC,EPSBUSD,EPSUSDT,EPXBUSD,EPXUSDT,ERDBNB,ERDBTC,ERDBUSD,ERDPAX,ERDUSDC,ERDUSDT,ERNBNB,ERNBUSD,ERNUSDT,ETCBNB,ETCBRL,ETCBTC,ETCBUSD,ETCETH,ETCEUR,ETCGBP,ETCPAX,ETCTRY,ETCTUSD,ETCTWD,ETCUSDC,ETCUSDT,ETCUSDTETHBTC,ETHAUD,ETHBEARBUSD,ETHBEARUSDT,ETHBIDR,ETHBKRW,ETHBRL,ETHBTC,ETHBULLBUSD,ETHBULLUSDT,ETHBUSD,ETHDAI,ETHEUR,ETHGBP,ETHNGN,ETHPAX,ETHPLN,ETHRUB,ETHTRY,ETHTUSD,ETHTWD,ETHUAH,ETHUSDC,ETHUSDP,ETHUSDT,ETHUST,ETHZAR,EURBUSD,EURUSDT,EVXBTC,EVXETH,EZBTC,EZETH,FARMBNB,FARMBTC,FARMBUSD,FARMETH,FARMUSDT,FETBNB,FETBTC,FETBUSD,FETTRY,FETUSDT,FIDABNB,FIDABTC,FIDABUSD,FIDAUSDT,FILBNB,FILBTC,FILBUSD,FILETH,FILTRY,FILUSDT,FIOBNB,FIOBTC,FIOBUSD,FIOUSDT,FIROBTC,FIROBUSD,FIROETH,FIROUSDT,FISBIDR,FISBRL,FISBTC,FISBUSD,FISTRY,FISUSDT,FLMBNB,FLMBTC,FLMBUSD,FLMUSDT,FLOKITRY,FLOKITUSD,FLOKIUSDT,FLOWBNB,FLOWBTC,FLOWBUSD,FLOWUSDT,FLUXBTC,FLUXBUSD,FLUXUSDT,FORBNB,FORBTC,FORBUSD,FORTHBTC,FORTHBUSD,FORTHUSDT,FORUSDT,FRONTBTC,FRONTBUSD,FRONTETH,FRONTUSDT,FTMAUD,FTMBIDR,FTMBNB,FTMBRL,FTMBTC,FTMBUSD,FTMETH,FTMEUR,FTMPAX,FTMRUB,FTMTRY,FTMTUSD,FTMUSDC,FTMUSDT,FTTBNB,FTTBTC,FTTBUSD,FTTETH,FTTUSDT,FUELBTC,FUELETH,FUNBNB,FUNBTC,FUNETH,FUNUSDT,FXSBTC,FXSBUSD,FXSUSDT,GALAAUD,GALABNB,GALABRL,GALABTC,GALABUSD,GALAETH,GALAEUR,GALATRY,GALATWD,GALAUSDT,GALBNB,GALBRL,GALBTC,GALBUSD,GALETH,GALEUR,GALTRY,GALUSDT,GASBTC,GASBUSD,GASUSDT,GBPBUSD,GBPUSDT,GFTBUSD,GHSTBUSD,GHSTETH,GHSTUSDT,GLMBTC,GLMBUSD,GLMETH,GLMRBNB,GLMRBTC,GLMRBUSD,GLMRUSDT,GLMUSDT,GMTAUD,GMTBNB,GMTBRL,GMTBTC,GMTBUSD,GMTETH,GMTEUR,GMTGBP,GMTTRY,GMTTWD,GMTUSDT,GMXBTC,GMXBUSD,GMXUSDT,GNOBNB,GNOBTC,GNOBUSD,GNOUSDT,GNSBTC,GNSUSDT,GNTBNB,GNTBTC,GNTETH,GOBNB,GOBTC,GRSBTC,GRSETH,GRTBTC,GRTBUSD,GRTETH,GRTEUR,GRTTRY,GRTTWD,GRTUSDT,GSTTWD,GTCBNB,GTCBTC,GTCBUSD,GTCUSDT,GTOBNB,GTOBTC,GTOBUSD,GTOETH,GTOPAX,GTOTUSD,GTOUSDC,GTOUSDT,GVTBTC,GVTETH,GXSBNB,GXSBTC,GXSETH,GXSUSDT,HARDBNB,HARDBTC,HARDBUSD,HARDUSDT,HBARBNB,HBARBTC,HBARBUSD,HBARUSDT,HCBTC,HCETH,HCUSDT,HEGICBUSD,HEGICETH,HFTBTC,HFTBUSD,HFTUSDT,HIFIETH,HIFIUSDT,HIGHBNB,HIGHBTC,HIGHBUSD,HIGHUSDT,HIVEBNB,HIVEBTC,HIVEBUSD,HIVEUSDT,HNTBTC,HNTBUSD,HNTUSDT,HOOKBNB,HOOKBTC,HOOKBUSD,HOOKUSDT,HOTBNB,HOTBRL,HOTBTC,HOTBUSD,HOTETH,HOTEUR,HOTTRY,HOTUSDT,HSRBTC,HSRETH,ICNBTC,ICNETH,ICPBNB,ICPBTC,ICPBUSD,ICPETH,ICPEUR,ICPRUB,ICPTRY,ICPUSDT,ICXBNB,ICXBTC,ICXBUSD,ICXETH,ICXUSDT,IDBNB,IDBTC,IDEUR,IDEXBNB,IDEXBTC,IDEXBUSD,IDEXUSDT,IDTRY,IDTUSD,IDUSDT,ILVBNB,ILVBTC,ILVBUSD,ILVUSDT,IMXBNB,IMXBTC,IMXBUSD,IMXUSDT,INJBNB,INJBTC,INJBUSD,INJTRY,INJUSDT,INSBTC,INSETH,IOSTBTC,IOSTBUSD,IOSTETH,IOSTUSDT,IOTABNB,IOTABTC,IOTABUSD,IOTAETH,IOTAUSDT,IOTXBTC,IOTXBUSD,IOTXETH,IOTXUSDT,IQBNB,IQBUSD,IRISBNB,IRISBTC,IRISBUSD,IRISUSDT,JASMYBNB,JASMYBTC,JASMYBUSD,JASMYETH,JASMYEUR,JASMYTRY,JASMYUSDT,JOEBTC,JOEBUSD,JOETRY,JOEUSDT,JSTBNB,JSTBTC,JSTBUSD,JSTUSDT,JUVBTC,JUVBUSD,JUVUSDT,KAVABNB,KAVABTC,KAVABUSD,KAVAETH,KAVAUSDT,KDABTC,KDABUSD,KDAUSDT,KEEPBNB,KEEPBTC,KEEPBUSD,KEEPUSDT,KEYBTC,KEYBUSD,KEYETH,KEYUSDT,KLAYBNB,KLAYBTC,KLAYBUSD,KLAYUSDT,KMDBTC,KMDBUSD,KMDETH,KMDUSDT,KNCBNB,KNCBTC,KNCBUSD,KNCETH,KNCUSDT,KP3RBNB,KP3RBUSD,KP3RUSDT,KSMAUD,KSMBNB,KSMBTC,KSMBUSD,KSMETH,KSMUSDT,LAZIOBTC,LAZIOBUSD,LAZIOEUR,LAZIOTRY,LAZIOUSDT,LDOBTC,LDOBUSD,LDOTUSD,LDOUSDT,LENDBKRW,LENDBTC,LENDBUSD,LENDETH,LENDUSDT,LEVERBUSD,LEVERUSDT,LINABNB,LINABTC,LINABUSD,LINAUSDT,LINKAUD,LINKBKRW,LINKBNB,LINKBRL,LINKBTC,LINKBUSD,LINKETH,LINKEUR,LINKGBP,LINKNGN,LINKPAX,LINKTRY,LINKTUSD,LINKTWD,LINKUSDC,LINKUSDT,LITBTC,LITBUSD,LITETH,LITUSDT,LOKABNB,LOKABTC,LOKABUSD,LOKAUSDT,LOOKSTWD,LOOMBNB,LOOMBTC,LOOMBUSD,LOOMETH,LOOMUSDT,LOOTTWD,LOOTUSDT,LPTBNB,LPTBTC,LPTBUSD,LPTUSDT,LQTYBTC,LQTYUSDT,LRCBNB,LRCBTC,LRCBUSD,LRCETH,LRCTRY,LRCUSDT,LSKBNB,LSKBTC,LSKBUSD,LSKETH,LSKUSDT,LTCBNB,LTCBRL,LTCBTC,LTCBUSD,LTCETH,LTCEUR,LTCGBP,LTCNGN,LTCPAX,LTCRUB,LTCTRY,LTCTUSD,LTCTWD,LTCUAH,LTCUSDC,LTCUSDT,LTOBNB,LTOBTC,LTOBUSD,LTOUSDT,LUNAAUD,LUNABIDR,LUNABNB,LUNABRL,LUNABTC,LUNABUSD,LUNAETH,LUNAEUR,LUNAGBP,LUNATRY,LUNAUSDT,LUNAUST,LUNBTC,LUNCBUSD,LUNCUSDT,LUNETH,MAGICBTC,MAGICBUSD,MAGICTRY,MAGICUSDT,MANABIDR,MANABNB,MANABRL,MANABTC,MANABUSD,MANAETH,MANATRY,MANATWD,MANAUSDT,MASKBNB,MASKBUSD,MASKUSDT,MATICAUD,MATICBIDR,MATICBNB,MATICBRL,MATICBTC,MATICBUSD,MATICETH,MATICEUR,MATICGBP,MATICRUB,MATICTRY,MATICTUSD,MATICTWD,MATICUSDT,MAVBTC,MAVTUSD,MAVUSDT,MAXTWD,MAXUSDT,MBLBNB,MBLBTC,MBLBUSD,MBLUSDT,MBOXBNB,MBOXBTC,MBOXBUSD,MBOXTRY,MBOXUSDT,MCBNB,MCBTC,MCBUSD,MCOBNB,MCOBTC,MCOETH,MCOUSDT,MCUSDT,MDABTC,MDAETH,MDTBNB,MDTBTC,MDTBUSD,MDTUSDT,MDXBNB,MDXBTC,MDXBUSD,MDXUSDT,MFTBNB,MFTBTC,MFTETH,MFTUSDT,MINABNB,MINABTC,MINABUSD,MINATRY,MINAUSDT,MIRBTC,MIRBUSD,MIRUSDT,MITHBNB,MITHBTC,MITHUSDT,MKRBNB,MKRBTC,MKRBUSD,MKRUSDT,MLNBNB,MLNBTC,MLNBUSD,MLNUSDT,MOBBTC,MOBBUSD,MOBUSDT,MODBTC,MODETH,MOVRBNB,MOVRBTC,MOVRBUSD,MOVRUSDT,MTHBTC,MTHETH,MTLBTC,MTLBUSD,MTLETH,MTLUSDT,MULTIBTC,MULTIBUSD,MULTIUSDT,NANOBNB,NANOBTC,NANOBUSD,NANOETH,NANOUSDT,NASBNB,NASBTC,NASETH,NAVBNB,NAVBTC,NAVETH,NBSBTC,NBSUSDT,NCASHBNB,NCASHBTC,NCASHETH,NEARBNB,NEARBTC,NEARBUSD,NEARETH,NEAREUR,NEARRUB,NEARTRY,NEARUSDT,NEBLBNB,NEBLBTC,NEBLBUSD,NEBLUSDT,NEOBNB,NEOBTC,NEOBUSD,NEOETH,NEOPAX,NEORUB,NEOTRY,NEOTUSD,NEOUSDC,NEOUSDT,NEXOBTC,NEXOBUSD,NEXOUSDT,NKNBNB,NKNBTC,NKNBUSD,NKNUSDT,NMRBTC,NMRBUSD,NMRUSDT,NPXSBTC,NPXSETH,NPXSUSDC,NPXSUSDT,NUAUD,NUBNB,NUBTC,NUBUSD,NULSBNB,NULSBTC,NULSBUSD,NULSETH,NULSUSDT,NURUB,NUUSDT,NXSBNB,NXSBTC,NXSETH,OAXBTC,OAXETH,OAXUSDT,OCEANBNB,OCEANBTC,OCEANBUSD,OCEANUSDT,OGBTC,OGBUSD,OGNBNB,OGNBTC,OGNBUSD,OGNUSDT,OGTRY,OGUSDT,OMBTC,OMBUSD,OMGBNB,OMGBTC,OMGBUSD,OMGETH,OMGUSDT,OMUSDT,ONEBIDR,ONEBNB,ONEBTC,ONEBUSD,ONEETH,ONEPAX,ONETRY,ONETUSD,ONEUSDC,ONEUSDT,ONGBNB,ONGBTC,ONGUSDT,ONTBNB,ONTBTC,ONTBUSD,ONTETH,ONTPAX,ONTTRY,ONTUSDC,ONTUSDT,OOKIBNB,OOKIBUSD,OOKIETH,OOKIUSDT,OPBNB,OPBTC,OPBUSD,OPETH,OPEUR,OPTRY,OPTUSD,OPUSDT,ORNBTC,ORNBUSD,ORNUSDT,OSMOBTC,OSMOBUSD,OSMOUSDT,OSTBNB,OSTBTC,OSTETH,OXTBTC,OXTBUSD,OXTUSDT,PAXBNB,PAXBTC,PAXBUSD,PAXETH,PAXGBNB,PAXGBTC,PAXGBUSD,PAXGTRY,PAXGUSDT,PAXTUSD,PAXUSDT,PENDLEBTC,PENDLETUSD,PENDLEUSDT,PEOPLEBNB,PEOPLEBTC,PEOPLEBUSD,PEOPLEETH,PEOPLEUSDT,PEPETRY,PEPETUSD,PEPEUSDT,PERLBNB,PERLBTC,PERLUSDC,PERLUSDT,PERPBTC,PERPBUSD,PERPUSDT,PHABTC,PHABUSD,PHAUSDT,PHBBNB,PHBBTC,PHBBUSD,PHBPAX,PHBTUSD,PHBUSDC,PHBUSDT,PHXBNB,PHXBTC,PHXETH,PIVXBNB,PIVXBTC,PLABNB,PLABTC,PLABUSD,PLAUSDT,PNTBTC,PNTUSDT,POABNB,POABTC,POAETH,POEBTC,POEETH,POLSBNB,POLSBTC,POLSBUSD,POLSUSDT,POLYBNB,POLYBTC,POLYBUSD,POLYUSDT,POLYXBTC,POLYXBUSD,POLYXUSDT,PONDBTC,PONDBUSD,PONDUSDT,PORTOBTC,PORTOBUSD,PORTOEUR,PORTOTRY,PORTOUSDT,POWRBNB,POWRBTC,POWRBUSD,POWRETH,POWRUSDT,PPTBTC,PPTETH,PROMBNB,PROMBTC,PROMBUSD,PROMUSDT,PROSBUSD,PROSETH,PROSUSDT,PSGBTC,PSGBUSD,PSGUSDT,PUNDIXBUSD,PUNDIXETH,PUNDIXUSDT,PYRBTC,PYRBUSD,PYRUSDT,QIBNB,QIBTC,QIBUSD,QIUSDT,QKCBTC,QKCBUSD,QKCETH,QKCUSDT,QLCBNB,QLCBTC,QLCETH,QNTBNB,QNTBTC,QNTBUSD,QNTUSDT,QSPBNB,QSPBTC,QSPETH,QTUMBNB,QTUMBTC,QTUMBUSD,QTUMETH,QTUMUSDT,QUICKBNB,QUICKBTC,QUICKBUSD,QUICKUSDT,RADBNB,RADBTC,RADBUSD,RADTRY,RADUSDT,RAMPBTC,RAMPBUSD,RAMPUSDT,RAREBNB,RAREBTC,RAREBUSD,RAREUSDT,RAYBNB,RAYBUSD,RAYUSDT,RCNBNB,RCNBTC,RCNETH,RDNBNB,RDNBTC,RDNETH,RDNTBTC,RDNTTUSD,RDNTUSDT,REEFBIDR,REEFBTC,REEFBUSD,REEFTRY,REEFUSDT,REIBNB,REIBUSD,REIETH,REIUSDT,RENBNB,RENBTC,RENBTCBTC,RENBTCETH,RENBUSD,RENUSDT,REPBNB,REPBTC,REPBUSD,REPUSDT,REQBTC,REQBUSD,REQETH,REQUSDT,RGTBNB,RGTBTC,RGTBUSD,RGTUSDT,RIFBTC,RIFUSDT,RLCBNB,RLCBTC,RLCBUSD,RLCETH,RLCUSDT,RLYTWD,RLYUSDT,RNDRBTC,RNDRBUSD,RNDRTRY,RNDRUSDT,ROSEBNB,ROSEBTC,ROSEBUSD,ROSEETH,ROSETRY,ROSEUSDT,RPLBTC,RPLBUSD,RPLUSDT,RPXBNB,RPXBTC,RPXETH,RSRBNB,RSRBTC,RSRBUSD,RSRUSDT,RUNEAUD,RUNEBNB,RUNEBTC,RUNEBUSD,RUNEETH,RUNEEUR,RUNEGBP,RUNETRY,RUNEUSDT,RVNBTC,RVNBUSD,RVNTRY,RVNUSDT,SALTBTC,SALTETH,SANDAUD,SANDBIDR,SANDBNB,SANDBRL,SANDBTC,SANDBUSD,SANDETH,SANDTRY,SANDTWD,SANDUSDT,SANTOSBRL,SANTOSBTC,SANTOSBUSD,SANTOSTRY,SANTOSUSDT,SCBTC,SCBUSD,SCETH,SCRTBTC,SCRTBUSD,SCRTETH,SCRTUSDT,SCUSDT,SFPBTC,SFPBUSD,SFPUSDT,SHIBAUD,SHIBBRL,SHIBBUSD,SHIBDOGE,SHIBEUR,SHIBGBP,SHIBRUB,SHIBTRY,SHIBTWD,SHIBUAH,SHIBUSDT,SKLBTC,SKLBUSD,SKLUSDT,SKYBNB,SKYBTC,SKYETH,SLPBIDR,SLPBNB,SLPBUSD,SLPETH,SLPTRY,SLPUSDT,SNGLSBTC,SNGLSETH,SNMBTC,SNMBUSD,SNMETH,SNTBTC,SNTBUSD,SNTETH,SNTUSDT,SNXBNB,SNXBTC,SNXBUSD,SNXETH,SNXUSDT,SOLAUD,SOLBIDR,SOLBNB,SOLBRL,SOLBTC,SOLBUSD,SOLETH,SOLEUR,SOLGBP,SOLRUB,SOLTRY,SOLTUSD,SOLTWD,SOLUSDC,SOLUSDT,SPARTABNB,SPELLBNB,SPELLBTC,SPELLBUSD,SPELLTRY,SPELLUSDT,SRMBIDR,SRMBNB,SRMBTC,SRMBUSD,SRMUSDT,SSVBTC,SSVBUSD,SSVETH,SSVTUSD,SSVUSDT,STEEMBNB,STEEMBTC,STEEMBUSD,STEEMETH,STEEMUSDT,STGBTC,STGBUSD,STGUSDT,STMXBTC,STMXBUSD,STMXETH,STMXUSDT,STORJBTC,STORJBUSD,STORJETH,STORJTRY,STORJUSDT,STORMBNB,STORMBTC,STORMETH,STORMUSDT,STPTBNB,STPTBTC,STPTBUSD,STPTUSDT,STRATBNB,STRATBTC,STRATBUSD,STRATETH,STRATUSDT,STRAXBTC,STRAXBUSD,STRAXETH,STRAXUSDT,STXBNB,STXBTC,STXBUSD,STXTRY,STXUSDT,SUBBTC,SUBETH,SUIBNB,SUIBTC,SUIEUR,SUITRY,SUITUSD,SUIUSDT,SUNBTC,SUNBUSD,SUNUSDT,SUPERBTC,SUPERBUSD,SUPERUSDT,SUSDBTC,SUSDETH,SUSDUSDT,SUSHIBNB,SUSHIBTC,SUSHIBUSD,SUSHIUSDT,SWRVBNB,SWRVBUSD,SXPAUD,SXPBIDR,SXPBNB,SXPBTC,SXPBUSD,SXPEUR,SXPGBP,SXPTRY,SXPUSDT,SYNBTC,SYNUSDT,SYSBNB,SYSBTC,SYSBUSD,SYSETH,SYSUSDT,TBUSD,TCTBNB,TCTBTC,TCTUSDT,TFUELBNB,TFUELBTC,TFUELBUSD,TFUELPAX,TFUELTUSD,TFUELUSDC,TFUELUSDT,THETABNB,THETABTC,THETABUSD,THETAETH,THETAEUR,THETAUSDT,TKOBIDR,TKOBTC,TKOBUSD,TKOUSDT,TLMBNB,TLMBTC,TLMBUSD,TLMTRY,TLMUSDT,TNBBTC,TNBETH,TNTBTC,TNTETH,TOMOBNB,TOMOBTC,TOMOBUSD,TOMOUSDC,TOMOUSDT,TORNBNB,TORNBTC,TORNBUSD,TORNUSDT,TRBBNB,TRBBTC,TRBBUSD,TRBUSDT,TRIBEBNB,TRIBEBTC,TRIBEBUSD,TRIBEUSDT,TRIGBNB,TRIGBTC,TRIGETH,TROYBNB,TROYBTC,TROYBUSD,TROYUSDT,TRUBTC,TRUBUSD,TRURUB,TRUUSDT,TRXAUD,TRXBNB,TRXBTC,TRXBUSD,TRXETH,TRXEUR,TRXNGN,TRXPAX,TRXTRY,TRXTUSD,TRXUSDC,TRXUSDT,TRXXRP,TUSDBNB,TUSDBTC,TUSDBTUSD,TUSDBUSD,TUSDETH,TUSDT,TUSDUSDT,TVKBTC,TVKBUSD,TVKUSDT,TWTBTC,TWTBUSD,TWTTRY,TWTUSDT,UFTBUSD,UFTETH,UFTUSDT,UMABTC,UMABUSD,UMATRY,UMAUSDT,UNFIBNB,UNFIBTC,UNFIBUSD,UNFIETH,UNFIUSDT,UNIAUD,UNIBNB,UNIBTC,UNIBUSD,UNIETH,UNIEUR,UNIUSDT,USDCBNB,USDCBUSD,USDCPAX,USDCTUSD,USDCTWD,USDCUSDT,USDPBUSD,USDPUSDT,USDSBUSDS,USDSBUSDT,USDSPAX,USDSTUSD,USDSUSDC,USDSUSDT,USDTARS,USDTBIDR,USDTBKRW,USDTBRL,USDTBVND,USDTDAI,USDTIDRT,USDTNGN,USDTPLN,USDTRON,USDTRUB,USDTTRY,USDTTWD,USDTUAH,USDTZAR,USTBTC,USTBUSD,USTCBUSD,USTCUSDT,USTUSDT,UTKBTC,UTKBUSD,UTKUSDT,VENBNB,VENBTC,VENETH,VENUSDT,VETBNB,VETBTC,VETBUSD,VETETH,VETEUR,VETGBP,VETTRY,VETUSDT,VGXBTC,VGXETH,VGXUSDT,VIABNB,VIABTC,VIAETH,VIBBTC,VIBBUSD,VIBEBTC,VIBEETH,VIBETH,VIBUSDT,VIDTBTC,VIDTBUSD,VIDTUSDT,VITEBNB,VITEBTC,VITEBUSD,VITEUSDT,VOXELBNB,VOXELBTC,VOXELBUSD,VOXELETH,VOXELUSDT,VTHOBNB,VTHOBUSD,VTHOUSDT,WABIBNB,WABIBTC,WABIETH,WANBNB,WANBTC,WANETH,WANUSDT,WAVESBNB,WAVESBTC,WAVESBUSD,WAVESETH,WAVESEUR,WAVESPAX,WAVESRUB,WAVESTRY,WAVESTUSD,WAVESUSDC,WAVESUSDT,WAXPBNB,WAXPBTC,WAXPBUSD,WAXPUSDT,WBETHETH,WBTCBTC,WBTCBUSD,WBTCETH,WBTCUSDT,WINBNB,WINBRL,WINBTC,WINBUSD,WINEUR,WINGBNB,WINGBTC,WINGBUSD,WINGETH,WINGSBTC,WINGSETH,WINGUSDT,WINTRX,WINUSDC,WINUSDT,WNXMBNB,WNXMBTC,WNXMBUSD,WNXMUSDT,WOOBNB,WOOBTC,WOOBUSD,WOOUSDT,WPRBTC,WPRETH,WRXBNB,WRXBTC,WRXBUSD,WRXEUR,WRXUSDT,WTCBNB,WTCBTC,WTCETH,WTCUSDT,XECBUSD,XECUSDT,XEMBNB,XEMBTC,XEMBUSD,XEMETH,XEMUSDT,XLMBNB,XLMBTC,XLMBUSD,XLMETH,XLMEUR,XLMPAX,XLMTRY,XLMTUSD,XLMUSDC,XLMUSDT,XMRBNB,XMRBTC,XMRBUSD,XMRETH,XMRUSDT,XNOBTC,XNOBUSD,XNOETH,XNOUSDT,XRPAUD,XRPBEARBUSD,XRPBEARUSDT,XRPBIDR,XRPBKRW,XRPBNB,XRPBRL,XRPBTC,XRPBULLBUSD,XRPBULLUSDT,XRPBUSD,XRPETH,XRPEUR,XRPGBP,XRPNGN,XRPPAX,XRPRUB,XRPTRY,XRPTUSD,XRPTWD,XRPUSDC,XRPUSDT,XTZBNB,XTZBTC,XTZBUSD,XTZETH,XTZTRY,XTZTWD,XTZUSDT,XVGBTC,XVGBUSD,XVGETH,XVGUSDT,XVSBNB,XVSBTC,XVSBUSD,XVSTRY,XVSUSDT,XZCBNB,XZCBTC,XZCETH,XZCUSDT,XZCXRP,YFIBNB,YFIBTC,YFIBUSD,YFIEUR,YFIIBNB,YFIIBTC,YFIIBUSD,YFIIUSDT,YFITWD,YFIUSDT,YGGBNB,YGGBTC,YGGBUSD,YGGUSDT,YOYOBNB,YOYOBTC,YOYOETH,ZECBNB,ZECBTC,ZECBUSD,ZECETH,ZECPAX,ZECTUSD,ZECUSDC,ZECUSDT,ZENBNB,ZENBTC,ZENBUSD,ZENETH,ZENUSDT,ZILBIDR,ZILBNB,ZILBTC,ZILBUSD,ZILETH,ZILEUR,ZILTRY,ZILUSDT,ZRXBNB,ZRXBTC,ZRXBUSD,ZRXETH,ZRXUSDT, } func toSymbol(s string) string { From d730340b7a7405ace59507bc6612594510e851b0 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 15:57:21 +0800 Subject: [PATCH 1121/1392] remove diff quantity check --- pkg/strategy/xfunding/strategy.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 454f62c932..41d391d2ba 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -638,7 +638,7 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { if futuresBase.Compare(fixedpoint.Zero) < 0 { orderPrice := ticker.Buy orderQuantity := futuresBase.Abs() - orderQuantity = fixedpoint.Max(orderQuantity, s.minQuantity) + // orderQuantity = fixedpoint.Max(orderQuantity, s.minQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) @@ -745,7 +745,8 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { log.Infof("position diff quantity: %s", diffQuantity.String()) - orderQuantity := fixedpoint.Max(diffQuantity, s.minQuantity) + orderQuantity := diffQuantity + // orderQuantity := fixedpoint.Max(diffQuantity, s.minQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { log.Warnf("unexpected dust quantity, skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) From 7904c73c536310fc041621dd9a47e8d6ba9df2cf Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 16:18:17 +0800 Subject: [PATCH 1122/1392] xfunding: use closePosition option when only dust left in the futures position --- pkg/strategy/xfunding/strategy.go | 35 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 41d391d2ba..9d57f51e44 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -638,10 +638,25 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { if futuresBase.Compare(fixedpoint.Zero) < 0 { orderPrice := ticker.Buy orderQuantity := futuresBase.Abs() - // orderQuantity = fixedpoint.Max(orderQuantity, s.minQuantity) + orderQuantity = fixedpoint.Max(orderQuantity, s.minQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { - log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Price: orderPrice, + Market: s.futuresMarket, + + // quantity: Cannot be sent with closePosition=true(Close-All) + // reduceOnly: Cannot be sent with closePosition=true + ClosePosition: true, + } + + if _, err := s.futuresOrderExecutor.SubmitOrders(ctx, submitOrder); err != nil { + log.WithError(err).Errorf("can not submit futures order with close position: %+v", submitOrder) + } return } @@ -722,7 +737,8 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { } // if - futures position < max futures position, increase it - if futuresBase.Neg().Compare(maxFuturesBasePosition) >= 0 { + // posDiff := futuresBase.Abs().Sub(maxFuturesBasePosition) + if futuresBase.Abs().Compare(maxFuturesBasePosition) >= 0 { s.setPositionState(PositionReady) bbgo.Notify("Position Ready") @@ -746,12 +762,15 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { log.Infof("position diff quantity: %s", diffQuantity.String()) orderQuantity := diffQuantity - // orderQuantity := fixedpoint.Max(diffQuantity, s.minQuantity) + orderQuantity = fixedpoint.Max(diffQuantity, s.minQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) - if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { - log.Warnf("unexpected dust quantity, skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) - return - } + + /* + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Warnf("unexpected dust quantity, skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) + return + } + */ submitOrder := types.SubmitOrder{ Symbol: s.Symbol, From 84e9b03be7608de5475e1d221ff7feeea78b62fa Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 16:20:59 +0800 Subject: [PATCH 1123/1392] xfunding: show balance --- pkg/strategy/xfunding/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 9d57f51e44..54968f3d4c 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -857,7 +857,7 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { // avoid increase the order size if s.spotMarket.IsDustQuantity(orderQuantity, orderPrice) { - log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.spotMarket) + log.Infof("skip spot order with dust quantity %s, market=%+v balance=%+v", orderQuantity.String(), s.spotMarket, b) return } From c818f79932e98a76665aae5a2e59abcd87e3b78a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 16:21:25 +0800 Subject: [PATCH 1124/1392] fix --- pkg/strategy/xfunding/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 54968f3d4c..90a16ab82c 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -851,7 +851,9 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { orderPrice := ticker.Sell orderQuantity := diffQuantity - if b, ok := s.spotSession.Account.Balance(s.spotMarket.BaseCurrency); ok { + b, ok := s.spotSession.Account.Balance(s.spotMarket.BaseCurrency) + + if ok { orderQuantity = fixedpoint.Min(b.Available, orderQuantity) } From 5d0bdd19e314e7ba36708c4d7f1c247e0a5d20d5 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 17:13:42 +0800 Subject: [PATCH 1125/1392] xfunding: always transfer balance out when reducing the futures position --- pkg/strategy/xfunding/strategy.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 90a16ab82c..36825561f0 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -635,6 +635,17 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { return } + spotBase := s.SpotPosition.GetBase() + if !s.spotMarket.IsDustQuantity(spotBase, s.SpotPosition.AverageCost) { + if balance, ok := s.futuresSession.Account.Balance(s.futuresMarket.BaseCurrency); ok && balance.Available.Sign() > 0 { + if err := backoff.RetryGeneral(ctx, func() error { + return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, balance.Available) + }); err != nil { + log.WithError(err).Errorf("spot-to-futures transfer in retry failed") + } + } + } + if futuresBase.Compare(fixedpoint.Zero) < 0 { orderPrice := ticker.Buy orderQuantity := futuresBase.Abs() From e82341b2bd5e753723848c48d17683922b8f7046 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 17:21:52 +0800 Subject: [PATCH 1126/1392] xfunding: add more transfer logs --- pkg/strategy/xfunding/transfer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 3de3540aa2..65fea2cd90 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -52,14 +52,14 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset st balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) if err != nil { - log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) + log.Infof("balance query error, adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return err } b, ok := balances[asset] if !ok { - log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) + log.Infof("balance not found, adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return fmt.Errorf("%s balance not found", asset) } @@ -75,7 +75,7 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset st // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here if amount.IsZero() { - log.Infof("adding to pending base transfer: %s %s + %s ", quantity.String(), asset, s.State.PendingBaseTransfer.String()) + log.Infof("zero amount, adding to pending base transfer: %s %s + %s ", quantity.String(), asset, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } From 2813ede7ed9f26f32687bce73827fd2a4b5f5116 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 17:26:14 +0800 Subject: [PATCH 1127/1392] xfunding: fix transferOut, and de-leverage the trade amount from the caller --- pkg/strategy/xfunding/strategy.go | 5 ++++- pkg/strategy/xfunding/transfer.go | 7 +++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 36825561f0..0c24a9991b 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -405,8 +405,11 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order switch s.getPositionState() { case PositionClosing: + // de-leverage and get the collateral base quantity for transfer + quantity := trade.Quantity.Div(s.Leverage) + if err := backoff.RetryGeneral(ctx, func() error { - return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, trade.Quantity) + return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, quantity) }); err != nil { log.WithError(err).Errorf("spot-to-futures transfer in retry failed") return diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 65fea2cd90..54b306d48f 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -41,15 +41,12 @@ func (s *Strategy) resetTransfer(ctx context.Context, ex FuturesTransfer, asset return nil } -func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset string, tradeQuantity fixedpoint.Value) error { +func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset string, quantity fixedpoint.Value) error { // if transfer done if s.State.TotalBaseTransfer.IsZero() { return nil } - // de-leverage and get the collateral base quantity for transfer - quantity := tradeQuantity.Div(s.Leverage) - balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) if err != nil { log.Infof("balance query error, adding to pending base transfer: %s %s + %s", quantity.String(), asset, s.State.PendingBaseTransfer.String()) @@ -64,6 +61,8 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset st return fmt.Errorf("%s balance not found", asset) } + log.Infof("found futures balance: %+v", b) + // add the previous pending base transfer and the current trade quantity amount := s.State.PendingBaseTransfer.Add(quantity) From 34d42afbec53a9da55fd3e6810fbe424b6ce2d97 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 17:35:24 +0800 Subject: [PATCH 1128/1392] xfunding: fix syncSpotPosition cancel order issue --- pkg/strategy/xfunding/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 0c24a9991b..abe35c9fa2 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -826,7 +826,7 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { return } - log.Infof("spot/futures positions: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + log.Infof("syncSpotPosition: spot/futures positions: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) if futuresBase.Sign() > 0 { // unexpected error @@ -834,7 +834,7 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { return } - _ = s.futuresOrderExecutor.GracefulCancel(ctx) + _ = s.spotOrderExecutor.GracefulCancel(ctx) ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { From 017278826bb0d88c24f5c69111f00e3cee56d3e7 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 17:37:18 +0800 Subject: [PATCH 1129/1392] xfunding: log failed order --- pkg/strategy/xfunding/strategy.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index abe35c9fa2..6bc82ee141 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -877,17 +877,18 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { return } - createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + submitOrder := types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimitMaker, Quantity: orderQuantity, Price: orderPrice, Market: s.futuresMarket, - }) + } + createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, submitOrder) if err != nil { - log.WithError(err).Errorf("can not submit spot order") + log.WithError(err).Errorf("can not submit spot order: %+v", submitOrder) return } From a766d88d605d1bea49ae6255fb6f6e0b9d632706 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 17:41:47 +0800 Subject: [PATCH 1130/1392] xfunding: fix balance check --- pkg/strategy/xfunding/strategy.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 6bc82ee141..dcecd5bc9e 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -866,11 +866,13 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { orderPrice := ticker.Sell orderQuantity := diffQuantity b, ok := s.spotSession.Account.Balance(s.spotMarket.BaseCurrency) - - if ok { - orderQuantity = fixedpoint.Min(b.Available, orderQuantity) + if !ok { + log.Warnf("%s balance not found, can not sync spot position", s.spotMarket.BaseCurrency) + return } + orderQuantity = fixedpoint.Min(b.Available, orderQuantity) + // avoid increase the order size if s.spotMarket.IsDustQuantity(orderQuantity, orderPrice) { log.Infof("skip spot order with dust quantity %s, market=%+v balance=%+v", orderQuantity.String(), s.spotMarket, b) From 12aad7b292aeb28fe3484dd8b4fe55ccf17703a8 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 17:43:57 +0800 Subject: [PATCH 1131/1392] xfunding: log spot balance --- pkg/strategy/xfunding/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index dcecd5bc9e..c81f08d773 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -871,6 +871,8 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { return } + log.Infof("spot balance: %+v", b) + orderQuantity = fixedpoint.Min(b.Available, orderQuantity) // avoid increase the order size From e4ababd39edb83b8b24b7abf8965aece2344fcfd Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 17:44:27 +0800 Subject: [PATCH 1132/1392] xfunding: fix spot order parameters --- pkg/strategy/xfunding/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index c81f08d773..99bb23dd8d 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -887,7 +887,7 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { Type: types.OrderTypeLimitMaker, Quantity: orderQuantity, Price: orderPrice, - Market: s.futuresMarket, + Market: s.spotMarket, } createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, submitOrder) From bd347d5aa5b386415cb3c2394e10dfc8df83d53f Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 17:57:53 +0800 Subject: [PATCH 1133/1392] xfunding: log positionRisks --- pkg/strategy/xfunding/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 99bb23dd8d..1a2634d781 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -714,7 +714,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } - log.Infof("position comparision: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + log.Infof("syncFuturesPosition: position comparision: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) if futuresBase.Sign() > 0 { // unexpected error @@ -1133,6 +1133,8 @@ func (s *Strategy) checkAndRestorePositionRisks(ctx context.Context) error { return err } + log.Infof("fetched futures position risks: %+v", positionRisks) + for _, positionRisk := range positionRisks { if positionRisk.Symbol != s.Symbol { continue From f6a3be6ff5a0743b1f0179ab485ab5b456afb856 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 16 Jun 2023 18:08:25 +0800 Subject: [PATCH 1134/1392] xfunding: improve checkAndRestorePositionRisks --- pkg/strategy/xfunding/strategy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 1a2634d781..f59136dd24 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -1135,6 +1135,11 @@ func (s *Strategy) checkAndRestorePositionRisks(ctx context.Context) error { log.Infof("fetched futures position risks: %+v", positionRisks) + if len(positionRisks) == 0 { + s.FuturesPosition.Reset() + return nil + } + for _, positionRisk := range positionRisks { if positionRisk.Symbol != s.Symbol { continue From f505dda80f2e3759974bcd683ca54eda411615ed Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 16:59:10 +0800 Subject: [PATCH 1135/1392] xfunding: handle reset transfer when starting up --- pkg/strategy/xfunding/strategy.go | 15 +++++++++++++++ pkg/strategy/xfunding/transfer.go | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index f59136dd24..e36b12c6a9 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -347,6 +347,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order bbgo.Notify("Spot Position", s.SpotPosition) bbgo.Notify("Futures Position", s.FuturesPosition) bbgo.Notify("Neutral Position", s.NeutralPosition) + bbgo.Notify("State", s.State.PositionState) // sync funding fee txns if !s.ProfitStats.LastFundingFeeTime.IsZero() { @@ -356,6 +357,20 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order // TEST CODE: // s.syncFundingFeeRecords(ctx, time.Now().Add(-3*24*time.Hour)) + switch s.State.PositionState { + case PositionOpening: + // transfer all base assets from the spot account into the spot account + if err := s.transferIn(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, fixedpoint.Zero); err != nil { + log.WithError(err).Errorf("futures asset transfer in error") + } + + case PositionClosing, PositionClosed: + // transfer all base assets from the futures account back to the spot account + if err := s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, fixedpoint.Zero); err != nil { + log.WithError(err).Errorf("futures asset transfer out error") + } + } + s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { // we act differently on the spot account diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 54b306d48f..e2cb026752 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -64,7 +64,10 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset st log.Infof("found futures balance: %+v", b) // add the previous pending base transfer and the current trade quantity - amount := s.State.PendingBaseTransfer.Add(quantity) + amount := b.MaxWithdrawAmount + if !quantity.IsZero() { + amount = s.State.PendingBaseTransfer.Add(quantity) + } // try to transfer more if we enough balance amount = fixedpoint.Min(amount, b.MaxWithdrawAmount) From e8922a4c3a430da21c2ccdb9f22d02b05128f95b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 5 Jul 2023 17:18:28 +0800 Subject: [PATCH 1136/1392] xfunding: support transferIn with zero quantity --- pkg/strategy/xfunding/transfer.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index e2cb026752..afd5c8a0a3 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -113,17 +113,19 @@ func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, asset str } // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(quantity) < 0 { + if !quantity.IsZero() && b.Available.Compare(quantity) < 0 { log.Infof("adding to pending base transfer: %s %s", quantity, asset) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } - amount := s.State.PendingBaseTransfer.Add(quantity) + amount := b.Available + if !quantity.IsZero() { + amount = s.State.PendingBaseTransfer.Add(quantity) + } pos := s.SpotPosition.GetBase().Abs() rest := pos.Sub(s.State.TotalBaseTransfer) - if rest.Sign() < 0 { return nil } From dc16e0c2992758c4802b2be607d526c120067d03 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 6 Jul 2023 15:58:42 +0800 Subject: [PATCH 1137/1392] xfunding: reset LastFundingFeeTime --- pkg/strategy/xfunding/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 454f62c932..56a3a4fb54 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -309,6 +309,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order FundingFeeCurrency: s.futuresMarket.QuoteCurrency, TotalFundingFee: fixedpoint.Zero, FundingFeeRecords: nil, + LastFundingFeeTime: time.Time{}, } } From f9eba6481686f79a4975420de20656dcca57fd03 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 6 Jul 2023 16:02:37 +0800 Subject: [PATCH 1138/1392] xfunding: always sync funding fee --- pkg/strategy/xfunding/strategy.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 56a3a4fb54..616abb47fb 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -350,9 +350,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order bbgo.Notify("Neutral Position", s.NeutralPosition) // sync funding fee txns - if !s.ProfitStats.LastFundingFeeTime.IsZero() { - s.syncFundingFeeRecords(ctx, s.ProfitStats.LastFundingFeeTime) - } + s.syncFundingFeeRecords(ctx, s.ProfitStats.LastFundingFeeTime) // TEST CODE: // s.syncFundingFeeRecords(ctx, time.Now().Add(-3*24*time.Hour)) @@ -538,6 +536,10 @@ func (s *Strategy) handleAccountUpdate(ctx context.Context, e *binance.AccountUp func (s *Strategy) syncFundingFeeRecords(ctx context.Context, since time.Time) { now := time.Now() + if since.IsZero() { + since = now.AddDate(0, -3, 0) + } + log.Infof("syncing funding fee records from the income history query: %s <=> %s", since, now) defer log.Infof("sync funding fee records done") From 7e2d5f84fb3a6bd174c5964d62bb09c83d481a29 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 6 Jul 2023 13:20:51 +0800 Subject: [PATCH 1139/1392] add google spreadsheet api integration sample code --- go.mod | 19 +++-- go.sum | 69 +++++++++---------- utils/google-spreadsheet/main.go | 115 +++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 43 deletions(-) create mode 100644 utils/google-spreadsheet/main.go diff --git a/go.mod b/go.mod index 8b64f30cc1..ac93724e94 100644 --- a/go.mod +++ b/go.mod @@ -54,23 +54,27 @@ require ( github.com/x-cray/logrus-prefixed-formatter v0.5.2 github.com/zserge/lorca v0.1.9 go.uber.org/multierr v1.7.0 + golang.org/x/oauth2 v0.5.0 golang.org/x/sync v0.1.0 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba gonum.org/v1/gonum v0.8.2 - google.golang.org/grpc v1.45.0 - google.golang.org/protobuf v1.28.0 + google.golang.org/api v0.111.0 + google.golang.org/grpc v1.53.0 + google.golang.org/protobuf v1.29.1 gopkg.in/tucnak/telebot.v2 v2.5.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + cloud.google.com/go/compute v1.18.0 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect github.com/VividCortex/ewma v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-simplejson v0.5.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cockroachdb/apd v1.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -78,7 +82,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/denisenkom/go-mssqldb v0.12.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dmarkham/enumer v1.5.8 // indirect github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -91,7 +94,10 @@ require ( github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect + github.com/googleapis/gax-go/v2 v2.7.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect @@ -111,7 +117,6 @@ require ( github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pascaldekloe/name v1.0.0 // indirect github.com/pelletier/go-toml v1.8.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect @@ -132,6 +137,7 @@ require ( github.com/tklauser/numcpus v0.2.2 // indirect github.com/ugorji/go/codec v1.2.3 // indirect github.com/ziutek/mymysql v1.5.4 // indirect + go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v0.19.0 // indirect go.opentelemetry.io/otel/metric v0.19.0 // indirect go.opentelemetry.io/otel/trace v0.19.0 // indirect @@ -144,7 +150,8 @@ require ( golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect golang.org/x/tools v0.7.0 // indirect - google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index f8c6b72d2c..846f413716 100644 --- a/go.sum +++ b/go.sum @@ -13,15 +13,21 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -49,8 +55,6 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= -github.com/adshao/go-binance/v2 v2.4.1 h1:fOZ2tCbN7sgDZvvsawUMjhsOoe40X87JVE4DklIyyyc= -github.com/adshao/go-binance/v2 v2.4.1/go.mod h1:6Qoh+CYcj8U43h4HgT6mqJnsGj4mWZKA/nsj8LN8ZTU= github.com/adshao/go-binance/v2 v2.4.2 h1:NBNMUyXrci45v3sr0RkZosiBYSw1/yuqCrJNkyEM8U0= github.com/adshao/go-binance/v2 v2.4.2/go.mod h1:41Up2dG4NfMXpCldrDPETEtiOq+pHoGsFZ73xGgaumo= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -60,7 +64,6 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/arrow v0.0.0-20201229220542-30ce2eb5d4dc/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -72,12 +75,9 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= @@ -95,8 +95,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0= @@ -108,11 +108,6 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 h1:5/aEFreBh9hH/0G+33xtczJCvMaulqsm9nDuu2BZUEo= @@ -149,15 +144,11 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM= -github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/ethereum/go-ethereum v1.10.23 h1:Xk8XAT4/UuqcjMLIMF+7imjkg32kfVFKoeyQDaO2yWM= @@ -239,6 +230,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -281,9 +273,10 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -300,8 +293,12 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorgonia/bindgen v0.0.0-20180812032444-09626750019e/go.mod h1:YzKk63P9jQHkwAo2rXHBv02yPxDzoQT2cBV0x5bGV/8= @@ -312,7 +309,6 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -538,8 +534,6 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U= -github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= @@ -595,7 +589,6 @@ github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRr github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= github.com/rollbar/rollbar-go v1.4.5 h1:Z+5yGaZdB7MFv7t759KUR3VEkGdwHjo7Avvf3ApHTVI= @@ -656,8 +649,8 @@ github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 h1:Gb2Tyox57N github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -668,7 +661,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= @@ -717,6 +709,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/otel v0.19.0 h1:Lenfy7QHRXPZVsw/12CWpxX6d/JkrX8wrx2vO8G80Ng= go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg= go.opentelemetry.io/otel/metric v0.19.0 h1:dtZ1Ju44gkJkYvo+3qGqVXmf88tc+a42edOywypengg= @@ -725,7 +719,6 @@ go.opentelemetry.io/otel/oteltest v0.19.0 h1:YVfA0ByROYqTwOxqHVZYZExzEpfZor+MU1r go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA= go.opentelemetry.io/otel/trace v0.19.0 h1:1ucYlenXIDA1OlHVLDZKX0ObXV5RLaq06DtUKz5e5zc= go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -834,6 +827,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= @@ -848,6 +842,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -933,7 +929,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= @@ -1003,7 +998,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20190226202314-149afe6ec0b6/go.mod h1:jevfED4GnIEnJrWW55YmY9DMhajHcnkqVnEXmEtMyNI= gonum.org/v1/gonum v0.0.0-20190902003836-43865b531bee/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= @@ -1031,12 +1025,16 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.111.0 h1:bwKi+z2BsdwYFRKrqwutM+axAlYLz83gt5pDSXCJT+0= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1061,7 +1059,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -1069,8 +1066,8 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf h1:JTjwKJX9erVpsw17w+OIPP7iAgEkN/r8urhWSunEDTs= -google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488 h1:QQF+HdiI4iocoxUjjpLgvTYDHKm99C/VtTBFnfiCJos= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1084,10 +1081,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1101,8 +1097,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1127,7 +1123,6 @@ gopkg.in/tucnak/telebot.v2 v2.5.0/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/utils/google-spreadsheet/main.go b/utils/google-spreadsheet/main.go new file mode 100644 index 0000000000..33b550259e --- /dev/null +++ b/utils/google-spreadsheet/main.go @@ -0,0 +1,115 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/option" + "google.golang.org/api/sheets/v4" +) + +// Retrieve a token, saves the token, then returns the generated client. +func getClient(config *oauth2.Config) *http.Client { + // The file token.json stores the user's access and refresh tokens, and is + // created automatically when the authorization flow completes for the first + // time. + tokFile := "token.json" + tok, err := tokenFromFile(tokFile) + if err != nil { + tok = getTokenFromWeb(config) + saveToken(tokFile, tok) + } + return config.Client(context.Background(), tok) +} + +// Request a token from the web, then returns the retrieved token. +func getTokenFromWeb(config *oauth2.Config) *oauth2.Token { + authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) + fmt.Printf("Go to the following link in your browser then type the "+ + "authorization code: \n%v\n", authURL) + + var authCode string + if _, err := fmt.Scan(&authCode); err != nil { + log.Fatalf("Unable to read authorization code: %v", err) + } + + tok, err := config.Exchange(context.TODO(), authCode) + if err != nil { + log.Fatalf("Unable to retrieve token from web: %v", err) + } + return tok +} + +// Retrieves a token from a local file. +func tokenFromFile(file string) (*oauth2.Token, error) { + f, err := os.Open(file) + if err != nil { + return nil, err + } + defer f.Close() + tok := &oauth2.Token{} + err = json.NewDecoder(f).Decode(tok) + return tok, err +} + +// Saves a token to a file path. +func saveToken(path string, token *oauth2.Token) { + fmt.Printf("Saving credential file to: %s\n", path) + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + log.Fatalf("Unable to cache oauth token: %v", err) + } + defer f.Close() + json.NewEncoder(f).Encode(token) +} + +func main() { + ctx := context.Background() + b, err := os.ReadFile("credentials.json") + if err != nil { + log.Fatalf("Unable to read client secret file: %v", err) + } + + // If modifying these scopes, delete your previously saved token.json. + config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets.readonly") + if err != nil { + log.Fatalf("Unable to parse client secret file to config: %v", err) + } + client := getClient(config) + + srv, err := sheets.NewService(ctx, option.WithHTTPClient(client)) + if err != nil { + log.Fatalf("Unable to retrieve Sheets client: %v", err) + } + + spreadsheetUrl := os.Getenv("GOOGLE_SPREADSHEET_URL") + spreadsheetId := os.Getenv("GOOGLE_SPREADSHEET_ID") + readRange := os.Getenv("GOOGLE_SPREADSHEET_READ_RANGE") + + fmt.Println("Reading ", spreadsheetUrl) + + // Prints the names and majors of students in a sample spreadsheet: + // https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit + // spreadsheetId := "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms" + // readRange := "Class Data!A2:E" + resp, err := srv.Spreadsheets.Values.Get(spreadsheetId, readRange).Do() + if err != nil { + log.Fatalf("Unable to retrieve data from sheet: %v", err) + } + + if len(resp.Values) == 0 { + fmt.Println("No data found.") + } else { + fmt.Println("Name, Major:") + for _, row := range resp.Values { + // Print columns A and E, which correspond to indices 0 and 4. + fmt.Printf("%s, %s\n", row[0], row[4]) + } + } +} From 70e4d42c4505d42d9e4d61937d9fafc1086f420b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Mar 2023 16:38:30 +0800 Subject: [PATCH 1140/1392] update go module files --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ac93724e94..f808adeaff 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/valyala/fastjson v1.5.1 github.com/wcharczuk/go-chart/v2 v2.1.0 github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 diff --git a/go.sum b/go.sum index 846f413716..cdc1af5feb 100644 --- a/go.sum +++ b/go.sum @@ -665,6 +665,8 @@ github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= From d3d2a1c4ecf5fdecb01375a67db4b6800a4e0243 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 6 Jul 2023 15:58:06 +0800 Subject: [PATCH 1141/1392] doc: add google spreadsheet setup process --- doc/topics/google-spreadsheet.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 doc/topics/google-spreadsheet.md diff --git a/doc/topics/google-spreadsheet.md b/doc/topics/google-spreadsheet.md new file mode 100644 index 0000000000..fbd34b9989 --- /dev/null +++ b/doc/topics/google-spreadsheet.md @@ -0,0 +1,28 @@ +# Google Spreadsheet Integration + +## Setup + +Run gcloud init to setup a new profile: + +```shell +gcloud init +``` + +Enable Sheet API: + +```shell +gcloud services enable sheets.googleapis.com +``` + +You will need to setup a service account for your bbgo application, +check the following documentation to setup the authentication: + + + +And + + + + + + From 401625db470043fa75a6c003535756e3f1573bfc Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 6 Jul 2023 16:35:16 +0800 Subject: [PATCH 1142/1392] ignore .credentials --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 5e9314db07..f41e4ff8a7 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,5 @@ coverage_dum.txt *_local.yaml /.chglog/ + +/.credentials From 9bec294aa13cff74bc8df5be379e51d9838c015a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 7 Jul 2023 13:36:31 +0800 Subject: [PATCH 1143/1392] service/google: fix appendCells call --- pkg/service/google/sheets.go | 82 ++++++++++++++++++++++++++++++++ utils/google-spreadsheet/main.go | 80 +++++++++++++++++++------------ 2 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 pkg/service/google/sheets.go diff --git a/pkg/service/google/sheets.go b/pkg/service/google/sheets.go new file mode 100644 index 0000000000..c0d6e53813 --- /dev/null +++ b/pkg/service/google/sheets.go @@ -0,0 +1,82 @@ +package google + +import ( + "fmt" + + "github.com/sirupsen/logrus" + "google.golang.org/api/sheets/v4" +) + +func ReadSheetValuesRange(srv *sheets.Service, spreadsheetId, readRange string) (*sheets.ValueRange, error) { + logrus.Infof("ReadSheetValuesRange: %s", readRange) + resp, err := srv.Spreadsheets.Values.Get(spreadsheetId, readRange).Do() + return resp, err +} + +func AddNewSheet(srv *sheets.Service, spreadsheetId string, title string) (*sheets.BatchUpdateSpreadsheetResponse, error) { + logrus.Infof("AddNewSheet: %s", title) + return srv.Spreadsheets.BatchUpdate(spreadsheetId, &sheets.BatchUpdateSpreadsheetRequest{ + IncludeSpreadsheetInResponse: false, + Requests: []*sheets.Request{ + { + AddSheet: &sheets.AddSheetRequest{ + Properties: &sheets.SheetProperties{ + Hidden: false, + TabColor: nil, + TabColorStyle: nil, + Title: title, + }, + }, + }, + }, + }).Do() +} + +func ValuesToCellData(values []interface{}) (cells []*sheets.CellData) { + for _, anyValue := range values { + switch typedValue := anyValue.(type) { + case string: + cells = append(cells, &sheets.CellData{ + UserEnteredValue: &sheets.ExtendedValue{StringValue: &typedValue}, + }) + case float64: + cells = append(cells, &sheets.CellData{ + UserEnteredValue: &sheets.ExtendedValue{NumberValue: &typedValue}, + }) + case bool: + cells = append(cells, &sheets.CellData{ + UserEnteredValue: &sheets.ExtendedValue{BoolValue: &typedValue}, + }) + } + } + + return cells +} + +func GetSpreadSheetURL(spreadsheetId string) string { + return fmt.Sprintf("https://docs.google.com/spreadsheets/d/%s/edit#gid=0", spreadsheetId) +} + +func AppendRow(srv *sheets.Service, spreadsheetId string, sheetId int64, values []interface{}) (*sheets.BatchUpdateSpreadsheetResponse, error) { + row := &sheets.RowData{} + row.Values = ValuesToCellData(values) + + logrus.Infof("AppendRow: %+v", row.Values) + return srv.Spreadsheets.BatchUpdate(spreadsheetId, &sheets.BatchUpdateSpreadsheetRequest{ + Requests: []*sheets.Request{ + { + AppendCells: &sheets.AppendCellsRequest{ + Fields: "*", + Rows: []*sheets.RowData{row}, + SheetId: sheetId, + }, + }, + }, + }).Do() +} + +func DebugBatchUpdateSpreadsheetResponse(resp *sheets.BatchUpdateSpreadsheetResponse) { + logrus.Infof("BatchUpdateSpreadsheetResponse.SpreadsheetId: %+v", resp.SpreadsheetId) + logrus.Infof("BatchUpdateSpreadsheetResponse.UpdatedSpreadsheet: %+v", resp.UpdatedSpreadsheet) + logrus.Infof("BatchUpdateSpreadsheetResponse.Replies: %+v", resp.Replies) +} diff --git a/utils/google-spreadsheet/main.go b/utils/google-spreadsheet/main.go index 33b550259e..97812831f8 100644 --- a/utils/google-spreadsheet/main.go +++ b/utils/google-spreadsheet/main.go @@ -8,10 +8,12 @@ import ( "net/http" "os" + "github.com/sirupsen/logrus" "golang.org/x/oauth2" - "golang.org/x/oauth2/google" "google.golang.org/api/option" "google.golang.org/api/sheets/v4" + + googleservice "github.com/c9s/bbgo/pkg/service/google" ) // Retrieve a token, saves the token, then returns the generated client. @@ -20,10 +22,13 @@ func getClient(config *oauth2.Config) *http.Client { // created automatically when the authorization flow completes for the first // time. tokFile := "token.json" + if p, ok := os.LookupEnv("GOOGLE_AUTH_TOKEN_FILE"); ok { + tokFile = p + } + tok, err := tokenFromFile(tokFile) if err != nil { tok = getTokenFromWeb(config) - saveToken(tokFile, tok) } return config.Client(context.Background(), tok) } @@ -58,53 +63,66 @@ func tokenFromFile(file string) (*oauth2.Token, error) { return tok, err } -// Saves a token to a file path. -func saveToken(path string, token *oauth2.Token) { - fmt.Printf("Saving credential file to: %s\n", path) - f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - log.Fatalf("Unable to cache oauth token: %v", err) - } - defer f.Close() - json.NewEncoder(f).Encode(token) -} - func main() { ctx := context.Background() - b, err := os.ReadFile("credentials.json") + + tokenFile := os.Getenv("GOOGLE_AUTH_TOKEN_FILE") + + srv, err := sheets.NewService(ctx, + option.WithCredentialsFile(tokenFile), + ) + if err != nil { - log.Fatalf("Unable to read client secret file: %v", err) + log.Fatalf("Unable to new google sheets service client: %v", err) } - // If modifying these scopes, delete your previously saved token.json. - config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets.readonly") + spreadsheetId := os.Getenv("GOOGLE_SPREADSHEET_ID") + spreadsheetUrl := googleservice.GetSpreadSheetURL(spreadsheetId) + + logrus.Infoln(spreadsheetUrl) + + spreadsheet, err := srv.Spreadsheets.Get(spreadsheetId).Do() if err != nil { - log.Fatalf("Unable to parse client secret file to config: %v", err) + log.Fatalf("unable to get spreadsheet data: %v", err) + } + + logrus.Infof("spreadsheet: %+v", spreadsheet) + + for i, sheet := range spreadsheet.Sheets { + logrus.Infof("#%d sheetId: %d", i, sheet.Properties.SheetId) + logrus.Infof("#%d sheetTitle: %s", i, sheet.Properties.Title) } - client := getClient(config) - srv, err := sheets.NewService(ctx, option.WithHTTPClient(client)) + batchUpdateResp, err := googleservice.AddNewSheet(srv, spreadsheetId, fmt.Sprintf("Test Auto Add Sheet #%d", len(spreadsheet.Sheets)+1)) if err != nil { - log.Fatalf("Unable to retrieve Sheets client: %v", err) + log.Fatal(err) } - spreadsheetUrl := os.Getenv("GOOGLE_SPREADSHEET_URL") - spreadsheetId := os.Getenv("GOOGLE_SPREADSHEET_ID") - readRange := os.Getenv("GOOGLE_SPREADSHEET_READ_RANGE") + googleservice.DebugBatchUpdateSpreadsheetResponse(batchUpdateResp) + + appendCellsResp, err := googleservice.AppendRow(srv, spreadsheetId, 0, []interface{}{ + "Date", + "Net Profit", + "Profit", + "Gross Profit", + "Gross Loss", + "Total Profit", + "Total Loss", + }) + if err != nil { + log.Fatal(err) + } - fmt.Println("Reading ", spreadsheetUrl) + logrus.Infof("appendCellsResp: %+v", appendCellsResp) - // Prints the names and majors of students in a sample spreadsheet: - // https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit - // spreadsheetId := "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms" - // readRange := "Class Data!A2:E" - resp, err := srv.Spreadsheets.Values.Get(spreadsheetId, readRange).Do() + readRange := "Sheet1!A2:E" + resp, err := googleservice.ReadSheetValuesRange(srv, spreadsheetId, readRange) if err != nil { log.Fatalf("Unable to retrieve data from sheet: %v", err) } if len(resp.Values) == 0 { - fmt.Println("No data found.") + fmt.Println("No data found") } else { fmt.Println("Name, Major:") for _, row := range resp.Values { From e41d720867f4ed68f5710cfe7ee8ebce28adab79 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 7 Jul 2023 14:52:25 +0800 Subject: [PATCH 1144/1392] service/google: support reflect conversion --- pkg/service/google/sheets.go | 66 ++++++++++++++++++++++++++++++++ utils/google-spreadsheet/main.go | 24 +++++++----- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/pkg/service/google/sheets.go b/pkg/service/google/sheets.go index c0d6e53813..4951004186 100644 --- a/pkg/service/google/sheets.go +++ b/pkg/service/google/sheets.go @@ -2,9 +2,13 @@ package google import ( "fmt" + "reflect" + "time" "github.com/sirupsen/logrus" "google.golang.org/api/sheets/v4" + + "github.com/c9s/bbgo/pkg/fixedpoint" ) func ReadSheetValuesRange(srv *sheets.Service, spreadsheetId, readRange string) (*sheets.ValueRange, error) { @@ -43,6 +47,12 @@ func ValuesToCellData(values []interface{}) (cells []*sheets.CellData) { cells = append(cells, &sheets.CellData{ UserEnteredValue: &sheets.ExtendedValue{NumberValue: &typedValue}, }) + case int: + v := float64(typedValue) + cells = append(cells, &sheets.CellData{UserEnteredValue: &sheets.ExtendedValue{NumberValue: &v}}) + case int64: + v := float64(typedValue) + cells = append(cells, &sheets.CellData{UserEnteredValue: &sheets.ExtendedValue{NumberValue: &v}}) case bool: cells = append(cells, &sheets.CellData{ UserEnteredValue: &sheets.ExtendedValue{BoolValue: &typedValue}, @@ -57,6 +67,62 @@ func GetSpreadSheetURL(spreadsheetId string) string { return fmt.Sprintf("https://docs.google.com/spreadsheets/d/%s/edit#gid=0", spreadsheetId) } +func WriteStructHeader(srv *sheets.Service, spreadsheetId string, sheetId int64, structTag string, st interface{}) (*sheets.BatchUpdateSpreadsheetResponse, error) { + typeOfSt := reflect.TypeOf(st) + typeOfSt = typeOfSt.Elem() + + var headerTexts []interface{} + for i := 0; i < typeOfSt.NumField(); i++ { + tag := typeOfSt.Field(i).Tag + tagValue := tag.Get(structTag) + if len(tagValue) == 0 { + continue + } + + headerTexts = append(headerTexts, tagValue) + } + + return AppendRow(srv, spreadsheetId, sheetId, headerTexts) +} + +func WriteStructValues(srv *sheets.Service, spreadsheetId string, sheetId int64, structTag string, st interface{}) (*sheets.BatchUpdateSpreadsheetResponse, error) { + typeOfSt := reflect.TypeOf(st) + typeOfSt = typeOfSt.Elem() + + valueOfSt := reflect.ValueOf(st) + valueOfSt = valueOfSt.Elem() + + var texts []interface{} + for i := 0; i < typeOfSt.NumField(); i++ { + tag := typeOfSt.Field(i).Tag + tagValue := tag.Get(structTag) + if len(tagValue) == 0 { + continue + } + + valueInf := valueOfSt.Field(i).Interface() + + switch typedValue := valueInf.(type) { + case string: + texts = append(texts, typedValue) + case float64: + texts = append(texts, typedValue) + case int64: + texts = append(texts, typedValue) + case *float64: + texts = append(texts, typedValue) + case fixedpoint.Value: + texts = append(texts, typedValue.String()) + case *fixedpoint.Value: + texts = append(texts, typedValue.String()) + case time.Time: + texts = append(texts, typedValue.Format(time.RFC3339)) + } + } + + return AppendRow(srv, spreadsheetId, sheetId, texts) +} + func AppendRow(srv *sheets.Service, spreadsheetId string, sheetId int64, values []interface{}) (*sheets.BatchUpdateSpreadsheetResponse, error) { row := &sheets.RowData{} row.Values = ValuesToCellData(values) diff --git a/utils/google-spreadsheet/main.go b/utils/google-spreadsheet/main.go index 97812831f8..d48a4f63d0 100644 --- a/utils/google-spreadsheet/main.go +++ b/utils/google-spreadsheet/main.go @@ -13,7 +13,9 @@ import ( "google.golang.org/api/option" "google.golang.org/api/sheets/v4" + "github.com/c9s/bbgo/pkg/fixedpoint" googleservice "github.com/c9s/bbgo/pkg/service/google" + "github.com/c9s/bbgo/pkg/types" ) // Retrieve a token, saves the token, then returns the generated client. @@ -100,20 +102,24 @@ func main() { googleservice.DebugBatchUpdateSpreadsheetResponse(batchUpdateResp) - appendCellsResp, err := googleservice.AppendRow(srv, spreadsheetId, 0, []interface{}{ - "Date", - "Net Profit", - "Profit", - "Gross Profit", - "Gross Loss", - "Total Profit", - "Total Loss", + stats := types.NewProfitStats(types.Market{ + Symbol: "BTCUSDT", + BaseCurrency: "BTC", + QuoteCurrency: "USDT", }) + stats.TodayNetProfit = fixedpoint.NewFromFloat(100.0) + stats.TodayPnL = fixedpoint.NewFromFloat(100.0) + stats.TodayGrossLoss = fixedpoint.NewFromFloat(-100.0) + + _, err = googleservice.WriteStructHeader(srv, spreadsheetId, 0, "json", stats) if err != nil { log.Fatal(err) } - logrus.Infof("appendCellsResp: %+v", appendCellsResp) + _, err = googleservice.WriteStructValues(srv, spreadsheetId, 0, "json", stats) + if err != nil { + log.Fatal(err) + } readRange := "Sheet1!A2:E" resp, err := googleservice.ReadSheetValuesRange(srv, spreadsheetId, readRange) From 5962742b43479aff27a3f0651bdda69f27f70c1f Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 9 Jul 2023 13:17:39 +0800 Subject: [PATCH 1145/1392] all: integrate google spread sheet service --- config/scmaker.yaml | 5 ++++ doc/topics/google-spreadsheet.md | 12 +++++++++ pkg/bbgo/bootstrap.go | 12 +++++++++ pkg/bbgo/config.go | 15 +++++++++++- pkg/bbgo/environment.go | 13 ++++++++++ pkg/service/google/sheets.go | 42 +++++++++++++++++++++++++++----- 6 files changed, 92 insertions(+), 7 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index b735153477..0f18409a8e 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -5,6 +5,11 @@ sessions: makerFeeRate: 0% takerFeeRate: 0.025% +#services: +# googleSpreadSheet: +# jsonTokenFile: ".credentials/google-cloud/service-account-json-token.json" +# spreadSheetId: "YOUR_SPREADSHEET_ID" + exchangeStrategies: - on: max scmaker: diff --git a/doc/topics/google-spreadsheet.md b/doc/topics/google-spreadsheet.md index fbd34b9989..246f592db7 100644 --- a/doc/topics/google-spreadsheet.md +++ b/doc/topics/google-spreadsheet.md @@ -23,6 +23,18 @@ And +Download the JSON token file and store it in a safe place. +### Setting up service account permissions + +Go to Google Workspace and Add "Manage Domain Wide Delegation", add you client and with the following scopes: + +``` +https://www.googleapis.com/auth/drive +https://www.googleapis.com/auth/drive.file +https://www.googleapis.com/auth/drive.readonly +https://www.googleapis.com/auth/spreadsheets +https://www.googleapis.com/auth/spreadsheets.readonly +``` diff --git a/pkg/bbgo/bootstrap.go b/pkg/bbgo/bootstrap.go index 63b2dc4520..bb297e2db4 100644 --- a/pkg/bbgo/bootstrap.go +++ b/pkg/bbgo/bootstrap.go @@ -24,6 +24,12 @@ func BootstrapEnvironmentLightweight(ctx context.Context, environ *Environment, } } + if userConfig.Service != nil { + if err := environ.ConfigureService(ctx, userConfig.Service); err != nil { + return err + } + } + return nil } @@ -46,6 +52,12 @@ func BootstrapEnvironment(ctx context.Context, environ *Environment, userConfig } } + if userConfig.Service != nil { + if err := environ.ConfigureService(ctx, userConfig.Service); err != nil { + return err + } + } + if err := environ.ConfigureNotificationSystem(ctx, userConfig); err != nil { return errors.Wrap(err, "notification configure error") } diff --git a/pkg/bbgo/config.go b/pkg/bbgo/config.go index 2f13d20bb6..c8d6e3120b 100644 --- a/pkg/bbgo/config.go +++ b/pkg/bbgo/config.go @@ -21,7 +21,9 @@ import ( // DefaultFeeRate set the fee rate for most cases // BINANCE uses 0.1% for both maker and taker -// for BNB holders, it's 0.075% for both maker and taker +// +// for BNB holders, it's 0.075% for both maker and taker +// // MAX uses 0.050% for maker and 0.15% for taker var DefaultFeeRate = fixedpoint.NewFromFloat(0.075 * 0.01) @@ -312,6 +314,15 @@ type SyncConfig struct { } `json:"userDataStream,omitempty" yaml:"userDataStream,omitempty"` } +type GoogleSpreadSheetServiceConfig struct { + JsonTokenFile string `json:"jsonTokenFile" yaml:"jsonTokenFile"` + SpreadSheetID string `json:"spreadSheetId" yaml:"spreadSheetId"` +} + +type ServiceConfig struct { + GoogleSpreadSheetService *GoogleSpreadSheetServiceConfig `json:"googleSpreadSheet" yaml:"googleSpreadSheet"` +} + type Config struct { Build *BuildConfig `json:"build,omitempty" yaml:"build,omitempty"` @@ -327,6 +338,8 @@ type Config struct { Persistence *PersistenceConfig `json:"persistence,omitempty" yaml:"persistence,omitempty"` + Service *ServiceConfig `json:"services,omitempty" yaml:"services,omitempty"` + Sessions map[string]*ExchangeSession `json:"sessions,omitempty" yaml:"sessions,omitempty"` RiskControls *RiskControls `json:"riskControls,omitempty" yaml:"riskControls,omitempty"` diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index f89f71dbb5..b4e1ac7cda 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -26,6 +26,7 @@ import ( "github.com/c9s/bbgo/pkg/notifier/slacknotifier" "github.com/c9s/bbgo/pkg/notifier/telegramnotifier" "github.com/c9s/bbgo/pkg/service" + googleservice "github.com/c9s/bbgo/pkg/service/google" "github.com/c9s/bbgo/pkg/slack/slacklog" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" @@ -78,6 +79,7 @@ const ( // Environment presents the real exchange data layer type Environment struct { + // built-in service DatabaseService *service.DatabaseService OrderService *service.OrderService TradeService *service.TradeService @@ -92,6 +94,9 @@ type Environment struct { DepositService *service.DepositService PersistentService *service.PersistenceServiceFacade + // external services + GoogleSpreadSheetService *googleservice.SpreadSheetService + // startTime is the time of start point (which is used in the backtest) startTime time.Time @@ -216,6 +221,14 @@ func (environ *Environment) AddExchange(name string, exchange types.Exchange) (s return environ.AddExchangeSession(name, session) } +func (environ *Environment) ConfigureService(ctx context.Context, srvConfig *ServiceConfig) error { + if srvConfig.GoogleSpreadSheetService != nil { + environ.GoogleSpreadSheetService = googleservice.NewSpreadSheetService(ctx, srvConfig.GoogleSpreadSheetService.JsonTokenFile, srvConfig.GoogleSpreadSheetService.SpreadSheetID) + } + + return nil +} + func (environ *Environment) ConfigureExchangeSessions(userConfig *Config) error { // if sessions are not defined, we detect the sessions automatically if len(userConfig.Sessions) == 0 { diff --git a/pkg/service/google/sheets.go b/pkg/service/google/sheets.go index 4951004186..d77369e828 100644 --- a/pkg/service/google/sheets.go +++ b/pkg/service/google/sheets.go @@ -1,24 +1,54 @@ package google import ( + "context" "fmt" "reflect" "time" "github.com/sirupsen/logrus" + "google.golang.org/api/option" "google.golang.org/api/sheets/v4" "github.com/c9s/bbgo/pkg/fixedpoint" ) +var log = logrus.WithField("service", "google") + +type SpreadSheetService struct { + SpreadsheetID string + TokenFile string + + service *sheets.Service +} + +func NewSpreadSheetService(ctx context.Context, tokenFile string, spreadsheetID string) *SpreadSheetService { + if len(tokenFile) == 0 { + log.Panicf("google.SpreadSheetService: jsonTokenFile is not set") + } + + srv, err := sheets.NewService(ctx, + option.WithCredentialsFile(tokenFile), + ) + + if err != nil { + log.Panicf("google.SpreadSheetService: unable to initialize spreadsheet service: %v", err) + } + + return &SpreadSheetService{ + SpreadsheetID: spreadsheetID, + service: srv, + } +} + func ReadSheetValuesRange(srv *sheets.Service, spreadsheetId, readRange string) (*sheets.ValueRange, error) { - logrus.Infof("ReadSheetValuesRange: %s", readRange) + log.Infof("ReadSheetValuesRange: %s", readRange) resp, err := srv.Spreadsheets.Values.Get(spreadsheetId, readRange).Do() return resp, err } func AddNewSheet(srv *sheets.Service, spreadsheetId string, title string) (*sheets.BatchUpdateSpreadsheetResponse, error) { - logrus.Infof("AddNewSheet: %s", title) + log.Infof("AddNewSheet: %s", title) return srv.Spreadsheets.BatchUpdate(spreadsheetId, &sheets.BatchUpdateSpreadsheetRequest{ IncludeSpreadsheetInResponse: false, Requests: []*sheets.Request{ @@ -127,7 +157,7 @@ func AppendRow(srv *sheets.Service, spreadsheetId string, sheetId int64, values row := &sheets.RowData{} row.Values = ValuesToCellData(values) - logrus.Infof("AppendRow: %+v", row.Values) + log.Infof("AppendRow: %+v", row.Values) return srv.Spreadsheets.BatchUpdate(spreadsheetId, &sheets.BatchUpdateSpreadsheetRequest{ Requests: []*sheets.Request{ { @@ -142,7 +172,7 @@ func AppendRow(srv *sheets.Service, spreadsheetId string, sheetId int64, values } func DebugBatchUpdateSpreadsheetResponse(resp *sheets.BatchUpdateSpreadsheetResponse) { - logrus.Infof("BatchUpdateSpreadsheetResponse.SpreadsheetId: %+v", resp.SpreadsheetId) - logrus.Infof("BatchUpdateSpreadsheetResponse.UpdatedSpreadsheet: %+v", resp.UpdatedSpreadsheet) - logrus.Infof("BatchUpdateSpreadsheetResponse.Replies: %+v", resp.Replies) + log.Infof("BatchUpdateSpreadsheetResponse.SpreadsheetId: %+v", resp.SpreadsheetId) + log.Infof("BatchUpdateSpreadsheetResponse.UpdatedSpreadsheet: %+v", resp.UpdatedSpreadsheet) + log.Infof("BatchUpdateSpreadsheetResponse.Replies: %+v", resp.Replies) } From 791ee36839ad8a1be9724331b237c3f74f553e73 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 9 Jul 2023 13:23:31 +0800 Subject: [PATCH 1146/1392] doc: update config struct to the topics --- doc/topics/google-spreadsheet.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/topics/google-spreadsheet.md b/doc/topics/google-spreadsheet.md index 246f592db7..3f0efe092f 100644 --- a/doc/topics/google-spreadsheet.md +++ b/doc/topics/google-spreadsheet.md @@ -38,3 +38,11 @@ https://www.googleapis.com/auth/spreadsheets.readonly ``` +### Add settings to your bbgo.yaml + +```shell +services: + googleSpreadSheet: + jsonTokenFile: ".credentials/google-cloud/service-account-json-token.json" + spreadSheetId: "YOUR_SPREADSHEET_ID" +``` From 0891859b987f1be0a8ff056e74c5765f6113049f Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 9 Jul 2023 15:11:09 +0800 Subject: [PATCH 1147/1392] dynamic: support nested persistence --- pkg/dynamic/iterate.go | 12 ++++++++ pkg/dynamic/iterate_test.go | 59 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/pkg/dynamic/iterate.go b/pkg/dynamic/iterate.go index 33a1283632..e868a841d7 100644 --- a/pkg/dynamic/iterate.go +++ b/pkg/dynamic/iterate.go @@ -52,6 +52,10 @@ func IterateFields(obj interface{}, cb func(ft reflect.StructField, fv reflect.V return nil } +func isStructPtr(tpe reflect.Type) bool { + return tpe.Kind() == reflect.Ptr && tpe.Elem().Kind() == reflect.Struct +} + func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error { sv := reflect.ValueOf(obj) st := reflect.TypeOf(obj) @@ -82,11 +86,19 @@ func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) continue } + if isStructPtr(ft.Type) && !fv.IsNil() { + // recursive iterate the struct field + if err := IterateFieldsByTag(fv.Interface(), tagName, cb); err != nil { + return fmt.Errorf("unable to iterate struct fields over the type %v: %v", ft, err) + } + } + tag, ok := ft.Tag.Lookup(tagName) if !ok { continue } + // call the iterator if err := cb(tag, ft, fv); err != nil { return err } diff --git a/pkg/dynamic/iterate_test.go b/pkg/dynamic/iterate_test.go index b15b74bf80..30cf0f958a 100644 --- a/pkg/dynamic/iterate_test.go +++ b/pkg/dynamic/iterate_test.go @@ -40,5 +40,64 @@ func TestIterateFields(t *testing.T) { assert.Error(t, err) }) +} + +func TestIterateFieldsByTag(t *testing.T) { + t.Run("nested", func(t *testing.T) { + var a = struct { + A int `persistence:"a"` + B int `persistence:"b"` + C *struct { + D int `persistence:"d"` + E int `persistence:"e"` + } + }{ + A: 1, + B: 2, + C: &struct { + D int `persistence:"d"` + E int `persistence:"e"` + }{ + D: 3, + E: 4, + }, + } + + collectedTags := []string{} + cnt := 0 + err := IterateFieldsByTag(&a, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { + cnt++ + collectedTags = append(collectedTags, tag) + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 4, cnt) + assert.Equal(t, []string{"a", "b", "d", "e"}, collectedTags) + }) + + t.Run("nested nil", func(t *testing.T) { + var a = struct { + A int `persistence:"a"` + B int `persistence:"b"` + C *struct { + D int `persistence:"d"` + E int `persistence:"e"` + } + }{ + A: 1, + B: 2, + C: nil, + } + collectedTags := []string{} + cnt := 0 + err := IterateFieldsByTag(&a, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { + cnt++ + collectedTags = append(collectedTags, tag) + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 2, cnt) + assert.Equal(t, []string{"a", "b"}, collectedTags) + }) } From 62d394d18399038a1b6b83fea9cdb1f90d9e1871 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 9 Jul 2023 15:47:22 +0800 Subject: [PATCH 1148/1392] all: moving common strategy functionality to strategy/base --- pkg/dynamic/iterate.go | 2 +- pkg/dynamic/iterate_test.go | 28 ++++++++++++ pkg/strategy/base/strategy.go | 58 ++++++++++++++++++++++++ pkg/strategy/scmaker/strategy.go | 76 +++++++++----------------------- 4 files changed, 109 insertions(+), 55 deletions(-) create mode 100644 pkg/strategy/base/strategy.go diff --git a/pkg/dynamic/iterate.go b/pkg/dynamic/iterate.go index e868a841d7..8063698ba6 100644 --- a/pkg/dynamic/iterate.go +++ b/pkg/dynamic/iterate.go @@ -61,7 +61,7 @@ func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) st := reflect.TypeOf(obj) if st.Kind() != reflect.Ptr { - return fmt.Errorf("f should be a pointer of a struct, %s given", st) + return fmt.Errorf("obj should be a pointer of a struct, %s given", st) } // for pointer, check if it's nil diff --git a/pkg/dynamic/iterate_test.go b/pkg/dynamic/iterate_test.go index 30cf0f958a..11a42d1c00 100644 --- a/pkg/dynamic/iterate_test.go +++ b/pkg/dynamic/iterate_test.go @@ -8,6 +8,16 @@ import ( "github.com/stretchr/testify/assert" ) +type TestEmbedded struct { + Foo int `persistence:"foo"` + Bar int `persistence:"bar"` +} + +type TestA struct { + *TestEmbedded + Outer int `persistence:"outer"` +} + func TestIterateFields(t *testing.T) { t.Run("basic", func(t *testing.T) { @@ -100,4 +110,22 @@ func TestIterateFieldsByTag(t *testing.T) { assert.Equal(t, 2, cnt) assert.Equal(t, []string{"a", "b"}, collectedTags) }) + + t.Run("embedded", func(t *testing.T) { + a := &TestA{ + TestEmbedded: &TestEmbedded{Foo: 1, Bar: 2}, + Outer: 3, + } + + collectedTags := []string{} + cnt := 0 + err := IterateFieldsByTag(a, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { + cnt++ + collectedTags = append(collectedTags, tag) + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 3, cnt) + assert.Equal(t, []string{"foo", "bar", "outer"}, collectedTags) + }) } diff --git a/pkg/strategy/base/strategy.go b/pkg/strategy/base/strategy.go new file mode 100644 index 0000000000..2a7ce1cca2 --- /dev/null +++ b/pkg/strategy/base/strategy.go @@ -0,0 +1,58 @@ +package base + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +// LongShortStrategy provides the core functionality that is required by a long/short strategy. +type LongShortStrategy struct { + Position *types.Position `json:"position,omitempty" persistence:"position"` + ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + + parent, ctx context.Context + cancel context.CancelFunc + + Environ *bbgo.Environment + Session *bbgo.ExchangeSession + OrderExecutor *bbgo.GeneralOrderExecutor +} + +func (s *LongShortStrategy) Setup(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) { + s.parent = ctx + s.ctx, s.cancel = context.WithCancel(ctx) + + s.Environ = environ + s.Session = session + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(market) + } + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(market) + } + + // Always update the position fields + s.Position.Strategy = strategyID + s.Position.StrategyInstanceID = instanceID + + // if anyone of the fee rate is defined, this assumes that both are defined. + // so that zero maker fee could be applied + if session.MakerFeeRate.Sign() > 0 || session.TakerFeeRate.Sign() > 0 { + s.Position.SetExchangeFeeRate(session.ExchangeName, types.ExchangeFee{ + MakerFeeRate: session.MakerFeeRate, + TakerFeeRate: session.TakerFeeRate, + }) + } + + s.OrderExecutor = bbgo.NewGeneralOrderExecutor(session, market.Symbol, strategyID, instanceID, s.Position) + s.OrderExecutor.BindEnvironment(environ) + s.OrderExecutor.BindProfitStats(s.ProfitStats) + s.OrderExecutor.Bind() + s.OrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) +} diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index af10dbbaaf..6e5a9ad53e 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -12,6 +12,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/risk/riskcontrol" + "github.com/c9s/bbgo/pkg/strategy/base" "github.com/c9s/bbgo/pkg/types" ) @@ -36,6 +37,8 @@ func init() { // Strategy scmaker is a stable coin market maker type Strategy struct { + *base.LongShortStrategy + Environment *bbgo.Environment Market types.Market @@ -64,11 +67,6 @@ type Strategy struct { CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"` CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"` - Position *types.Position `json:"position,omitempty" persistence:"position"` - ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` - - session *bbgo.ExchangeSession - orderExecutor *bbgo.GeneralOrderExecutor liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook book *types.StreamOrderBook @@ -102,9 +100,9 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - instanceID := s.InstanceID() + s.LongShortStrategy = &base.LongShortStrategy{} + s.LongShortStrategy.Setup(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) - s.session = session s.book = types.NewStreamBook(s.Symbol) s.book.BindStream(session.UserDataStream) @@ -114,39 +112,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol) s.adjustmentOrderBook.BindStream(session.UserDataStream) - // If position is nil, we need to allocate a new position for calculation - if s.Position == nil { - s.Position = types.NewPositionFromMarket(s.Market) - } - - // Always update the position fields - s.Position.Strategy = ID - s.Position.StrategyInstanceID = instanceID - - // if anyone of the fee rate is defined, this assumes that both are defined. - // so that zero maker fee could be applied - if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { - s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ - MakerFeeRate: s.session.MakerFeeRate, - TakerFeeRate: s.session.TakerFeeRate, - }) - } - - if s.ProfitStats == nil { - s.ProfitStats = types.NewProfitStats(s.Market) - } - - s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) - s.orderExecutor.BindEnvironment(s.Environment) - s.orderExecutor.BindProfitStats(s.ProfitStats) - s.orderExecutor.Bind() - s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { - bbgo.Sync(ctx, s) - }) - if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") - s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.orderExecutor, s.PositionHardLimit, s.MaxPositionQuantity) + s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity) } if !s.CircuitBreakLossThreshold.IsZero() { @@ -194,10 +162,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - err := s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) + err := s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange) logErr(err, "unable to cancel liquidity orders") - err = s.adjustmentOrderBook.GracefulCancel(ctx, s.session.Exchange) + err = s.adjustmentOrderBook.GracefulCancel(ctx, s.Session.Exchange) logErr(err, "unable to cancel adjustment orders") }) @@ -237,24 +205,24 @@ func (s *Strategy) initializePriceRangeBollinger(session *bbgo.ExchangeSession) } func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { - _ = s.adjustmentOrderBook.GracefulCancel(ctx, s.session.Exchange) + _ = s.adjustmentOrderBook.GracefulCancel(ctx, s.Session.Exchange) if s.Position.IsDust() { return } - ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol) if logErr(err, "unable to query ticker") { return } - if _, err := s.session.UpdateAccount(ctx); err != nil { + if _, err := s.Session.UpdateAccount(ctx); err != nil { logErr(err, "unable to update account") return } - baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) - quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) + baseBal, _ := s.Session.Account.Balance(s.Market.BaseCurrency) + quoteBal, _ := s.Session.Account.Balance(s.Market.QuoteCurrency) var adjOrders []types.SubmitOrder @@ -262,7 +230,7 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { tickSize := s.Market.TickSize if s.Position.IsShort() { - price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(tickSize.Neg()), s.session.MakerFeeRate, s.MinProfit) + price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(tickSize.Neg()), s.Session.MakerFeeRate, s.MinProfit) quoteQuantity := fixedpoint.Min(price.Mul(posSize), quoteBal.Available) bidQuantity := quoteQuantity.Div(price) @@ -280,7 +248,7 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { TimeInForce: types.TimeInForceGTC, }) } else if s.Position.IsLong() { - price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(tickSize), s.session.MakerFeeRate, s.MinProfit) + price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(tickSize), s.Session.MakerFeeRate, s.MinProfit) askQuantity := fixedpoint.Min(posSize, baseBal.Available) if s.Market.IsDustQuantity(askQuantity, price) { @@ -298,7 +266,7 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { }) } - createdOrders, err := s.orderExecutor.SubmitOrders(ctx, adjOrders...) + createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, adjOrders...) if logErr(err, "unable to place liquidity orders") { return } @@ -312,12 +280,12 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { return } - err := s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) + err := s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange) if logErr(err, "unable to cancel orders") { return } - ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol) if logErr(err, "unable to query ticker") { return } @@ -331,13 +299,13 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { ticker.Sell = ticker.Buy.Add(s.Market.TickSize) } - if _, err := s.session.UpdateAccount(ctx); err != nil { + if _, err := s.Session.UpdateAccount(ctx); err != nil { logErr(err, "unable to update account") return } - baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) - quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) + baseBal, _ := s.Session.Account.Balance(s.Market.BaseCurrency) + quoteBal, _ := s.Session.Account.Balance(s.Market.QuoteCurrency) spread := ticker.Sell.Sub(ticker.Buy) tickSize := fixedpoint.Max(s.LiquidityLayerTickSize, s.Market.TickSize) @@ -497,7 +465,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { makerQuota.Commit() - createdOrders, err := s.orderExecutor.SubmitOrders(ctx, liqOrders...) + createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, liqOrders...) if logErr(err, "unable to place liquidity orders") { return } From c9c058e717a613ab282cd0fa18392d37659e3085 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 9 Jul 2023 16:04:27 +0800 Subject: [PATCH 1149/1392] base: simplify naming --- pkg/strategy/base/strategy.go | 6 +++--- pkg/strategy/scmaker/strategy.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/base/strategy.go b/pkg/strategy/base/strategy.go index 2a7ce1cca2..9515a7ba3c 100644 --- a/pkg/strategy/base/strategy.go +++ b/pkg/strategy/base/strategy.go @@ -7,8 +7,8 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// LongShortStrategy provides the core functionality that is required by a long/short strategy. -type LongShortStrategy struct { +// Strategy provides the core functionality that is required by a long/short strategy. +type Strategy struct { Position *types.Position `json:"position,omitempty" persistence:"position"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` @@ -20,7 +20,7 @@ type LongShortStrategy struct { OrderExecutor *bbgo.GeneralOrderExecutor } -func (s *LongShortStrategy) Setup(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) { +func (s *Strategy) Setup(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) { s.parent = ctx s.ctx, s.cancel = context.WithCancel(ctx) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 6e5a9ad53e..5af19703ac 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -37,7 +37,7 @@ func init() { // Strategy scmaker is a stable coin market maker type Strategy struct { - *base.LongShortStrategy + *base.Strategy Environment *bbgo.Environment Market types.Market @@ -100,8 +100,8 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - s.LongShortStrategy = &base.LongShortStrategy{} - s.LongShortStrategy.Setup(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) + s.Strategy = &base.Strategy{} + s.Strategy.Setup(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) s.book = types.NewStreamBook(s.Symbol) s.book.BindStream(session.UserDataStream) From 7c2de46273322e949347dd2a9fedc52015000b38 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 9 Jul 2023 19:55:36 +0800 Subject: [PATCH 1150/1392] pkg: rename base -> common --- pkg/strategy/{base => common}/strategy.go | 2 +- pkg/strategy/scmaker/strategy.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename pkg/strategy/{base => common}/strategy.go (99%) diff --git a/pkg/strategy/base/strategy.go b/pkg/strategy/common/strategy.go similarity index 99% rename from pkg/strategy/base/strategy.go rename to pkg/strategy/common/strategy.go index 9515a7ba3c..3ad80b468e 100644 --- a/pkg/strategy/base/strategy.go +++ b/pkg/strategy/common/strategy.go @@ -1,4 +1,4 @@ -package base +package common import ( "context" diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 5af19703ac..302dbc6b65 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -12,7 +12,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/risk/riskcontrol" - "github.com/c9s/bbgo/pkg/strategy/base" + "github.com/c9s/bbgo/pkg/strategy/common" "github.com/c9s/bbgo/pkg/types" ) @@ -37,7 +37,7 @@ func init() { // Strategy scmaker is a stable coin market maker type Strategy struct { - *base.Strategy + *common.Strategy Environment *bbgo.Environment Market types.Market @@ -100,7 +100,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - s.Strategy = &base.Strategy{} + s.Strategy = &common.Strategy{} s.Strategy.Setup(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) s.book = types.NewStreamBook(s.Symbol) From 5c88abe72fd3e53784fcf9e729c34e35fe475d7f Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 9 Jul 2023 21:23:42 +0800 Subject: [PATCH 1151/1392] add rsicross strategy --- config/rsicross.yaml | 53 +++++++++++ pkg/bbgo/order_executor_general.go | 2 + pkg/bbgo/trader.go | 7 +- pkg/cmd/strategy/builtin.go | 1 + pkg/dynamic/inject.go | 102 +------------------- pkg/dynamic/inject_test.go | 105 ++++++++++++++++++++ pkg/strategy/common/strategy.go | 4 +- pkg/strategy/linregmaker/strategy.go | 11 ++- pkg/strategy/rsicross/strategy.go | 137 +++++++++++++++++++++++++++ pkg/strategy/scmaker/strategy.go | 2 +- 10 files changed, 311 insertions(+), 113 deletions(-) create mode 100644 config/rsicross.yaml create mode 100644 pkg/dynamic/inject_test.go create mode 100644 pkg/strategy/rsicross/strategy.go diff --git a/config/rsicross.yaml b/config/rsicross.yaml new file mode 100644 index 0000000000..3f4761bf6f --- /dev/null +++ b/config/rsicross.yaml @@ -0,0 +1,53 @@ +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + envVarPrefix: binance + +exchangeStrategies: +- on: binance + rsicross: + symbol: BTCUSDT + interval: 5m + fastWindow: 7 + slowWindow: 12 + + quantity: 0.1 + + ### RISK CONTROLS + ## circuitBreakEMA is used for calculating the price for circuitBreak + # circuitBreakEMA: + # interval: 1m + # window: 14 + + ## circuitBreakLossThreshold is the maximum loss threshold for realized+unrealized PnL + # circuitBreakLossThreshold: -10.0 + + ## positionHardLimit is the maximum position limit + # positionHardLimit: 500.0 + + ## maxPositionQuantity is the maximum quantity per order that could be controlled in positionHardLimit, + ## this parameter is used with positionHardLimit togerther + # maxPositionQuantity: 10.0 + +backtest: + startTime: "2022-01-01" + endTime: "2022-02-01" + symbols: + - BTCUSDT + sessions: [binance] + # syncSecKLines: true + accounts: + binance: + makerFeeRate: 0.0% + takerFeeRate: 0.075% + balances: + BTC: 0.0 + USDT: 10000.0 diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index cee2aa5738..b3d3bdf13a 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -417,6 +417,8 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos return createdOrder, nil } + log.WithError(err).Errorf("unable to submit order: %v", err) + log.Infof("reduce quantity and retry order") return e.reduceQuantityAndSubmitOrder(ctx, price, *submitOrder) } diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 0d0c0ca8b0..edc58588a1 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -222,7 +222,6 @@ func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error { // load and run Session strategies for sessionName, strategies := range trader.exchangeStrategies { var session = trader.environment.sessions[sessionName] - var orderExecutor = trader.getSessionOrderExecutor(sessionName) for _, strategy := range strategies { rs := reflect.ValueOf(strategy) @@ -237,10 +236,6 @@ func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error { return err } - if err := dynamic.InjectField(rs, "OrderExecutor", orderExecutor, false); err != nil { - return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy) - } - if defaulter, ok := strategy.(StrategyDefaulter); ok { if err := defaulter.Defaults(); err != nil { panic(err) @@ -441,7 +436,7 @@ func (trader *Trader) injectCommonServices(ctx context.Context, s interface{}) e return fmt.Errorf("field Persistence is not a struct element, %s given", field) } - if err := dynamic.InjectField(elem, "Facade", ps, true); err != nil { + if err := dynamic.InjectField(elem.Interface(), "Facade", ps, true); err != nil { return err } diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 995f9f5b8e..3a7e338888 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -27,6 +27,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/pricealert" _ "github.com/c9s/bbgo/pkg/strategy/pricedrop" _ "github.com/c9s/bbgo/pkg/strategy/rebalance" + _ "github.com/c9s/bbgo/pkg/strategy/rsicross" _ "github.com/c9s/bbgo/pkg/strategy/rsmaker" _ "github.com/c9s/bbgo/pkg/strategy/schedule" _ "github.com/c9s/bbgo/pkg/strategy/scmaker" diff --git a/pkg/dynamic/inject.go b/pkg/dynamic/inject.go index 04a48599b0..ed39ab6a5b 100644 --- a/pkg/dynamic/inject.go +++ b/pkg/dynamic/inject.go @@ -3,22 +3,19 @@ package dynamic import ( "fmt" "reflect" - "testing" "time" "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/service" - "github.com/c9s/bbgo/pkg/types" ) type testEnvironment struct { startTime time.Time } -func InjectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error { +func InjectField(target interface{}, fieldName string, obj interface{}, pointerOnly bool) error { + rs := reflect.ValueOf(target) field := rs.FieldByName(fieldName) + if !field.IsValid() { return nil } @@ -131,96 +128,3 @@ func ParseStructAndInject(f interface{}, objects ...interface{}) error { return nil } - -func Test_injectField(t *testing.T) { - type TT struct { - TradeService *service.TradeService - } - - // only pointer object can be set. - var tt = &TT{} - - // get the value of the pointer, or it can not be set. - var rv = reflect.ValueOf(tt).Elem() - - _, ret := HasField(rv, "TradeService") - assert.True(t, ret) - - ts := &service.TradeService{} - - err := InjectField(rv, "TradeService", ts, true) - assert.NoError(t, err) -} - -func Test_parseStructAndInject(t *testing.T) { - t.Run("skip nil", func(t *testing.T) { - ss := struct { - a int - Env *testEnvironment - }{ - a: 1, - Env: nil, - } - err := ParseStructAndInject(&ss, nil) - assert.NoError(t, err) - assert.Nil(t, ss.Env) - }) - t.Run("pointer", func(t *testing.T) { - ss := struct { - a int - Env *testEnvironment - }{ - a: 1, - Env: nil, - } - err := ParseStructAndInject(&ss, &testEnvironment{}) - assert.NoError(t, err) - assert.NotNil(t, ss.Env) - }) - - t.Run("composition", func(t *testing.T) { - type TT struct { - *service.TradeService - } - ss := TT{} - err := ParseStructAndInject(&ss, &service.TradeService{}) - assert.NoError(t, err) - assert.NotNil(t, ss.TradeService) - }) - - t.Run("struct", func(t *testing.T) { - ss := struct { - a int - Env testEnvironment - }{ - a: 1, - } - err := ParseStructAndInject(&ss, testEnvironment{ - startTime: time.Now(), - }) - assert.NoError(t, err) - assert.NotEqual(t, time.Time{}, ss.Env.startTime) - }) - t.Run("interface/any", func(t *testing.T) { - ss := struct { - Any interface{} // anything - }{ - Any: nil, - } - err := ParseStructAndInject(&ss, &testEnvironment{ - startTime: time.Now(), - }) - assert.NoError(t, err) - assert.NotNil(t, ss.Any) - }) - t.Run("interface/stringer", func(t *testing.T) { - ss := struct { - Stringer types.Stringer // stringer interface - }{ - Stringer: nil, - } - err := ParseStructAndInject(&ss, &types.Trade{}) - assert.NoError(t, err) - assert.NotNil(t, ss.Stringer) - }) -} diff --git a/pkg/dynamic/inject_test.go b/pkg/dynamic/inject_test.go new file mode 100644 index 0000000000..e2f9464dd5 --- /dev/null +++ b/pkg/dynamic/inject_test.go @@ -0,0 +1,105 @@ +package dynamic + +import ( + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/types" +) + +func Test_injectField(t *testing.T) { + type TT struct { + TradeService *service.TradeService + } + + // only pointer object can be set. + var tt = &TT{} + + // get the value of the pointer, or it can not be set. + var rv = reflect.ValueOf(tt).Elem() + + _, ret := HasField(rv, "TradeService") + assert.True(t, ret) + + ts := &service.TradeService{} + + err := InjectField(rv, "TradeService", ts, true) + assert.NoError(t, err) +} + +func Test_parseStructAndInject(t *testing.T) { + t.Run("skip nil", func(t *testing.T) { + ss := struct { + a int + Env *testEnvironment + }{ + a: 1, + Env: nil, + } + err := ParseStructAndInject(&ss, nil) + assert.NoError(t, err) + assert.Nil(t, ss.Env) + }) + t.Run("pointer", func(t *testing.T) { + ss := struct { + a int + Env *testEnvironment + }{ + a: 1, + Env: nil, + } + err := ParseStructAndInject(&ss, &testEnvironment{}) + assert.NoError(t, err) + assert.NotNil(t, ss.Env) + }) + + t.Run("composition", func(t *testing.T) { + type TT struct { + *service.TradeService + } + ss := TT{} + err := ParseStructAndInject(&ss, &service.TradeService{}) + assert.NoError(t, err) + assert.NotNil(t, ss.TradeService) + }) + + t.Run("struct", func(t *testing.T) { + ss := struct { + a int + Env testEnvironment + }{ + a: 1, + } + err := ParseStructAndInject(&ss, testEnvironment{ + startTime: time.Now(), + }) + assert.NoError(t, err) + assert.NotEqual(t, time.Time{}, ss.Env.startTime) + }) + t.Run("interface/any", func(t *testing.T) { + ss := struct { + Any interface{} // anything + }{ + Any: nil, + } + err := ParseStructAndInject(&ss, &testEnvironment{ + startTime: time.Now(), + }) + assert.NoError(t, err) + assert.NotNil(t, ss.Any) + }) + t.Run("interface/stringer", func(t *testing.T) { + ss := struct { + Stringer types.Stringer // stringer interface + }{ + Stringer: nil, + } + err := ParseStructAndInject(&ss, &types.Trade{}) + assert.NoError(t, err) + assert.NotNil(t, ss.Stringer) + }) +} diff --git a/pkg/strategy/common/strategy.go b/pkg/strategy/common/strategy.go index 3ad80b468e..41fec7136b 100644 --- a/pkg/strategy/common/strategy.go +++ b/pkg/strategy/common/strategy.go @@ -20,7 +20,7 @@ type Strategy struct { OrderExecutor *bbgo.GeneralOrderExecutor } -func (s *Strategy) Setup(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) { +func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) { s.parent = ctx s.ctx, s.cancel = context.WithCancel(ctx) @@ -53,6 +53,6 @@ func (s *Strategy) Setup(ctx context.Context, environ *bbgo.Environment, session s.OrderExecutor.BindProfitStats(s.ProfitStats) s.OrderExecutor.Bind() s.OrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { - bbgo.Sync(ctx, s) + // bbgo.Sync(ctx, s) }) } diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index b8e5f9be05..fcc4b4f713 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -3,9 +3,10 @@ package linregmaker import ( "context" "fmt" - "github.com/c9s/bbgo/pkg/risk/dynamicrisk" "sync" + "github.com/c9s/bbgo/pkg/risk/dynamicrisk" + "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/util" @@ -221,20 +222,20 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { }) } - // Setup Exits + // Initialize Exits s.ExitMethods.SetAndSubscribe(session, s) - // Setup dynamic spread + // Initialize dynamic spread if s.DynamicSpread.IsEnabled() { s.DynamicSpread.Initialize(s.Symbol, session) } - // Setup dynamic exposure + // Initialize dynamic exposure if s.DynamicExposure.IsEnabled() { s.DynamicExposure.Initialize(s.Symbol, session) } - // Setup dynamic quantities + // Initialize dynamic quantities if len(s.DynamicQuantityIncrease) > 0 { s.DynamicQuantityIncrease.Initialize(s.Symbol, session) } diff --git a/pkg/strategy/rsicross/strategy.go b/pkg/strategy/rsicross/strategy.go new file mode 100644 index 0000000000..167ab7ee25 --- /dev/null +++ b/pkg/strategy/rsicross/strategy.go @@ -0,0 +1,137 @@ +package rsicross + +import ( + "context" + "fmt" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/risk/riskcontrol" + "github.com/c9s/bbgo/pkg/strategy/common" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "rsicross" + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + *common.Strategy + + Environment *bbgo.Environment + Market types.Market + + Symbol string `json:"symbol"` + Interval types.Interval `json:"interval"` + SlowWindow int `json:"slowWindow"` + FastWindow int `json:"fastWindow"` + + bbgo.OpenPositionOptions + + // risk related parameters + PositionHardLimit fixedpoint.Value `json:"positionHardLimit"` + MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"` + CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"` + CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"` + + positionRiskControl *riskcontrol.PositionRiskControl + circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s:%s:%d-%d", ID, s.Symbol, s.Interval, s.FastWindow, s.SlowWindow) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.Strategy = &common.Strategy{} + s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) + + if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { + log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") + s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity) + } + + if !s.CircuitBreakLossThreshold.IsZero() { + log.Infof("circuitBreakLossThreshold is configured, setting up CircuitBreakRiskControl...") + s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( + s.Position, + session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), + s.CircuitBreakLossThreshold, + s.ProfitStats) + } + + fastRsi := session.Indicators(s.Symbol).RSI(types.IntervalWindow{Interval: s.Interval, Window: s.FastWindow}) + slowRsi := session.Indicators(s.Symbol).RSI(types.IntervalWindow{Interval: s.Interval, Window: s.SlowWindow}) + rsiCross := indicator.Cross(fastRsi, slowRsi) + rsiCross.OnUpdate(func(v float64) { + switch indicator.CrossType(v) { + case indicator.CrossOver: + opts := s.OpenPositionOptions + opts.Long = true + + if price, ok := session.LastPrice(s.Symbol); ok { + opts.Price = price + } + + // opts.Price = closePrice + opts.Tags = []string{"rsiCrossOver"} + if _, err := s.OrderExecutor.OpenPosition(ctx, opts); err != nil { + logErr(err, "unable to open position") + } + + case indicator.CrossUnder: + if err := s.OrderExecutor.ClosePosition(ctx, fixedpoint.One); err != nil { + logErr(err, "failed to close position") + } + + } + }) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + }) + + return nil +} + +func (s *Strategy) preloadKLines(inc *indicator.KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) { + if store, ok := session.MarketDataStore(symbol); ok { + if kLinesData, ok := store.KLinesOfInterval(interval); ok { + for _, k := range *kLinesData { + inc.EmitUpdate(k) + } + } + } +} + +func logErr(err error, msgAndArgs ...interface{}) bool { + if err == nil { + return false + } + + if len(msgAndArgs) == 0 { + log.WithError(err).Error(err.Error()) + } else if len(msgAndArgs) == 1 { + msg := msgAndArgs[0].(string) + log.WithError(err).Error(msg) + } else if len(msgAndArgs) > 1 { + msg := msgAndArgs[0].(string) + log.WithError(err).Errorf(msg, msgAndArgs[1:]...) + } + + return true +} diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 302dbc6b65..6dc1dc6592 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -101,7 +101,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { s.Strategy = &common.Strategy{} - s.Strategy.Setup(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) + s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) s.book = types.NewStreamBook(s.Symbol) s.book.BindStream(session.UserDataStream) From 12bb22ae87a624286d970a0ce4e4d8643563609c Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 9 Jul 2023 21:24:56 +0800 Subject: [PATCH 1152/1392] rsicross: remove unused funcs --- pkg/strategy/rsicross/strategy.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pkg/strategy/rsicross/strategy.go b/pkg/strategy/rsicross/strategy.go index 167ab7ee25..dde9f1e6b8 100644 --- a/pkg/strategy/rsicross/strategy.go +++ b/pkg/strategy/rsicross/strategy.go @@ -108,16 +108,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -func (s *Strategy) preloadKLines(inc *indicator.KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) { - if store, ok := session.MarketDataStore(symbol); ok { - if kLinesData, ok := store.KLinesOfInterval(interval); ok { - for _, k := range *kLinesData { - inc.EmitUpdate(k) - } - } - } -} - func logErr(err error, msgAndArgs ...interface{}) bool { if err == nil { return false From 3b6cff8dc7070befcac1c894ac11cd30dd180dcb Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 10 Jul 2023 15:24:07 +0800 Subject: [PATCH 1153/1392] strategy: move risk control to common.Strategy --- config/rsicross.yaml | 2 +- pkg/strategy/common/strategy.go | 26 ++++++++++++++++++++++++++ pkg/strategy/rsicross/strategy.go | 24 ------------------------ 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/config/rsicross.yaml b/config/rsicross.yaml index 3f4761bf6f..7ad1de4353 100644 --- a/config/rsicross.yaml +++ b/config/rsicross.yaml @@ -16,7 +16,7 @@ exchangeStrategies: rsicross: symbol: BTCUSDT interval: 5m - fastWindow: 7 + fastWindow: 3 slowWindow: 12 quantity: 0.1 diff --git a/pkg/strategy/common/strategy.go b/pkg/strategy/common/strategy.go index 41fec7136b..6a076540c0 100644 --- a/pkg/strategy/common/strategy.go +++ b/pkg/strategy/common/strategy.go @@ -3,7 +3,11 @@ package common import ( "context" + log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/risk/riskcontrol" "github.com/c9s/bbgo/pkg/types" ) @@ -18,6 +22,14 @@ type Strategy struct { Environ *bbgo.Environment Session *bbgo.ExchangeSession OrderExecutor *bbgo.GeneralOrderExecutor + + PositionHardLimit fixedpoint.Value `json:"positionHardLimit"` + MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"` + CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"` + CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"` + + positionRiskControl *riskcontrol.PositionRiskControl + circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl } func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) { @@ -55,4 +67,18 @@ func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, se s.OrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { // bbgo.Sync(ctx, s) }) + + if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { + log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") + s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity) + } + + if !s.CircuitBreakLossThreshold.IsZero() { + log.Infof("circuitBreakLossThreshold is configured, setting up CircuitBreakRiskControl...") + s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( + s.Position, + session.Indicators(market.Symbol).EWMA(s.CircuitBreakEMA), + s.CircuitBreakLossThreshold, + s.ProfitStats) + } } diff --git a/pkg/strategy/rsicross/strategy.go b/pkg/strategy/rsicross/strategy.go index dde9f1e6b8..5710e418e7 100644 --- a/pkg/strategy/rsicross/strategy.go +++ b/pkg/strategy/rsicross/strategy.go @@ -10,7 +10,6 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" - "github.com/c9s/bbgo/pkg/risk/riskcontrol" "github.com/c9s/bbgo/pkg/strategy/common" "github.com/c9s/bbgo/pkg/types" ) @@ -33,15 +32,6 @@ type Strategy struct { FastWindow int `json:"fastWindow"` bbgo.OpenPositionOptions - - // risk related parameters - PositionHardLimit fixedpoint.Value `json:"positionHardLimit"` - MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"` - CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"` - CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"` - - positionRiskControl *riskcontrol.PositionRiskControl - circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl } func (s *Strategy) ID() string { @@ -60,20 +50,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Strategy = &common.Strategy{} s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) - if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { - log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") - s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity) - } - - if !s.CircuitBreakLossThreshold.IsZero() { - log.Infof("circuitBreakLossThreshold is configured, setting up CircuitBreakRiskControl...") - s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( - s.Position, - session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), - s.CircuitBreakLossThreshold, - s.ProfitStats) - } - fastRsi := session.Indicators(s.Symbol).RSI(types.IntervalWindow{Interval: s.Interval, Window: s.FastWindow}) slowRsi := session.Indicators(s.Symbol).RSI(types.IntervalWindow{Interval: s.Interval, Window: s.SlowWindow}) rsiCross := indicator.Cross(fastRsi, slowRsi) From 3293866a6c66985d07629ff83ecb064b167aa1e7 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 10 Jul 2023 15:27:36 +0800 Subject: [PATCH 1154/1392] common: pull out RiskController --- pkg/strategy/common/strategy.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/common/strategy.go b/pkg/strategy/common/strategy.go index 6a076540c0..5e2cd2c4d5 100644 --- a/pkg/strategy/common/strategy.go +++ b/pkg/strategy/common/strategy.go @@ -11,6 +11,16 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +type RiskController struct { + PositionHardLimit fixedpoint.Value `json:"positionHardLimit"` + MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"` + CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"` + CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"` + + positionRiskControl *riskcontrol.PositionRiskControl + circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl +} + // Strategy provides the core functionality that is required by a long/short strategy. type Strategy struct { Position *types.Position `json:"position,omitempty" persistence:"position"` @@ -23,13 +33,7 @@ type Strategy struct { Session *bbgo.ExchangeSession OrderExecutor *bbgo.GeneralOrderExecutor - PositionHardLimit fixedpoint.Value `json:"positionHardLimit"` - MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"` - CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"` - CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"` - - positionRiskControl *riskcontrol.PositionRiskControl - circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl + RiskController } func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) { From aabd7edd7b9519ece16b6d0766e0195f36167b0d Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 14:56:22 +0800 Subject: [PATCH 1155/1392] feature/profitTracker: prototype --- pkg/bbgo/order_executor_general.go | 5 +++ pkg/report/profit_tracker.go | 63 ++++++++++++++++++++++++++++++ pkg/types/profit.go | 7 ++++ 3 files changed, 75 insertions(+) create mode 100644 pkg/report/profit_tracker.go diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index cee2aa5738..ae79819924 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -3,6 +3,7 @@ package bbgo import ( "context" "fmt" + "github.com/c9s/bbgo/pkg/report" "strings" "time" @@ -162,6 +163,10 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { }) } +func (e *GeneralOrderExecutor) BindProfitTracker(profitTracker *report.ProfitTracker) { + profitTracker.Bind(e.tradeCollector, e.session) +} + func (e *GeneralOrderExecutor) Bind() { e.activeMakerOrders.BindStream(e.session.UserDataStream) e.orderStore.BindStream(e.session.UserDataStream) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go new file mode 100644 index 0000000000..04f67acae7 --- /dev/null +++ b/pkg/report/profit_tracker.go @@ -0,0 +1,63 @@ +package report + +import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type ProfitTracker struct { + types.IntervalWindow + + market types.Market + profitStatsSlice []*types.ProfitStats + currentProfitStats **types.ProfitStats +} + +// InitOld is for backward capability. ps is the ProfitStats of the strategy, market is the strategy market +func (p *ProfitTracker) InitOld(ps **types.ProfitStats, market types.Market) { + p.market = market + + if *ps == nil { + *ps = types.NewProfitStats(p.market) + } + + p.currentProfitStats = ps + p.profitStatsSlice = append(p.profitStatsSlice, *ps) +} + +// Init initialize the tracker with the given market +func (p *ProfitTracker) Init(market types.Market) { + p.market = market + *p.currentProfitStats = types.NewProfitStats(p.market) + p.profitStatsSlice = append(p.profitStatsSlice, *p.currentProfitStats) +} + +func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo.ExchangeSession) { + // TODO: Register kline close callback + tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + p.AddProfit(*profit) + }) + + tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + + }) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(p.market.Symbol, p.Interval, func(kline types.KLine) { + + })) +} + +// Rotate the tracker to make a new ProfitStats to record the profits +func (p *ProfitTracker) Rotate() { + *p.currentProfitStats = types.NewProfitStats(p.market) + p.profitStatsSlice = append(p.profitStatsSlice, *p.currentProfitStats) + // Truncate + if len(p.profitStatsSlice) > p.Window { + p.profitStatsSlice = p.profitStatsSlice[len(p.profitStatsSlice)-p.Window:] + } +} + +func (p *ProfitTracker) AddProfit(profit types.Profit) { + (*p.currentProfitStats).AddProfit(profit) +} diff --git a/pkg/types/profit.go b/pkg/types/profit.go index 121e4bc88c..aa57b2e22d 100644 --- a/pkg/types/profit.go +++ b/pkg/types/profit.go @@ -164,6 +164,9 @@ type ProfitStats struct { TodayGrossProfit fixedpoint.Value `json:"todayGrossProfit,omitempty"` TodayGrossLoss fixedpoint.Value `json:"todayGrossLoss,omitempty"` TodaySince int64 `json:"todaySince,omitempty"` + + startTime time.Time + endTime time.Time } func NewProfitStats(market Market) *ProfitStats { @@ -182,6 +185,8 @@ func NewProfitStats(market Market) *ProfitStats { TodayGrossProfit: fixedpoint.Zero, TodayGrossLoss: fixedpoint.Zero, TodaySince: 0, + startTime: time.Now().UTC(), + endTime: time.Now().UTC(), } } @@ -223,6 +228,8 @@ func (s *ProfitStats) AddProfit(profit Profit) { s.AccumulatedGrossLoss = s.AccumulatedGrossLoss.Add(profit.Profit) s.TodayGrossLoss = s.TodayGrossLoss.Add(profit.Profit) } + + s.endTime = profit.TradedAt.UTC() } func (s *ProfitStats) AddTrade(trade Trade) { From a31193021578a8006c46e0f4cdf77ad073b63ac9 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 15:06:58 +0800 Subject: [PATCH 1156/1392] feature/profitTracker: add AddTrade() --- pkg/report/profit_tracker.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 04f67acae7..85b7972d73 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -34,17 +34,17 @@ func (p *ProfitTracker) Init(market types.Market) { } func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo.ExchangeSession) { - // TODO: Register kline close callback tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { p.AddProfit(*profit) }) tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - + p.AddTrade(trade) }) + // Rotate profitStats slice session.MarketDataStream.OnKLineClosed(types.KLineWith(p.market.Symbol, p.Interval, func(kline types.KLine) { - + p.Rotate() })) } @@ -61,3 +61,7 @@ func (p *ProfitTracker) Rotate() { func (p *ProfitTracker) AddProfit(profit types.Profit) { (*p.currentProfitStats).AddProfit(profit) } + +func (p *ProfitTracker) AddTrade(trade types.Trade) { + (*p.currentProfitStats).AddTrade(trade) +} From 2c054e07e903555207335b63ec699a967934440c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 15:18:15 +0800 Subject: [PATCH 1157/1392] feature/profitTracker: use profitTracker in Supertrend strategy --- pkg/report/profit_tracker.go | 29 ++++++++++++++++------------- pkg/strategy/supertrend/strategy.go | 7 +++++++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 85b7972d73..86885034e6 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -9,9 +9,10 @@ import ( type ProfitTracker struct { types.IntervalWindow - market types.Market - profitStatsSlice []*types.ProfitStats - currentProfitStats **types.ProfitStats + ProfitStatsSlice []*types.ProfitStats + CurrentProfitStats **types.ProfitStats + + market types.Market } // InitOld is for backward capability. ps is the ProfitStats of the strategy, market is the strategy market @@ -22,18 +23,20 @@ func (p *ProfitTracker) InitOld(ps **types.ProfitStats, market types.Market) { *ps = types.NewProfitStats(p.market) } - p.currentProfitStats = ps - p.profitStatsSlice = append(p.profitStatsSlice, *ps) + p.CurrentProfitStats = ps + p.ProfitStatsSlice = append(p.ProfitStatsSlice, *ps) } // Init initialize the tracker with the given market func (p *ProfitTracker) Init(market types.Market) { p.market = market - *p.currentProfitStats = types.NewProfitStats(p.market) - p.profitStatsSlice = append(p.profitStatsSlice, *p.currentProfitStats) + *p.CurrentProfitStats = types.NewProfitStats(p.market) + p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) } func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, p.market.Symbol, types.SubscribeOptions{Interval: p.Interval}) + tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { p.AddProfit(*profit) }) @@ -50,18 +53,18 @@ func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo. // Rotate the tracker to make a new ProfitStats to record the profits func (p *ProfitTracker) Rotate() { - *p.currentProfitStats = types.NewProfitStats(p.market) - p.profitStatsSlice = append(p.profitStatsSlice, *p.currentProfitStats) + *p.CurrentProfitStats = types.NewProfitStats(p.market) + p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) // Truncate - if len(p.profitStatsSlice) > p.Window { - p.profitStatsSlice = p.profitStatsSlice[len(p.profitStatsSlice)-p.Window:] + if len(p.ProfitStatsSlice) > p.Window { + p.ProfitStatsSlice = p.ProfitStatsSlice[len(p.ProfitStatsSlice)-p.Window:] } } func (p *ProfitTracker) AddProfit(profit types.Profit) { - (*p.currentProfitStats).AddProfit(profit) + (*p.CurrentProfitStats).AddProfit(profit) } func (p *ProfitTracker) AddTrade(trade types.Trade) { - (*p.currentProfitStats).AddTrade(trade) + (*p.CurrentProfitStats).AddTrade(trade) } diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 3bf569d322..57891b7a21 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -39,6 +39,8 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + ProfitTracker *report.ProfitTracker `json:"profitTracker" persistence:"profit_tracker"` + // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -328,6 +330,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats = types.NewTradeStats(s.Symbol) } + if s.ProfitTracker.CurrentProfitStats == nil { + s.ProfitTracker.InitOld(&s.ProfitStats, s.Market) + } + // Interval profit report if bbgo.IsBackTesting { startTime := s.Environment.StartTime() @@ -349,6 +355,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindProfitStats(s.ProfitStats) s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.BindProfitTracker(s.ProfitTracker) s.orderExecutor.Bind() // AccountValueCalculator From 747f60b0d52b7ad0151803c19cfbbdef16826dd7 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 18:06:47 +0800 Subject: [PATCH 1158/1392] feature/profitTracker: integrate profit report with profit tracker --- pkg/bbgo/order_executor_general.go | 15 ++- pkg/report/profit_report.go | 181 +++++++++++----------------- pkg/report/profit_tracker.go | 57 +++++---- pkg/strategy/supertrend/strategy.go | 53 ++++---- 4 files changed, 136 insertions(+), 170 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index ae79819924..decca0d697 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -164,7 +164,20 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { } func (e *GeneralOrderExecutor) BindProfitTracker(profitTracker *report.ProfitTracker) { - profitTracker.Bind(e.tradeCollector, e.session) + e.session.Subscribe(types.KLineChannel, profitTracker.Market.Symbol, types.SubscribeOptions{Interval: profitTracker.Interval}) + + e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + profitTracker.AddProfit(*profit) + }) + + e.tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + profitTracker.AddTrade(trade) + }) + + // Rotate profitStats slice + e.session.MarketDataStream.OnKLineClosed(types.KLineWith(profitTracker.Market.Symbol, profitTracker.Interval, func(kline types.KLine) { + profitTracker.Rotate() + })) } func (e *GeneralOrderExecutor) Bind() { diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index 9d39b32786..c338a99217 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -2,7 +2,6 @@ package report import ( "fmt" - "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/data/tsv" "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -12,134 +11,104 @@ import ( // AccumulatedProfitReport For accumulated profit report output type AccumulatedProfitReport struct { - // AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades - AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"` + // ProfitMAWindow Accumulated profit SMA window + ProfitMAWindow int `json:"ProfitMAWindow"` - // IntervalWindow interval window, in days - IntervalWindow int `json:"intervalWindow"` - - // NumberOfInterval How many intervals to output to TSV - NumberOfInterval int `json:"NumberOfInterval"` + // ShortTermProfitWindow The window to sum up the short-term profit + ShortTermProfitWindow int `json:"shortTermProfitWindow"` // TsvReportPath The path to output report to TsvReportPath string `json:"tsvReportPath"` - // AccumulatedDailyProfitWindow The window to sum up the daily profit, in days - AccumulatedDailyProfitWindow int `json:"accumulatedDailyProfitWindow"` + symbol string - Symbol string + types.IntervalWindow // Accumulated profit - accumulatedProfit fixedpoint.Value - accumulatedProfitPerDay floats.Slice - previousAccumulatedProfit fixedpoint.Value + accumulatedProfit fixedpoint.Value + accumulatedProfitPerInterval floats.Slice // Accumulated profit MA - accumulatedProfitMA *indicator.SMA - accumulatedProfitMAPerDay floats.Slice + profitMA *indicator.SMA + profitMAPerInterval floats.Slice - // Daily profit - dailyProfit floats.Slice + // Profit of each interval + ProfitPerInterval floats.Slice // Accumulated fee - accumulatedFee fixedpoint.Value - accumulatedFeePerDay floats.Slice + accumulatedFee fixedpoint.Value + accumulatedFeePerInterval floats.Slice // Win ratio - winRatioPerDay floats.Slice + winRatioPerInterval floats.Slice // Profit factor - profitFactorPerDay floats.Slice + profitFactorPerInterval floats.Slice // Trade number - dailyTrades floats.Slice - accumulatedTrades int - previousAccumulatedTrades int + accumulatedTrades int + accumulatedTradesPerInterval floats.Slice // Extra values - extraValues [][2]string + strategyParameters [][2]string } -func (r *AccumulatedProfitReport) Initialize(Symbol string, session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor, TradeStats *types.TradeStats) { - r.Symbol = Symbol +func (r *AccumulatedProfitReport) Initialize(symbol string, interval types.Interval, window int) { + r.symbol = symbol + r.Interval = interval + r.Window = window - if r.AccumulatedProfitMAWindow <= 0 { - r.AccumulatedProfitMAWindow = 60 - } - if r.IntervalWindow <= 0 { - r.IntervalWindow = 7 - } - if r.AccumulatedDailyProfitWindow <= 0 { - r.AccumulatedDailyProfitWindow = 7 - } - if r.NumberOfInterval <= 0 { - r.NumberOfInterval = 1 + if r.ProfitMAWindow <= 0 { + r.ProfitMAWindow = 60 } - r.accumulatedProfitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1d, Window: r.AccumulatedProfitMAWindow}} - - session.Subscribe(types.KLineChannel, r.Symbol, types.SubscribeOptions{Interval: types.Interval1d}) - - // Record profit - orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) { - if profit == nil { - return - } - - r.RecordProfit(profit.Profit) - }) - // Record trade - orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - r.RecordTrade(trade.Fee) - }) + if r.Window <= 0 { + r.Window = 7 + } - // Record daily status - session.MarketDataStream.OnKLineClosed(types.KLineWith(r.Symbol, types.Interval1d, func(kline types.KLine) { - r.DailyUpdate(TradeStats) - })) -} + if r.ShortTermProfitWindow <= 0 { + r.ShortTermProfitWindow = 7 + } -func (r *AccumulatedProfitReport) AddExtraValue(valueAndTitle [2]string) { - r.extraValues = append(r.extraValues, valueAndTitle) + r.profitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: r.Interval, Window: r.ProfitMAWindow}} } -func (r *AccumulatedProfitReport) RecordProfit(profit fixedpoint.Value) { - r.accumulatedProfit = r.accumulatedProfit.Add(profit) +func (r *AccumulatedProfitReport) AddStrategyParameter(title string, value string) { + r.strategyParameters = append(r.strategyParameters, [2]string{title, value}) } -func (r *AccumulatedProfitReport) RecordTrade(fee fixedpoint.Value) { - r.accumulatedFee = r.accumulatedFee.Add(fee) +func (r *AccumulatedProfitReport) AddTrade(trade types.Trade) { + r.accumulatedFee = r.accumulatedFee.Add(trade.Fee) r.accumulatedTrades += 1 } -func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { - // Daily profit - r.dailyProfit.Update(r.accumulatedProfit.Sub(r.previousAccumulatedProfit).Float64()) - r.previousAccumulatedProfit = r.accumulatedProfit - +func (r *AccumulatedProfitReport) Rotate(ps *types.ProfitStats, ts *types.TradeStats) { // Accumulated profit - r.accumulatedProfitPerDay.Update(r.accumulatedProfit.Float64()) + r.accumulatedProfit.Add(ps.AccumulatedNetProfit) + r.accumulatedProfitPerInterval.Update(r.accumulatedProfit.Float64()) - // Accumulated profit MA - r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) - r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last(0)) + // Profit of each interval + r.ProfitPerInterval.Update(ps.AccumulatedNetProfit.Float64()) + + // Profit MA + r.profitMA.Update(r.accumulatedProfit.Float64()) + r.profitMAPerInterval.Update(r.profitMA.Last(0)) // Accumulated Fee - r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) + r.accumulatedFeePerInterval.Update(r.accumulatedFee.Float64()) + + // Trades + r.accumulatedTradesPerInterval.Update(float64(r.accumulatedTrades)) // Win ratio - r.winRatioPerDay.Update(tradeStats.WinningRatio.Float64()) + r.winRatioPerInterval.Update(ts.WinningRatio.Float64()) // Profit factor - r.profitFactorPerDay.Update(tradeStats.ProfitFactor.Float64()) - - // Daily trades - r.dailyTrades.Update(float64(r.accumulatedTrades - r.previousAccumulatedTrades)) - r.previousAccumulatedTrades = r.accumulatedTrades + r.profitFactorPerInterval.Update(ts.ProfitFactor.Float64()) } // Output Accumulated profit report to a TSV file -func (r *AccumulatedProfitReport) Output(symbol string) { +func (r *AccumulatedProfitReport) Output() { if r.TsvReportPath != "" { tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath) if err != nil { @@ -150,46 +119,34 @@ func (r *AccumulatedProfitReport) Output(symbol string) { titles := []string{ "#", "Symbol", - "accumulatedProfit", - "accumulatedProfitMA", - fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), + "Total Net Profit", + fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.Window), + fmt.Sprintf("%s %d Net Profit", r.Interval, r.ShortTermProfitWindow), "accumulatedFee", - "accumulatedNetProfit", "winRatio", "profitFactor", - "60D trades", + fmt.Sprintf("%s %d Trades", r.Interval, r.Window), } - for i := 0; i < len(r.extraValues); i++ { - titles = append(titles, r.extraValues[i][0]) + for i := 0; i < len(r.strategyParameters); i++ { + titles = append(titles, r.strategyParameters[i][0]) } _ = tsvwiter.Write(titles) // Output data row - for i := 0; i <= r.NumberOfInterval-1; i++ { - accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i) - accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit) - accumulatedProfitMA := r.accumulatedProfitMAPerDay.Index(r.IntervalWindow * i) - accumulatedProfitMAStr := fmt.Sprintf("%f", accumulatedProfitMA) - intervalAccumulatedProfit := r.dailyProfit.Tail(r.AccumulatedDailyProfitWindow+r.IntervalWindow*i).Sum() - r.dailyProfit.Tail(r.IntervalWindow*i).Sum() - intervalAccumulatedProfitStr := fmt.Sprintf("%f", intervalAccumulatedProfit) - accumulatedFee := fmt.Sprintf("%f", r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) - accumulatedNetProfit := fmt.Sprintf("%f", accumulatedProfit-r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) - winRatio := fmt.Sprintf("%f", r.winRatioPerDay.Index(r.IntervalWindow*i)) - profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i)) - trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum() - tradesStr := fmt.Sprintf("%f", trades) + for i := 0; i <= r.Window-1; i++ { values := []string{ fmt.Sprintf("%d", i+1), - symbol, accumulatedProfitStr, - accumulatedProfitMAStr, - intervalAccumulatedProfitStr, - accumulatedFee, - accumulatedNetProfit, - winRatio, profitFactor, - tradesStr, + r.symbol, + fmt.Sprintf("%f", r.accumulatedProfitPerInterval.Last(i)), + fmt.Sprintf("%f", r.profitMAPerInterval.Last(i)), + fmt.Sprintf("%f", r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow)), + fmt.Sprintf("%f", r.accumulatedFeePerInterval.Last(i)), + fmt.Sprintf("%f", r.winRatioPerInterval.Last(i)), + fmt.Sprintf("%f", r.profitFactorPerInterval.Last(i)), + fmt.Sprintf("%f", r.accumulatedTradesPerInterval.Last(i)), } - for j := 0; j < len(r.extraValues); j++ { - values = append(values, r.extraValues[j][1]) + for j := 0; j < len(r.strategyParameters); j++ { + values = append(values, r.strategyParameters[j][1]) } _ = tsvwiter.Write(values) } diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 86885034e6..2bfad3338a 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -1,64 +1,59 @@ package report import ( - "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) type ProfitTracker struct { types.IntervalWindow + // Accumulated profit report + AccumulatedProfitReport *AccumulatedProfitReport `json:"accumulatedProfitReport"` + + Market types.Market + ProfitStatsSlice []*types.ProfitStats CurrentProfitStats **types.ProfitStats - market types.Market + tradeStats *types.TradeStats } -// InitOld is for backward capability. ps is the ProfitStats of the strategy, market is the strategy market -func (p *ProfitTracker) InitOld(ps **types.ProfitStats, market types.Market) { - p.market = market +// InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market +func (p *ProfitTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { + p.Market = market if *ps == nil { - *ps = types.NewProfitStats(p.market) + *ps = types.NewProfitStats(p.Market) } + p.tradeStats = ts + p.CurrentProfitStats = ps p.ProfitStatsSlice = append(p.ProfitStatsSlice, *ps) -} -// Init initialize the tracker with the given market -func (p *ProfitTracker) Init(market types.Market) { - p.market = market - *p.CurrentProfitStats = types.NewProfitStats(p.market) - p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.Initialize(p.Market.Symbol, p.Interval, p.Window) + } } -func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, p.market.Symbol, types.SubscribeOptions{Interval: p.Interval}) - - tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { - p.AddProfit(*profit) - }) - - tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - p.AddTrade(trade) - }) - - // Rotate profitStats slice - session.MarketDataStream.OnKLineClosed(types.KLineWith(p.market.Symbol, p.Interval, func(kline types.KLine) { - p.Rotate() - })) +// Init initialize the tracker with the given Market +func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { + ps := types.NewProfitStats(p.Market) + p.InitOld(market, &ps, ts) } // Rotate the tracker to make a new ProfitStats to record the profits func (p *ProfitTracker) Rotate() { - *p.CurrentProfitStats = types.NewProfitStats(p.market) + *p.CurrentProfitStats = types.NewProfitStats(p.Market) p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) // Truncate if len(p.ProfitStatsSlice) > p.Window { p.ProfitStatsSlice = p.ProfitStatsSlice[len(p.ProfitStatsSlice)-p.Window:] } + + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.Rotate(*p.CurrentProfitStats, p.tradeStats) + } } func (p *ProfitTracker) AddProfit(profit types.Profit) { @@ -67,4 +62,8 @@ func (p *ProfitTracker) AddProfit(profit types.Profit) { func (p *ProfitTracker) AddTrade(trade types.Trade) { (*p.CurrentProfitStats).AddTrade(trade) + + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.AddTrade(trade) + } } diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 57891b7a21..946dcb11c7 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -39,8 +39,6 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` - ProfitTracker *report.ProfitTracker `json:"profitTracker" persistence:"profit_tracker"` - // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -104,8 +102,7 @@ type Strategy struct { // StrategyController bbgo.StrategyController - // Accumulated profit report - AccumulatedProfitReport *report.AccumulatedProfitReport `json:"accumulatedProfitReport"` + ProfitTracker *report.ProfitTracker `json:"profitTracker" persistence:"profit_tracker"` } func (s *Strategy) ID() string { @@ -330,8 +327,23 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats = types.NewTradeStats(s.Symbol) } - if s.ProfitTracker.CurrentProfitStats == nil { - s.ProfitTracker.InitOld(&s.ProfitStats, s.Market) + if s.ProfitTracker != nil { + if s.ProfitTracker.CurrentProfitStats == nil { + s.ProfitTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) + } + + // Add strategy parameters to report + if s.ProfitTracker.AccumulatedProfitReport != nil { + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) + } } // Interval profit report @@ -361,25 +373,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // AccountValueCalculator s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency) - // Accumulated profit report - if bbgo.IsBackTesting { - if s.AccumulatedProfitReport == nil { - s.AccumulatedProfitReport = &report.AccumulatedProfitReport{} - } - s.AccumulatedProfitReport.Initialize(s.Symbol, session, s.orderExecutor, s.TradeStats) - - // Add strategy parameters to report - s.AccumulatedProfitReport.AddExtraValue([2]string{"window", fmt.Sprintf("%d", s.Window)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)}) - } - // For drawing profitSlice := floats.Slice{1., 1.} price, _ := session.LastPrice(s.Symbol) @@ -527,10 +520,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - if bbgo.IsBackTesting { - // Output accumulated profit report - defer s.AccumulatedProfitReport.Output(s.Symbol) + // Output profit report + if s.ProfitTracker != nil { + if s.ProfitTracker.AccumulatedProfitReport != nil { + s.ProfitTracker.AccumulatedProfitReport.Output() + } + } + if bbgo.IsBackTesting { // Draw graph if s.DrawGraph { if err := s.Draw(&profitSlice, &cumProfitSlice); err != nil { From 07104aa68aa5094247e89b52179a6fe3b7a94a3e Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 18:32:44 +0800 Subject: [PATCH 1159/1392] feature/profitTracker: fix bugs --- config/supertrend.yaml | 8 ++++++++ pkg/bbgo/order_executor_general.go | 4 ++++ pkg/report/profit_report.go | 10 +++++----- pkg/report/profit_tracker.go | 9 +++++---- pkg/strategy/supertrend/strategy.go | 8 +++++--- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index e51af4c98a..6b20dd7a3f 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -112,3 +112,11 @@ exchangeStrategies: # If true, looking for lower lows in long position and higher highs in short position. If false, looking for # higher highs in long position and lower lows in short position oppositeDirectionAsPosition: false + + profitTracker: + Interval: 1d + Window: 30 + accumulatedProfitReport: + profitMAWindow: 60 + shortTermProfitWindow: 14 + tsvReportPath: res.tsv diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index decca0d697..d86d69a937 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -167,6 +167,10 @@ func (e *GeneralOrderExecutor) BindProfitTracker(profitTracker *report.ProfitTra e.session.Subscribe(types.KLineChannel, profitTracker.Market.Symbol, types.SubscribeOptions{Interval: profitTracker.Interval}) e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + if profit == nil { + return + } + profitTracker.AddProfit(*profit) }) diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index c338a99217..ff330a131c 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -12,7 +12,7 @@ import ( // AccumulatedProfitReport For accumulated profit report output type AccumulatedProfitReport struct { // ProfitMAWindow Accumulated profit SMA window - ProfitMAWindow int `json:"ProfitMAWindow"` + ProfitMAWindow int `json:"profitMAWindow"` // ShortTermProfitWindow The window to sum up the short-term profit ShortTermProfitWindow int `json:"shortTermProfitWindow"` @@ -84,7 +84,7 @@ func (r *AccumulatedProfitReport) AddTrade(trade types.Trade) { func (r *AccumulatedProfitReport) Rotate(ps *types.ProfitStats, ts *types.TradeStats) { // Accumulated profit - r.accumulatedProfit.Add(ps.AccumulatedNetProfit) + r.accumulatedProfit = r.accumulatedProfit.Add(ps.AccumulatedNetProfit) r.accumulatedProfitPerInterval.Update(r.accumulatedProfit.Float64()) // Profit of each interval @@ -120,12 +120,12 @@ func (r *AccumulatedProfitReport) Output() { "#", "Symbol", "Total Net Profit", - fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.Window), - fmt.Sprintf("%s %d Net Profit", r.Interval, r.ShortTermProfitWindow), + fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.ProfitMAWindow), + fmt.Sprintf("%s%d Net Profit", r.Interval, r.ShortTermProfitWindow), "accumulatedFee", "winRatio", "profitFactor", - fmt.Sprintf("%s %d Trades", r.Interval, r.Window), + fmt.Sprintf("%s%d Trades", r.Interval, r.Window), } for i := 0; i < len(r.strategyParameters); i++ { titles = append(titles, r.strategyParameters[i][0]) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 2bfad3338a..faac2b2e86 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -44,16 +44,17 @@ func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { // Rotate the tracker to make a new ProfitStats to record the profits func (p *ProfitTracker) Rotate() { + // Update report + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.Rotate(*p.CurrentProfitStats, p.tradeStats) + } + *p.CurrentProfitStats = types.NewProfitStats(p.Market) p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) // Truncate if len(p.ProfitStatsSlice) > p.Window { p.ProfitStatsSlice = p.ProfitStatsSlice[len(p.ProfitStatsSlice)-p.Window:] } - - if p.AccumulatedProfitReport != nil { - p.AccumulatedProfitReport.Rotate(*p.CurrentProfitStats, p.tradeStats) - } } func (p *ProfitTracker) AddProfit(profit types.Profit) { diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 946dcb11c7..7e8fb87f84 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -39,6 +39,8 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + ProfitTracker *report.ProfitTracker `json:"profitTracker"` + // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -101,8 +103,6 @@ type Strategy struct { // StrategyController bbgo.StrategyController - - ProfitTracker *report.ProfitTracker `json:"profitTracker" persistence:"profit_tracker"` } func (s *Strategy) ID() string { @@ -367,7 +367,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindProfitStats(s.ProfitStats) s.orderExecutor.BindTradeStats(s.TradeStats) - s.orderExecutor.BindProfitTracker(s.ProfitTracker) + if s.ProfitTracker != nil { + s.orderExecutor.BindProfitTracker(s.ProfitTracker) + } s.orderExecutor.Bind() // AccountValueCalculator From 54c5c8126670a50773e0c07b45cd3984146bcfdb Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 15:09:09 +0800 Subject: [PATCH 1160/1392] fix/profitTracker: fix typo in config --- config/supertrend.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 6b20dd7a3f..673d3f47e8 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -114,8 +114,8 @@ exchangeStrategies: oppositeDirectionAsPosition: false profitTracker: - Interval: 1d - Window: 30 + interval: 1d + window: 30 accumulatedProfitReport: profitMAWindow: 60 shortTermProfitWindow: 14 From e589a95c9708dfd70f2d34195f57b53bda2c189a Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 15:20:00 +0800 Subject: [PATCH 1161/1392] improve/profitTracker: do not bind in order executor --- pkg/bbgo/order_executor_general.go | 22 --------------- pkg/report/profit_tracker.go | 23 ++++++++++++++++ pkg/strategy/supertrend/strategy.go | 42 ++++++++++++++--------------- 3 files changed, 44 insertions(+), 43 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index d86d69a937..cee2aa5738 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -3,7 +3,6 @@ package bbgo import ( "context" "fmt" - "github.com/c9s/bbgo/pkg/report" "strings" "time" @@ -163,27 +162,6 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { }) } -func (e *GeneralOrderExecutor) BindProfitTracker(profitTracker *report.ProfitTracker) { - e.session.Subscribe(types.KLineChannel, profitTracker.Market.Symbol, types.SubscribeOptions{Interval: profitTracker.Interval}) - - e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { - if profit == nil { - return - } - - profitTracker.AddProfit(*profit) - }) - - e.tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - profitTracker.AddTrade(trade) - }) - - // Rotate profitStats slice - e.session.MarketDataStream.OnKLineClosed(types.KLineWith(profitTracker.Market.Symbol, profitTracker.Interval, func(kline types.KLine) { - profitTracker.Rotate() - })) -} - func (e *GeneralOrderExecutor) Bind() { e.activeMakerOrders.BindStream(e.session.UserDataStream) e.orderStore.BindStream(e.session.UserDataStream) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index faac2b2e86..7be3c86252 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -1,6 +1,8 @@ package report import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -42,6 +44,27 @@ func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { p.InitOld(market, &ps, ts) } +func (p *ProfitTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) { + session.Subscribe(types.KLineChannel, p.Market.Symbol, types.SubscribeOptions{Interval: p.Interval}) + + tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + if profit == nil { + return + } + + p.AddProfit(*profit) + }) + + tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + p.AddTrade(trade) + }) + + // Rotate profitStats slice + session.MarketDataStream.OnKLineClosed(types.KLineWith(p.Market.Symbol, p.Interval, func(kline types.KLine) { + p.Rotate() + })) +} + // Rotate the tracker to make a new ProfitStats to record the profits func (p *ProfitTracker) Rotate() { // Update report diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 7e8fb87f84..d5181e89c0 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -327,25 +327,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats = types.NewTradeStats(s.Symbol) } - if s.ProfitTracker != nil { - if s.ProfitTracker.CurrentProfitStats == nil { - s.ProfitTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) - } - - // Add strategy parameters to report - if s.ProfitTracker.AccumulatedProfitReport != nil { - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) - } - } - // Interval profit report if bbgo.IsBackTesting { startTime := s.Environment.StartTime() @@ -367,10 +348,29 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindProfitStats(s.ProfitStats) s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.Bind() + + // Setup profit tracker if s.ProfitTracker != nil { - s.orderExecutor.BindProfitTracker(s.ProfitTracker) + if s.ProfitTracker.CurrentProfitStats == nil { + s.ProfitTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) + } + + // Add strategy parameters to report + if s.ProfitTracker.AccumulatedProfitReport != nil { + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) + } + + s.ProfitTracker.Bind(s.session, s.orderExecutor.TradeCollector()) } - s.orderExecutor.Bind() // AccountValueCalculator s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency) From e13115737b984119751de7b5a78dfa680ee3db25 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 15:26:57 +0800 Subject: [PATCH 1162/1392] improve/profitTracker: subscribe kline in strategy Subscribe() --- pkg/report/profit_tracker.go | 6 ++++-- pkg/strategy/supertrend/strategy.go | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 7be3c86252..95e83341f0 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -20,6 +20,10 @@ type ProfitTracker struct { tradeStats *types.TradeStats } +func (p *ProfitTracker) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, p.Market.Symbol, types.SubscribeOptions{Interval: p.Interval}) +} + // InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market func (p *ProfitTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { p.Market = market @@ -45,8 +49,6 @@ func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { } func (p *ProfitTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) { - session.Subscribe(types.KLineChannel, p.Market.Symbol, types.SubscribeOptions{Interval: p.Interval}) - tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { if profit == nil { return diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index d5181e89c0..40eead9b08 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -130,6 +130,11 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LinearRegression.Interval}) s.ExitMethods.SetAndSubscribe(session, s) + + // Profit tracker + if s.ProfitTracker != nil { + s.ProfitTracker.Subscribe(session) + } } // Position control From 5a8a2e8631bd4145591b636ae902b3787ae073f6 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 15:37:19 +0800 Subject: [PATCH 1163/1392] improve/profitStatsTracker: rename ProfitTracker to ProfitStatsTracker --- config/supertrend.yaml | 2 +- pkg/report/profit_tracker.go | 16 ++++++------ pkg/strategy/supertrend/strategy.go | 40 ++++++++++++++--------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 673d3f47e8..99815245bd 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -113,7 +113,7 @@ exchangeStrategies: # higher highs in long position and lower lows in short position oppositeDirectionAsPosition: false - profitTracker: + profitStatsTracker: interval: 1d window: 30 accumulatedProfitReport: diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 95e83341f0..9a01710103 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -6,7 +6,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -type ProfitTracker struct { +type ProfitStatsTracker struct { types.IntervalWindow // Accumulated profit report @@ -20,12 +20,12 @@ type ProfitTracker struct { tradeStats *types.TradeStats } -func (p *ProfitTracker) Subscribe(session *bbgo.ExchangeSession) { +func (p *ProfitStatsTracker) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, p.Market.Symbol, types.SubscribeOptions{Interval: p.Interval}) } // InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market -func (p *ProfitTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { +func (p *ProfitStatsTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { p.Market = market if *ps == nil { @@ -43,12 +43,12 @@ func (p *ProfitTracker) InitOld(market types.Market, ps **types.ProfitStats, ts } // Init initialize the tracker with the given Market -func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { +func (p *ProfitStatsTracker) Init(market types.Market, ts *types.TradeStats) { ps := types.NewProfitStats(p.Market) p.InitOld(market, &ps, ts) } -func (p *ProfitTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) { +func (p *ProfitStatsTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) { tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { if profit == nil { return @@ -68,7 +68,7 @@ func (p *ProfitTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo } // Rotate the tracker to make a new ProfitStats to record the profits -func (p *ProfitTracker) Rotate() { +func (p *ProfitStatsTracker) Rotate() { // Update report if p.AccumulatedProfitReport != nil { p.AccumulatedProfitReport.Rotate(*p.CurrentProfitStats, p.tradeStats) @@ -82,11 +82,11 @@ func (p *ProfitTracker) Rotate() { } } -func (p *ProfitTracker) AddProfit(profit types.Profit) { +func (p *ProfitStatsTracker) AddProfit(profit types.Profit) { (*p.CurrentProfitStats).AddProfit(profit) } -func (p *ProfitTracker) AddTrade(trade types.Trade) { +func (p *ProfitStatsTracker) AddTrade(trade types.Trade) { (*p.CurrentProfitStats).AddTrade(trade) if p.AccumulatedProfitReport != nil { diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 40eead9b08..da904a064e 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -39,7 +39,7 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` - ProfitTracker *report.ProfitTracker `json:"profitTracker"` + ProfitStatsTracker *report.ProfitStatsTracker `json:"profitStatsTracker"` // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -132,8 +132,8 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { s.ExitMethods.SetAndSubscribe(session, s) // Profit tracker - if s.ProfitTracker != nil { - s.ProfitTracker.Subscribe(session) + if s.ProfitStatsTracker != nil { + s.ProfitStatsTracker.Subscribe(session) } } @@ -356,25 +356,25 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.Bind() // Setup profit tracker - if s.ProfitTracker != nil { - if s.ProfitTracker.CurrentProfitStats == nil { - s.ProfitTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) + if s.ProfitStatsTracker != nil { + if s.ProfitStatsTracker.CurrentProfitStats == nil { + s.ProfitStatsTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) } // Add strategy parameters to report - if s.ProfitTracker.AccumulatedProfitReport != nil { - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) + if s.ProfitStatsTracker.AccumulatedProfitReport != nil { + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) } - s.ProfitTracker.Bind(s.session, s.orderExecutor.TradeCollector()) + s.ProfitStatsTracker.Bind(s.session, s.orderExecutor.TradeCollector()) } // AccountValueCalculator @@ -528,9 +528,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se defer wg.Done() // Output profit report - if s.ProfitTracker != nil { - if s.ProfitTracker.AccumulatedProfitReport != nil { - s.ProfitTracker.AccumulatedProfitReport.Output() + if s.ProfitStatsTracker != nil { + if s.ProfitStatsTracker.AccumulatedProfitReport != nil { + s.ProfitStatsTracker.AccumulatedProfitReport.Output() } } From 1289d4c4ca9d6848d6b3c2ead79a9bc9e842cd71 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 15:50:18 +0800 Subject: [PATCH 1164/1392] improve/profitStatsTracker: temporarily remove lines relate to time in profit stats --- .../{profit_tracker.go => profit_stats_tracker.go} | 0 pkg/types/profit.go | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) rename pkg/report/{profit_tracker.go => profit_stats_tracker.go} (100%) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_stats_tracker.go similarity index 100% rename from pkg/report/profit_tracker.go rename to pkg/report/profit_stats_tracker.go diff --git a/pkg/types/profit.go b/pkg/types/profit.go index aa57b2e22d..c7742e0b8f 100644 --- a/pkg/types/profit.go +++ b/pkg/types/profit.go @@ -165,8 +165,8 @@ type ProfitStats struct { TodayGrossLoss fixedpoint.Value `json:"todayGrossLoss,omitempty"` TodaySince int64 `json:"todaySince,omitempty"` - startTime time.Time - endTime time.Time + //StartTime time.Time + //EndTime time.Time } func NewProfitStats(market Market) *ProfitStats { @@ -185,8 +185,8 @@ func NewProfitStats(market Market) *ProfitStats { TodayGrossProfit: fixedpoint.Zero, TodayGrossLoss: fixedpoint.Zero, TodaySince: 0, - startTime: time.Now().UTC(), - endTime: time.Now().UTC(), + //StartTime: time.Now().UTC(), + //EndTime: time.Now().UTC(), } } @@ -229,7 +229,7 @@ func (s *ProfitStats) AddProfit(profit Profit) { s.TodayGrossLoss = s.TodayGrossLoss.Add(profit.Profit) } - s.endTime = profit.TradedAt.UTC() + //s.EndTime = profit.TradedAt.UTC() } func (s *ProfitStats) AddTrade(trade Trade) { From 3935d5fec7b7d543a893b56637ab49e655f0a19d Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 16:01:20 +0800 Subject: [PATCH 1165/1392] fix/profitStatsTracker: market is initiated after strategy Subscribe() --- pkg/report/profit_stats_tracker.go | 4 ++-- pkg/strategy/supertrend/strategy.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/report/profit_stats_tracker.go b/pkg/report/profit_stats_tracker.go index 9a01710103..9ad4c636b9 100644 --- a/pkg/report/profit_stats_tracker.go +++ b/pkg/report/profit_stats_tracker.go @@ -20,8 +20,8 @@ type ProfitStatsTracker struct { tradeStats *types.TradeStats } -func (p *ProfitStatsTracker) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, p.Market.Symbol, types.SubscribeOptions{Interval: p.Interval}) +func (p *ProfitStatsTracker) Subscribe(session *bbgo.ExchangeSession, symbol string) { + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: p.Interval}) } // InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index da904a064e..ea5764a778 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -133,7 +133,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Profit tracker if s.ProfitStatsTracker != nil { - s.ProfitStatsTracker.Subscribe(session) + s.ProfitStatsTracker.Subscribe(session, s.Symbol) } } From 22ea3192c17a80c87acc5e951183e584e5a6661c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 16:33:27 +0800 Subject: [PATCH 1166/1392] ref/profitStatsTracker: TradeCollector is move to core pkg --- pkg/report/profit_stats_tracker.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/report/profit_stats_tracker.go b/pkg/report/profit_stats_tracker.go index 9ad4c636b9..7330cf16f6 100644 --- a/pkg/report/profit_stats_tracker.go +++ b/pkg/report/profit_stats_tracker.go @@ -2,6 +2,7 @@ package report import ( "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -48,7 +49,7 @@ func (p *ProfitStatsTracker) Init(market types.Market, ts *types.TradeStats) { p.InitOld(market, &ps, ts) } -func (p *ProfitStatsTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) { +func (p *ProfitStatsTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *core.TradeCollector) { tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { if profit == nil { return From 5853434aecf946ca11e11439ab415e957b50b930 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 10 Jul 2023 16:54:22 +0800 Subject: [PATCH 1167/1392] all: move v2 indicator to indicator/v2 --- pkg/bbgo/indicator_set.go | 64 +++++++++---------- pkg/indicator/ewma.go | 4 +- pkg/indicator/ewma_test.go | 8 +-- pkg/indicator/float64updater_callbacks.go | 15 ----- pkg/indicator/ghfilter_test.go | 2 +- pkg/indicator/mapper.go | 33 ---------- pkg/indicator/pivot.go | 12 +--- pkg/indicator/types.go | 22 ------- pkg/indicator/util.go | 2 +- pkg/indicator/v2.go | 19 ------ pkg/indicator/{v2_atr.go => v2/atr.go} | 2 +- .../{v2_atr_test.go => v2/atr_test.go} | 2 +- pkg/indicator/{v2_atrp.go => v2/atrp.go} | 8 ++- pkg/indicator/{v2_boll.go => v2/boll.go} | 20 +++--- pkg/indicator/v2/cma.go | 28 ++++++++ pkg/indicator/{v2_cross.go => v2/cross.go} | 9 +-- pkg/indicator/{v2_ewma.go => v2/ewma.go} | 12 ++-- .../{klinestream.go => v2/klines.go} | 8 ++- .../{ => v2}/klinestream_callbacks.go | 2 +- pkg/indicator/{v2_macd.go => v2/macd.go} | 8 ++- .../{v2_macd_test.go => v2/macd_test.go} | 10 ++- .../{v2_multiply.go => v2/multiply.go} | 19 +++--- pkg/indicator/v2/pivothigh.go | 34 ++++++++++ pkg/indicator/v2/pivotlow.go | 34 ++++++++++ pkg/indicator/{v2_price.go => v2/price.go} | 32 ++++------ pkg/indicator/{v2_rma.go => v2/rma.go} | 23 ++++--- pkg/indicator/{v2_rsi.go => v2/rsi.go} | 28 ++++++-- .../{v2_rsi_test.go => v2/rsi_test.go} | 8 +-- pkg/indicator/{v2_sma.go => v2/sma.go} | 16 +++-- pkg/indicator/{v2_stddev.go => v2/stddev.go} | 8 +-- pkg/indicator/{v2_stoch.go => v2/stoch.go} | 12 ++-- .../{v2_stoch_test.go => v2/stoch_test.go} | 4 +- .../{ => v2}/stochstream_callbacks.go | 2 +- .../{v2_subtract.go => v2/subtract.go} | 15 +++-- .../{v2_test.go => v2/subtract_test.go} | 10 +-- pkg/indicator/{v2_tr.go => v2/tr.go} | 6 +- .../{v2_tr_test.go => v2/tr_test.go} | 2 +- pkg/indicator/v2_cma.go | 23 ------- pkg/indicator/v2_pivothigh.go | 27 -------- pkg/indicator/v2_pivotlow.go | 27 -------- pkg/indicator/volatility.go | 4 +- pkg/indicator/vwap.go | 4 +- pkg/indicator/vwap_test.go | 2 +- pkg/risk/riskcontrol/circuit_break.go | 6 +- pkg/risk/riskcontrol/circuit_break_test.go | 5 +- pkg/strategy/bollmaker/dynamic_spread.go | 7 +- pkg/strategy/bollmaker/strategy.go | 6 +- pkg/strategy/bollmaker/trend.go | 6 +- .../factorzoo/factors/price_mean_reversion.go | 4 +- .../factors/price_volume_divergence.go | 4 +- pkg/strategy/factorzoo/factors/return_rate.go | 2 +- pkg/strategy/fmaker/A18.go | 2 +- pkg/strategy/fmaker/A2.go | 2 +- pkg/strategy/fmaker/A3.go | 2 +- pkg/strategy/fmaker/A34.go | 2 +- pkg/strategy/fmaker/R.go | 2 +- pkg/strategy/fmaker/S0.go | 2 +- pkg/strategy/fmaker/S1.go | 2 +- pkg/strategy/fmaker/S2.go | 2 +- pkg/strategy/fmaker/S3.go | 2 +- pkg/strategy/fmaker/S4.go | 2 +- pkg/strategy/fmaker/S5.go | 2 +- pkg/strategy/fmaker/S6.go | 2 +- pkg/strategy/fmaker/S7.go | 2 +- pkg/strategy/harmonic/shark.go | 2 +- pkg/strategy/irr/neg_return_rate.go | 2 +- pkg/strategy/rsicross/strategy.go | 10 +-- pkg/strategy/scmaker/intensity.go | 14 ++-- pkg/strategy/scmaker/strategy.go | 20 +++--- pkg/{indicator => types}/float64updater.go | 2 +- pkg/types/float64updater_callbacks.go | 15 +++++ pkg/types/kline.go | 38 +++++++++++ .../series_float64.go} | 40 ++++++++---- 73 files changed, 443 insertions(+), 396 deletions(-) delete mode 100644 pkg/indicator/float64updater_callbacks.go delete mode 100644 pkg/indicator/mapper.go delete mode 100644 pkg/indicator/types.go delete mode 100644 pkg/indicator/v2.go rename pkg/indicator/{v2_atr.go => v2/atr.go} (91%) rename pkg/indicator/{v2_atr_test.go => v2/atr_test.go} (99%) rename pkg/indicator/{v2_atrp.go => v2/atrp.go} (72%) rename pkg/indicator/{v2_boll.go => v2/boll.go} (69%) create mode 100644 pkg/indicator/v2/cma.go rename pkg/indicator/{v2_cross.go => v2/cross.go} (83%) rename pkg/indicator/{v2_ewma.go => v2/ewma.go} (59%) rename pkg/indicator/{klinestream.go => v2/klines.go} (90%) rename pkg/indicator/{ => v2}/klinestream_callbacks.go (94%) rename pkg/indicator/{v2_macd.go => v2/macd.go} (80%) rename pkg/indicator/{v2_macd_test.go => v2/macd_test.go} (87%) rename pkg/indicator/{v2_multiply.go => v2/multiply.go} (57%) create mode 100644 pkg/indicator/v2/pivothigh.go create mode 100644 pkg/indicator/v2/pivotlow.go rename pkg/indicator/{v2_price.go => v2/price.go} (59%) rename pkg/indicator/{v2_rma.go => v2/rma.go} (66%) rename pkg/indicator/{v2_rsi.go => v2/rsi.go} (66%) rename pkg/indicator/{v2_rsi_test.go => v2/rsi_test.go} (86%) rename pkg/indicator/{v2_sma.go => v2/sma.go} (57%) rename pkg/indicator/{v2_stddev.go => v2/stddev.go} (71%) rename pkg/indicator/{v2_stoch.go => v2/stoch.go} (88%) rename pkg/indicator/{v2_stoch_test.go => v2/stoch_test.go} (99%) rename pkg/indicator/{ => v2}/stochstream_callbacks.go (93%) rename pkg/indicator/{v2_subtract.go => v2/subtract.go} (71%) rename pkg/indicator/{v2_test.go => v2/subtract_test.go} (72%) rename pkg/indicator/{v2_tr.go => v2/tr.go} (89%) rename pkg/indicator/{v2_tr_test.go => v2/tr_test.go} (99%) delete mode 100644 pkg/indicator/v2_cma.go delete mode 100644 pkg/indicator/v2_pivothigh.go delete mode 100644 pkg/indicator/v2_pivotlow.go rename pkg/{indicator => types}/float64updater.go (86%) create mode 100644 pkg/types/float64updater_callbacks.go rename pkg/{indicator/float64series.go => types/series_float64.go} (73%) diff --git a/pkg/bbgo/indicator_set.go b/pkg/bbgo/indicator_set.go index 8dfa08a48c..0d5dbd3636 100644 --- a/pkg/bbgo/indicator_set.go +++ b/pkg/bbgo/indicator_set.go @@ -3,7 +3,7 @@ package bbgo import ( "github.com/sirupsen/logrus" - "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/types" ) @@ -16,8 +16,8 @@ type IndicatorSet struct { store *MarketDataStore // caches - kLines map[types.Interval]*indicator.KLineStream - closePrices map[types.Interval]*indicator.PriceStream + kLines map[types.Interval]*indicatorv2.KLineStream + closePrices map[types.Interval]*indicatorv2.PriceStream } func NewIndicatorSet(symbol string, stream types.Stream, store *MarketDataStore) *IndicatorSet { @@ -26,17 +26,17 @@ func NewIndicatorSet(symbol string, stream types.Stream, store *MarketDataStore) store: store, stream: stream, - kLines: make(map[types.Interval]*indicator.KLineStream), - closePrices: make(map[types.Interval]*indicator.PriceStream), + kLines: make(map[types.Interval]*indicatorv2.KLineStream), + closePrices: make(map[types.Interval]*indicatorv2.PriceStream), } } -func (i *IndicatorSet) KLines(interval types.Interval) *indicator.KLineStream { +func (i *IndicatorSet) KLines(interval types.Interval) *indicatorv2.KLineStream { if kLines, ok := i.kLines[interval]; ok { return kLines } - kLines := indicator.KLines(i.stream, i.Symbol, interval) + kLines := indicatorv2.KLines(i.stream, i.Symbol, interval) if kLinesWindow, ok := i.store.KLinesOfInterval(interval); ok { kLines.BackFill(*kLinesWindow) } else { @@ -47,60 +47,60 @@ func (i *IndicatorSet) KLines(interval types.Interval) *indicator.KLineStream { return kLines } -func (i *IndicatorSet) OPEN(interval types.Interval) *indicator.PriceStream { - return indicator.OpenPrices(i.KLines(interval)) +func (i *IndicatorSet) OPEN(interval types.Interval) *indicatorv2.PriceStream { + return indicatorv2.OpenPrices(i.KLines(interval)) } -func (i *IndicatorSet) HIGH(interval types.Interval) *indicator.PriceStream { - return indicator.HighPrices(i.KLines(interval)) +func (i *IndicatorSet) HIGH(interval types.Interval) *indicatorv2.PriceStream { + return indicatorv2.HighPrices(i.KLines(interval)) } -func (i *IndicatorSet) LOW(interval types.Interval) *indicator.PriceStream { - return indicator.LowPrices(i.KLines(interval)) +func (i *IndicatorSet) LOW(interval types.Interval) *indicatorv2.PriceStream { + return indicatorv2.LowPrices(i.KLines(interval)) } -func (i *IndicatorSet) CLOSE(interval types.Interval) *indicator.PriceStream { +func (i *IndicatorSet) CLOSE(interval types.Interval) *indicatorv2.PriceStream { if closePrices, ok := i.closePrices[interval]; ok { return closePrices } - closePrices := indicator.ClosePrices(i.KLines(interval)) + closePrices := indicatorv2.ClosePrices(i.KLines(interval)) i.closePrices[interval] = closePrices return closePrices } -func (i *IndicatorSet) VOLUME(interval types.Interval) *indicator.PriceStream { - return indicator.Volumes(i.KLines(interval)) +func (i *IndicatorSet) VOLUME(interval types.Interval) *indicatorv2.PriceStream { + return indicatorv2.Volumes(i.KLines(interval)) } -func (i *IndicatorSet) RSI(iw types.IntervalWindow) *indicator.RSIStream { - return indicator.RSI2(i.CLOSE(iw.Interval), iw.Window) +func (i *IndicatorSet) RSI(iw types.IntervalWindow) *indicatorv2.RSIStream { + return indicatorv2.RSI2(i.CLOSE(iw.Interval), iw.Window) } -func (i *IndicatorSet) EMA(iw types.IntervalWindow) *indicator.EWMAStream { +func (i *IndicatorSet) EMA(iw types.IntervalWindow) *indicatorv2.EWMAStream { return i.EWMA(iw) } -func (i *IndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMAStream { - return indicator.EWMA2(i.CLOSE(iw.Interval), iw.Window) +func (i *IndicatorSet) EWMA(iw types.IntervalWindow) *indicatorv2.EWMAStream { + return indicatorv2.EWMA2(i.CLOSE(iw.Interval), iw.Window) } -func (i *IndicatorSet) STOCH(iw types.IntervalWindow, dPeriod int) *indicator.StochStream { - return indicator.Stoch2(i.KLines(iw.Interval), iw.Window, dPeriod) +func (i *IndicatorSet) STOCH(iw types.IntervalWindow, dPeriod int) *indicatorv2.StochStream { + return indicatorv2.Stoch(i.KLines(iw.Interval), iw.Window, dPeriod) } -func (i *IndicatorSet) BOLL(iw types.IntervalWindow, k float64) *indicator.BOLLStream { - return indicator.BOLL2(i.CLOSE(iw.Interval), iw.Window, k) +func (i *IndicatorSet) BOLL(iw types.IntervalWindow, k float64) *indicatorv2.BOLLStream { + return indicatorv2.BOLL(i.CLOSE(iw.Interval), iw.Window, k) } -func (i *IndicatorSet) MACD(interval types.Interval, shortWindow, longWindow, signalWindow int) *indicator.MACDStream { - return indicator.MACD2(i.CLOSE(interval), shortWindow, longWindow, signalWindow) +func (i *IndicatorSet) MACD(interval types.Interval, shortWindow, longWindow, signalWindow int) *indicatorv2.MACDStream { + return indicatorv2.MACD2(i.CLOSE(interval), shortWindow, longWindow, signalWindow) } -func (i *IndicatorSet) ATR(interval types.Interval, window int) *indicator.ATRStream { - return indicator.ATR2(i.KLines(interval), window) +func (i *IndicatorSet) ATR(interval types.Interval, window int) *indicatorv2.ATRStream { + return indicatorv2.ATR2(i.KLines(interval), window) } -func (i *IndicatorSet) ATRP(interval types.Interval, window int) *indicator.ATRPStream { - return indicator.ATRP2(i.KLines(interval), window) +func (i *IndicatorSet) ATRP(interval types.Interval, window int) *indicatorv2.ATRPStream { + return indicatorv2.ATRP2(i.KLines(interval), window) } diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index 8d7b136989..7abce2f83a 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -76,9 +76,9 @@ func (inc *EWMA) PushK(k types.KLine) { inc.EmitUpdate(inc.Last(0)) } -func CalculateKLinesEMA(allKLines []types.KLine, priceF KLineValueMapper, window int) float64 { +func CalculateKLinesEMA(allKLines []types.KLine, priceF types.KLineValueMapper, window int) float64 { var multiplier = 2.0 / (float64(window) + 1) - return ewma(MapKLinePrice(allKLines, priceF), multiplier) + return ewma(types.MapKLinePrice(allKLines, priceF), multiplier) } // see https://www.investopedia.com/ask/answers/122314/what-exponential-moving-average-ema-formula-and-how-ema-calculated.asp diff --git a/pkg/indicator/ewma_test.go b/pkg/indicator/ewma_test.go index 23bc812853..3bea83ebfa 100644 --- a/pkg/indicator/ewma_test.go +++ b/pkg/indicator/ewma_test.go @@ -1027,7 +1027,7 @@ func buildKLines(prices []fixedpoint.Value) (klines []types.KLine) { func Test_calculateEWMA(t *testing.T) { type args struct { allKLines []types.KLine - priceF KLineValueMapper + priceF types.KLineValueMapper window int } var input []fixedpoint.Value @@ -1043,7 +1043,7 @@ func Test_calculateEWMA(t *testing.T) { name: "ETHUSDT EMA 7", args: args{ allKLines: buildKLines(input), - priceF: KLineClosePriceMapper, + priceF: types.KLineClosePriceMapper, window: 7, }, want: 571.72, // with open price, binance desktop returns 571.45, trading view returns 570.8957, for close price, binance mobile returns 571.72 @@ -1052,7 +1052,7 @@ func Test_calculateEWMA(t *testing.T) { name: "ETHUSDT EMA 25", args: args{ allKLines: buildKLines(input), - priceF: KLineClosePriceMapper, + priceF: types.KLineClosePriceMapper, window: 25, }, want: 571.30, @@ -1061,7 +1061,7 @@ func Test_calculateEWMA(t *testing.T) { name: "ETHUSDT EMA 99", args: args{ allKLines: buildKLines(input), - priceF: KLineClosePriceMapper, + priceF: types.KLineClosePriceMapper, window: 99, }, want: 577.62, // binance mobile uses 577.58 diff --git a/pkg/indicator/float64updater_callbacks.go b/pkg/indicator/float64updater_callbacks.go deleted file mode 100644 index 3226608630..0000000000 --- a/pkg/indicator/float64updater_callbacks.go +++ /dev/null @@ -1,15 +0,0 @@ -// Code generated by "callbackgen -type Float64Updater"; DO NOT EDIT. - -package indicator - -import () - -func (f *Float64Updater) OnUpdate(cb func(v float64)) { - f.updateCallbacks = append(f.updateCallbacks, cb) -} - -func (f *Float64Updater) EmitUpdate(v float64) { - for _, cb := range f.updateCallbacks { - cb(v) - } -} diff --git a/pkg/indicator/ghfilter_test.go b/pkg/indicator/ghfilter_test.go index 8494388590..d93553d964 100644 --- a/pkg/indicator/ghfilter_test.go +++ b/pkg/indicator/ghfilter_test.go @@ -6070,7 +6070,7 @@ func Test_GHFilter(t *testing.T) { func Test_GHFilterEstimationAccurate(t *testing.T) { type args struct { allKLines []types.KLine - priceF KLineValueMapper + priceF types.KLineValueMapper window int } var klines []types.KLine diff --git a/pkg/indicator/mapper.go b/pkg/indicator/mapper.go deleted file mode 100644 index e169bef168..0000000000 --- a/pkg/indicator/mapper.go +++ /dev/null @@ -1,33 +0,0 @@ -package indicator - -import "github.com/c9s/bbgo/pkg/types" - -type KLineValueMapper func(k types.KLine) float64 - -func KLineOpenPriceMapper(k types.KLine) float64 { - return k.Open.Float64() -} - -func KLineClosePriceMapper(k types.KLine) float64 { - return k.Close.Float64() -} - -func KLineTypicalPriceMapper(k types.KLine) float64 { - return (k.High.Float64() + k.Low.Float64() + k.Close.Float64()) / 3. -} - -func KLinePriceVolumeMapper(k types.KLine) float64 { - return k.Close.Mul(k.Volume).Float64() -} - -func KLineVolumeMapper(k types.KLine) float64 { - return k.Volume.Float64() -} - -func MapKLinePrice(kLines []types.KLine, f KLineValueMapper) (prices []float64) { - for _, k := range kLines { - prices = append(prices, f(k)) - } - - return prices -} diff --git a/pkg/indicator/pivot.go b/pkg/indicator/pivot.go index c32db25cd6..617d8347e2 100644 --- a/pkg/indicator/pivot.go +++ b/pkg/indicator/pivot.go @@ -52,7 +52,7 @@ func (inc *Pivot) CalculateAndUpdate(klines []types.KLine) { recentT := klines[end-(inc.Window-1) : end+1] - l, h, err := calculatePivot(recentT, inc.Window, KLineLowPriceMapper, KLineHighPriceMapper) + l, h, err := calculatePivot(recentT, inc.Window, types.KLineLowPriceMapper, types.KLineHighPriceMapper) if err != nil { log.WithError(err).Error("can not calculate pivots") return @@ -90,7 +90,7 @@ func (inc *Pivot) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } -func calculatePivot(klines []types.KLine, window int, valLow KLineValueMapper, valHigh KLineValueMapper) (float64, float64, error) { +func calculatePivot(klines []types.KLine, window int, valLow types.KLineValueMapper, valHigh types.KLineValueMapper) (float64, float64, error) { length := len(klines) if length == 0 || length < window { return 0., 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) @@ -115,11 +115,3 @@ func calculatePivot(klines []types.KLine, window int, valLow KLineValueMapper, v return pl, ph, nil } - -func KLineLowPriceMapper(k types.KLine) float64 { - return k.Low.Float64() -} - -func KLineHighPriceMapper(k types.KLine) float64 { - return k.High.Float64() -} diff --git a/pkg/indicator/types.go b/pkg/indicator/types.go deleted file mode 100644 index 7e750b9da2..0000000000 --- a/pkg/indicator/types.go +++ /dev/null @@ -1,22 +0,0 @@ -package indicator - -import "github.com/c9s/bbgo/pkg/types" - -type Float64Calculator interface { - Calculate(x float64) float64 - PushAndEmit(x float64) -} - -type Float64Source interface { - types.Series - OnUpdate(f func(v float64)) -} - -type Float64Subscription interface { - types.Series - AddSubscriber(f func(v float64)) -} - -type Float64Truncator interface { - Truncate() -} diff --git a/pkg/indicator/util.go b/pkg/indicator/util.go index f0a76d2b04..ce5c209b2d 100644 --- a/pkg/indicator/util.go +++ b/pkg/indicator/util.go @@ -7,7 +7,7 @@ func max(x, y int) int { return y } -func min(x, y int) int { +func Min(x, y int) int { if x < y { return x } diff --git a/pkg/indicator/v2.go b/pkg/indicator/v2.go deleted file mode 100644 index 80fd680957..0000000000 --- a/pkg/indicator/v2.go +++ /dev/null @@ -1,19 +0,0 @@ -package indicator - -/* -NEW INDICATOR DESIGN: - -klines := kLines(marketDataStream) -closePrices := closePrices(klines) -macd := MACD(klines, {Fast: 12, Slow: 10}) - -equals to: - -klines := KLines(marketDataStream) -closePrices := ClosePrice(klines) -fastEMA := EMA(closePrices, 7) -slowEMA := EMA(closePrices, 25) -macd := Subtract(fastEMA, slowEMA) -signal := EMA(macd, 16) -histogram := Subtract(macd, signal) -*/ diff --git a/pkg/indicator/v2_atr.go b/pkg/indicator/v2/atr.go similarity index 91% rename from pkg/indicator/v2_atr.go rename to pkg/indicator/v2/atr.go index fac3e8e060..da92c5c5b8 100644 --- a/pkg/indicator/v2_atr.go +++ b/pkg/indicator/v2/atr.go @@ -1,4 +1,4 @@ -package indicator +package indicatorv2 type ATRStream struct { // embedded struct diff --git a/pkg/indicator/v2_atr_test.go b/pkg/indicator/v2/atr_test.go similarity index 99% rename from pkg/indicator/v2_atr_test.go rename to pkg/indicator/v2/atr_test.go index 23b3cc9702..7beb2be4c0 100644 --- a/pkg/indicator/v2_atr_test.go +++ b/pkg/indicator/v2/atr_test.go @@ -1,4 +1,4 @@ -package indicator +package indicatorv2 import ( "encoding/json" diff --git a/pkg/indicator/v2_atrp.go b/pkg/indicator/v2/atrp.go similarity index 72% rename from pkg/indicator/v2_atrp.go rename to pkg/indicator/v2/atrp.go index bb9b982d36..e22810ff78 100644 --- a/pkg/indicator/v2_atrp.go +++ b/pkg/indicator/v2/atrp.go @@ -1,12 +1,14 @@ -package indicator +package indicatorv2 + +import "github.com/c9s/bbgo/pkg/types" type ATRPStream struct { - *Float64Series + *types.Float64Series } func ATRP2(source KLineSubscription, window int) *ATRPStream { s := &ATRPStream{ - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), } tr := TR2(source) atr := RMA2(tr, window, true) diff --git a/pkg/indicator/v2_boll.go b/pkg/indicator/v2/boll.go similarity index 69% rename from pkg/indicator/v2_boll.go rename to pkg/indicator/v2/boll.go index ee0dfd15b2..eef85c513b 100644 --- a/pkg/indicator/v2_boll.go +++ b/pkg/indicator/v2/boll.go @@ -1,10 +1,14 @@ -package indicator +package indicatorv2 + +import ( + "github.com/c9s/bbgo/pkg/types" +) type BOLLStream struct { // the band series - *Float64Series + *types.Float64Series - UpBand, DownBand *Float64Series + UpBand, DownBand *types.Float64Series window int k float64 @@ -20,15 +24,15 @@ type BOLLStream struct { // // -> calculate SMA // -> calculate stdDev -> calculate bandWidth -> get latest SMA -> upBand, downBand -func BOLL2(source Float64Source, window int, k float64) *BOLLStream { +func BOLL(source types.Float64Source, window int, k float64) *BOLLStream { // bind these indicators before our main calculator sma := SMA2(source, window) - stdDev := StdDev2(source, window) + stdDev := StdDev(source, window) s := &BOLLStream{ - Float64Series: NewFloat64Series(), - UpBand: NewFloat64Series(), - DownBand: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), + UpBand: types.NewFloat64Series(), + DownBand: types.NewFloat64Series(), window: window, k: k, SMA: sma, diff --git a/pkg/indicator/v2/cma.go b/pkg/indicator/v2/cma.go new file mode 100644 index 0000000000..9d0cbea648 --- /dev/null +++ b/pkg/indicator/v2/cma.go @@ -0,0 +1,28 @@ +package indicatorv2 + +import ( + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type CMAStream struct { + *types.Float64Series +} + +func CMA2(source types.Float64Source) *CMAStream { + s := &CMAStream{ + Float64Series: types.NewFloat64Series(), + } + s.Bind(source, s) + return s +} + +func (s *CMAStream) Calculate(x float64) float64 { + l := float64(s.Slice.Length()) + cma := (s.Slice.Last(0)*l + x) / (l + 1.) + return cma +} + +func (s *CMAStream) Truncate() { + s.Slice.Truncate(indicator.MaxNumOfEWMA) +} diff --git a/pkg/indicator/v2_cross.go b/pkg/indicator/v2/cross.go similarity index 83% rename from pkg/indicator/v2_cross.go rename to pkg/indicator/v2/cross.go index 835e87c302..8f1b39767e 100644 --- a/pkg/indicator/v2_cross.go +++ b/pkg/indicator/v2/cross.go @@ -1,7 +1,8 @@ -package indicator +package indicatorv2 import ( "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" ) type CrossType float64 @@ -13,7 +14,7 @@ const ( // CrossStream subscribes 2 upstreams, and calculate the cross signal type CrossStream struct { - *Float64Series + *types.Float64Series a, b floats.Slice } @@ -21,9 +22,9 @@ type CrossStream struct { // Cross creates the CrossStream object: // // cross := Cross(fastEWMA, slowEWMA) -func Cross(a, b Float64Source) *CrossStream { +func Cross(a, b types.Float64Source) *CrossStream { s := &CrossStream{ - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), } a.OnUpdate(func(v float64) { s.a.Push(v) diff --git a/pkg/indicator/v2_ewma.go b/pkg/indicator/v2/ewma.go similarity index 59% rename from pkg/indicator/v2_ewma.go rename to pkg/indicator/v2/ewma.go index 16feb99010..fdd9745f0d 100644 --- a/pkg/indicator/v2_ewma.go +++ b/pkg/indicator/v2/ewma.go @@ -1,15 +1,17 @@ -package indicator +package indicatorv2 + +import "github.com/c9s/bbgo/pkg/types" type EWMAStream struct { - *Float64Series + *types.Float64Series window int multiplier float64 } -func EWMA2(source Float64Source, window int) *EWMAStream { +func EWMA2(source types.Float64Source, window int) *EWMAStream { s := &EWMAStream{ - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), window: window, multiplier: 2.0 / float64(1+window), } @@ -18,7 +20,7 @@ func EWMA2(source Float64Source, window int) *EWMAStream { } func (s *EWMAStream) Calculate(v float64) float64 { - last := s.slice.Last(0) + last := s.Slice.Last(0) if last == 0.0 { return v } diff --git a/pkg/indicator/klinestream.go b/pkg/indicator/v2/klines.go similarity index 90% rename from pkg/indicator/klinestream.go rename to pkg/indicator/v2/klines.go index 11215b3e7f..b61d1199d9 100644 --- a/pkg/indicator/klinestream.go +++ b/pkg/indicator/v2/klines.go @@ -1,4 +1,4 @@ -package indicator +package indicatorv2 import "github.com/c9s/bbgo/pkg/types" @@ -60,3 +60,9 @@ func KLines(source types.Stream, symbol string, interval types.Interval) *KLineS return s } + +type KLineSubscription interface { + AddSubscriber(f func(k types.KLine)) + Length() int + Last(i int) *types.KLine +} diff --git a/pkg/indicator/klinestream_callbacks.go b/pkg/indicator/v2/klinestream_callbacks.go similarity index 94% rename from pkg/indicator/klinestream_callbacks.go rename to pkg/indicator/v2/klinestream_callbacks.go index eb6e59dbbb..6cf2e46ce9 100644 --- a/pkg/indicator/klinestream_callbacks.go +++ b/pkg/indicator/v2/klinestream_callbacks.go @@ -1,6 +1,6 @@ // Code generated by "callbackgen -type KLineStream"; DO NOT EDIT. -package indicator +package indicatorv2 import ( "github.com/c9s/bbgo/pkg/types" diff --git a/pkg/indicator/v2_macd.go b/pkg/indicator/v2/macd.go similarity index 80% rename from pkg/indicator/v2_macd.go rename to pkg/indicator/v2/macd.go index de62cd4964..1679cf6c09 100644 --- a/pkg/indicator/v2_macd.go +++ b/pkg/indicator/v2/macd.go @@ -1,4 +1,8 @@ -package indicator +package indicatorv2 + +import ( + "github.com/c9s/bbgo/pkg/types" +) type MACDStream struct { *SubtractStream @@ -9,7 +13,7 @@ type MACDStream struct { Histogram *SubtractStream } -func MACD2(source Float64Source, shortWindow, longWindow, signalWindow int) *MACDStream { +func MACD2(source types.Float64Source, shortWindow, longWindow, signalWindow int) *MACDStream { // bind and calculate these first fastEWMA := EWMA2(source, shortWindow) slowEWMA := EWMA2(source, longWindow) diff --git a/pkg/indicator/v2_macd_test.go b/pkg/indicator/v2/macd_test.go similarity index 87% rename from pkg/indicator/v2_macd_test.go rename to pkg/indicator/v2/macd_test.go index 6f86d0ffa0..3b6b72405c 100644 --- a/pkg/indicator/v2_macd_test.go +++ b/pkg/indicator/v2/macd_test.go @@ -1,4 +1,4 @@ -package indicator +package indicatorv2 import ( "encoding/json" @@ -21,6 +21,14 @@ fast = s.ewm(span=12, adjust=False).mean() print(fast - slow) */ +func buildKLines(prices []fixedpoint.Value) (klines []types.KLine) { + for _, p := range prices { + klines = append(klines, types.KLine{Close: p}) + } + + return klines +} + func Test_MACD2(t *testing.T) { var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) var input []fixedpoint.Value diff --git a/pkg/indicator/v2_multiply.go b/pkg/indicator/v2/multiply.go similarity index 57% rename from pkg/indicator/v2_multiply.go rename to pkg/indicator/v2/multiply.go index ee76bdc001..89543685b5 100644 --- a/pkg/indicator/v2_multiply.go +++ b/pkg/indicator/v2/multiply.go @@ -1,15 +1,18 @@ -package indicator +package indicatorv2 -import "github.com/c9s/bbgo/pkg/datatype/floats" +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) type MultiplyStream struct { - *Float64Series + *types.Float64Series a, b floats.Slice } -func Multiply(a, b Float64Source) *MultiplyStream { +func Multiply(a, b types.Float64Source) *MultiplyStream { s := &MultiplyStream{ - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), } a.OnUpdate(func(v float64) { @@ -29,13 +32,13 @@ func (s *MultiplyStream) calculate() { return } - if s.a.Length() > s.slice.Length() { - var numNewElems = s.a.Length() - s.slice.Length() + if s.a.Length() > s.Slice.Length() { + var numNewElems = s.a.Length() - s.Slice.Length() var tailA = s.a.Tail(numNewElems) var tailB = s.b.Tail(numNewElems) var tailC = tailA.Mul(tailB) for _, f := range tailC { - s.slice.Push(f) + s.Slice.Push(f) s.EmitUpdate(f) } } diff --git a/pkg/indicator/v2/pivothigh.go b/pkg/indicator/v2/pivothigh.go new file mode 100644 index 0000000000..74267ab885 --- /dev/null +++ b/pkg/indicator/v2/pivothigh.go @@ -0,0 +1,34 @@ +package indicatorv2 + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +type PivotHighStream struct { + *types.Float64Series + rawValues floats.Slice + window, rightWindow int +} + +func PivotHigh2(source types.Float64Source, window, rightWindow int) *PivotHighStream { + s := &PivotHighStream{ + Float64Series: types.NewFloat64Series(), + window: window, + rightWindow: rightWindow, + } + + s.Subscribe(source, func(x float64) { + s.rawValues.Push(x) + if low, ok := s.calculatePivotHigh(s.rawValues, s.window, s.rightWindow); ok { + s.PushAndEmit(low) + } + }) + return s +} + +func (s *PivotHighStream) calculatePivotHigh(highs floats.Slice, left, right int) (float64, bool) { + return floats.FindPivot(highs, left, right, func(a, pivot float64) bool { + return a < pivot + }) +} diff --git a/pkg/indicator/v2/pivotlow.go b/pkg/indicator/v2/pivotlow.go new file mode 100644 index 0000000000..cdd7aadded --- /dev/null +++ b/pkg/indicator/v2/pivotlow.go @@ -0,0 +1,34 @@ +package indicatorv2 + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +type PivotLowStream struct { + *types.Float64Series + rawValues floats.Slice + window, rightWindow int +} + +func PivotLow(source types.Float64Source, window, rightWindow int) *PivotLowStream { + s := &PivotLowStream{ + Float64Series: types.NewFloat64Series(), + window: window, + rightWindow: rightWindow, + } + + s.Subscribe(source, func(x float64) { + s.rawValues.Push(x) + if low, ok := s.calculatePivotLow(s.rawValues, s.window, s.rightWindow); ok { + s.PushAndEmit(low) + } + }) + return s +} + +func (s *PivotLowStream) calculatePivotLow(lows floats.Slice, left, right int) (float64, bool) { + return floats.FindPivot(lows, left, right, func(a, pivot float64) bool { + return a > pivot + }) +} diff --git a/pkg/indicator/v2_price.go b/pkg/indicator/v2/price.go similarity index 59% rename from pkg/indicator/v2_price.go rename to pkg/indicator/v2/price.go index d4c546e57f..7f261a14ed 100644 --- a/pkg/indicator/v2_price.go +++ b/pkg/indicator/v2/price.go @@ -1,24 +1,18 @@ -package indicator +package indicatorv2 import ( "github.com/c9s/bbgo/pkg/types" ) -type KLineSubscription interface { - AddSubscriber(f func(k types.KLine)) - Length() int - Last(i int) *types.KLine -} - type PriceStream struct { - *Float64Series + *types.Float64Series - mapper KLineValueMapper + mapper types.KLineValueMapper } -func Price(source KLineSubscription, mapper KLineValueMapper) *PriceStream { +func Price(source KLineSubscription, mapper types.KLineValueMapper) *PriceStream { s := &PriceStream{ - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), mapper: mapper, } @@ -37,37 +31,37 @@ func Price(source KLineSubscription, mapper KLineValueMapper) *PriceStream { func (s *PriceStream) AddSubscriber(f func(v float64)) { s.OnUpdate(f) - if len(s.slice) == 0 { + if len(s.Slice) == 0 { return } // push historical value to the subscriber - for _, v := range s.slice { + for _, v := range s.Slice { f(v) } } func (s *PriceStream) PushAndEmit(v float64) { - s.slice.Push(v) + s.Slice.Push(v) s.EmitUpdate(v) } func ClosePrices(source KLineSubscription) *PriceStream { - return Price(source, KLineClosePriceMapper) + return Price(source, types.KLineClosePriceMapper) } func LowPrices(source KLineSubscription) *PriceStream { - return Price(source, KLineLowPriceMapper) + return Price(source, types.KLineLowPriceMapper) } func HighPrices(source KLineSubscription) *PriceStream { - return Price(source, KLineHighPriceMapper) + return Price(source, types.KLineHighPriceMapper) } func OpenPrices(source KLineSubscription) *PriceStream { - return Price(source, KLineOpenPriceMapper) + return Price(source, types.KLineOpenPriceMapper) } func Volumes(source KLineSubscription) *PriceStream { - return Price(source, KLineVolumeMapper) + return Price(source, types.KLineVolumeMapper) } diff --git a/pkg/indicator/v2_rma.go b/pkg/indicator/v2/rma.go similarity index 66% rename from pkg/indicator/v2_rma.go rename to pkg/indicator/v2/rma.go index 0464ad96d7..247b469f9f 100644 --- a/pkg/indicator/v2_rma.go +++ b/pkg/indicator/v2/rma.go @@ -1,8 +1,15 @@ -package indicator +package indicatorv2 + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +const MaxNumOfRMA = 1000 +const MaxNumOfRMATruncateSize = 500 type RMAStream struct { // embedded structs - *Float64Series + *types.Float64Series // config fields Adjust bool @@ -12,9 +19,9 @@ type RMAStream struct { sum, previous float64 } -func RMA2(source Float64Source, window int, adjust bool) *RMAStream { +func RMA2(source types.Float64Source, window int, adjust bool) *RMAStream { s := &RMAStream{ - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), window: window, Adjust: adjust, } @@ -41,17 +48,17 @@ func (s *RMAStream) Calculate(x float64) float64 { if s.counter < s.window { // we can use x, but we need to use 0. to make the same behavior as the result from python pandas_ta - s.slice.Push(0) + s.Slice.Push(0) } - s.slice.Push(tmp) + s.Slice.Push(tmp) s.previous = tmp return tmp } func (s *RMAStream) Truncate() { - if len(s.slice) > MaxNumOfRMA { - s.slice = s.slice[MaxNumOfRMATruncateSize-1:] + if len(s.Slice) > MaxNumOfRMA { + s.Slice = s.Slice[MaxNumOfRMATruncateSize-1:] } } diff --git a/pkg/indicator/v2_rsi.go b/pkg/indicator/v2/rsi.go similarity index 66% rename from pkg/indicator/v2_rsi.go rename to pkg/indicator/v2/rsi.go index 11cf984c93..d4ca1489d8 100644 --- a/pkg/indicator/v2_rsi.go +++ b/pkg/indicator/v2/rsi.go @@ -1,20 +1,24 @@ -package indicator +package indicatorv2 + +import ( + "github.com/c9s/bbgo/pkg/types" +) type RSIStream struct { // embedded structs - *Float64Series + *types.Float64Series // config fields window int // private states - source Float64Source + source types.Float64Source } -func RSI2(source Float64Source, window int) *RSIStream { +func RSI2(source types.Float64Source, window int) *RSIStream { s := &RSIStream{ source: source, - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), window: window, } s.Bind(source, s) @@ -42,3 +46,17 @@ func (s *RSIStream) Calculate(_ float64) float64 { rsi := 100.0 - (100.0 / (1.0 + rs)) return rsi } + +func max(x, y int) int { + if x > y { + return x + } + return y +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} diff --git a/pkg/indicator/v2_rsi_test.go b/pkg/indicator/v2/rsi_test.go similarity index 86% rename from pkg/indicator/v2_rsi_test.go rename to pkg/indicator/v2/rsi_test.go index 311624024a..779132d34c 100644 --- a/pkg/indicator/v2_rsi_test.go +++ b/pkg/indicator/v2/rsi_test.go @@ -1,4 +1,4 @@ -package indicator +package indicatorv2 import ( "encoding/json" @@ -75,11 +75,11 @@ func Test_RSI2(t *testing.T) { prices.PushAndEmit(price) } - assert.Equal(t, floats.Slice(tt.values), prices.slice) + assert.Equal(t, floats.Slice(tt.values), prices.Slice) - if assert.Equal(t, len(tt.want), len(rsi.slice)) { + if assert.Equal(t, len(tt.want), len(rsi.Slice)) { for i, v := range tt.want { - assert.InDelta(t, v, rsi.slice[i], 0.000001, "Expected rsi.slice[%d] to be %v, but got %v", i, v, rsi.slice[i]) + assert.InDelta(t, v, rsi.Slice[i], 0.000001, "Expected rsi.slice[%d] to be %v, but got %v", i, v, rsi.Slice[i]) } } }) diff --git a/pkg/indicator/v2_sma.go b/pkg/indicator/v2/sma.go similarity index 57% rename from pkg/indicator/v2_sma.go rename to pkg/indicator/v2/sma.go index b6a79277c6..dd054c58c8 100644 --- a/pkg/indicator/v2_sma.go +++ b/pkg/indicator/v2/sma.go @@ -1,16 +1,20 @@ -package indicator +package indicatorv2 -import "github.com/c9s/bbgo/pkg/types" +import ( + "github.com/c9s/bbgo/pkg/types" +) + +const MaxNumOfSMA = 5_000 type SMAStream struct { - *Float64Series + *types.Float64Series window int rawValues *types.Queue } -func SMA2(source Float64Source, window int) *SMAStream { +func SMA2(source types.Float64Source, window int) *SMAStream { s := &SMAStream{ - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), window: window, rawValues: types.NewQueue(window), } @@ -25,5 +29,5 @@ func (s *SMAStream) Calculate(v float64) float64 { } func (s *SMAStream) Truncate() { - s.slice = s.slice.Truncate(MaxNumOfSMA) + s.Slice = s.Slice.Truncate(MaxNumOfSMA) } diff --git a/pkg/indicator/v2_stddev.go b/pkg/indicator/v2/stddev.go similarity index 71% rename from pkg/indicator/v2_stddev.go rename to pkg/indicator/v2/stddev.go index 9e465f9704..a9e94974d0 100644 --- a/pkg/indicator/v2_stddev.go +++ b/pkg/indicator/v2/stddev.go @@ -1,9 +1,9 @@ -package indicator +package indicatorv2 import "github.com/c9s/bbgo/pkg/types" type StdDevStream struct { - *Float64Series + *types.Float64Series rawValues *types.Queue @@ -11,9 +11,9 @@ type StdDevStream struct { multiplier float64 } -func StdDev2(source Float64Source, window int) *StdDevStream { +func StdDev(source types.Float64Source, window int) *StdDevStream { s := &StdDevStream{ - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), rawValues: types.NewQueue(window), window: window, } diff --git a/pkg/indicator/v2_stoch.go b/pkg/indicator/v2/stoch.go similarity index 88% rename from pkg/indicator/v2_stoch.go rename to pkg/indicator/v2/stoch.go index 462e4b851b..808169e431 100644 --- a/pkg/indicator/v2_stoch.go +++ b/pkg/indicator/v2/stoch.go @@ -1,10 +1,12 @@ -package indicator +package indicatorv2 import ( "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) +const DPeriod int = 3 + // Stochastic Oscillator // - https://www.investopedia.com/terms/s/stochasticoscillator.asp // @@ -30,7 +32,7 @@ type StochStream struct { } // Stochastic Oscillator -func Stoch2(source KLineSubscription, window, dPeriod int) *StochStream { +func Stoch(source KLineSubscription, window, dPeriod int) *StochStream { highPrices := HighPrices(source) lowPrices := LowPrices(source) @@ -42,8 +44,8 @@ func Stoch2(source KLineSubscription, window, dPeriod int) *StochStream { } source.AddSubscriber(func(kLine types.KLine) { - lowest := s.lowPrices.slice.Tail(s.window).Min() - highest := s.highPrices.slice.Tail(s.window).Max() + lowest := s.lowPrices.Slice.Tail(s.window).Min() + highest := s.highPrices.Slice.Tail(s.window).Max() var k float64 = 50.0 var d float64 = 0.0 @@ -53,7 +55,7 @@ func Stoch2(source KLineSubscription, window, dPeriod int) *StochStream { } s.K.Push(k) - + d = s.K.Tail(s.dPeriod).Mean() s.D.Push(d) s.EmitUpdate(k, d) diff --git a/pkg/indicator/v2_stoch_test.go b/pkg/indicator/v2/stoch_test.go similarity index 99% rename from pkg/indicator/v2_stoch_test.go rename to pkg/indicator/v2/stoch_test.go index fa5f05b098..73a3becca2 100644 --- a/pkg/indicator/v2_stoch_test.go +++ b/pkg/indicator/v2/stoch_test.go @@ -1,4 +1,4 @@ -package indicator +package indicatorv2 import ( "encoding/json" @@ -58,7 +58,7 @@ func TestSTOCH2_update(t *testing.T) { t.Run(tt.name, func(t *testing.T) { stream := &types.StandardStream{} kLines := KLines(stream, "", "") - kd := Stoch2(kLines, tt.window, DPeriod) + kd := Stoch(kLines, tt.window, DPeriod) for _, k := range tt.kLines { stream.EmitKLineClosed(k) diff --git a/pkg/indicator/stochstream_callbacks.go b/pkg/indicator/v2/stochstream_callbacks.go similarity index 93% rename from pkg/indicator/stochstream_callbacks.go rename to pkg/indicator/v2/stochstream_callbacks.go index 539b13992a..b4bc8bb80c 100644 --- a/pkg/indicator/stochstream_callbacks.go +++ b/pkg/indicator/v2/stochstream_callbacks.go @@ -1,6 +1,6 @@ // Code generated by "callbackgen -type StochStream"; DO NOT EDIT. -package indicator +package indicatorv2 import () diff --git a/pkg/indicator/v2_subtract.go b/pkg/indicator/v2/subtract.go similarity index 71% rename from pkg/indicator/v2_subtract.go rename to pkg/indicator/v2/subtract.go index 33a1917304..622570e2d5 100644 --- a/pkg/indicator/v2_subtract.go +++ b/pkg/indicator/v2/subtract.go @@ -1,12 +1,13 @@ -package indicator +package indicatorv2 import ( "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" ) // SubtractStream subscribes 2 upstream data, and then subtract these 2 values type SubtractStream struct { - *Float64Series + *types.Float64Series a, b floats.Slice i int @@ -14,9 +15,9 @@ type SubtractStream struct { // Subtract creates the SubtractStream object // subtract := Subtract(longEWMA, shortEWMA) -func Subtract(a, b Float64Source) *SubtractStream { +func Subtract(a, b types.Float64Source) *SubtractStream { s := &SubtractStream{ - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), } a.OnUpdate(func(v float64) { @@ -35,13 +36,13 @@ func (s *SubtractStream) calculate() { return } - if s.a.Length() > s.slice.Length() { - var numNewElems = s.a.Length() - s.slice.Length() + if s.a.Length() > s.Slice.Length() { + var numNewElems = s.a.Length() - s.Slice.Length() var tailA = s.a.Tail(numNewElems) var tailB = s.b.Tail(numNewElems) var tailC = tailA.Sub(tailB) for _, f := range tailC { - s.slice.Push(f) + s.Slice.Push(f) s.EmitUpdate(f) } } diff --git a/pkg/indicator/v2_test.go b/pkg/indicator/v2/subtract_test.go similarity index 72% rename from pkg/indicator/v2_test.go rename to pkg/indicator/v2/subtract_test.go index 717713d11b..1ef3aace3a 100644 --- a/pkg/indicator/v2_test.go +++ b/pkg/indicator/v2/subtract_test.go @@ -1,4 +1,4 @@ -package indicator +package indicatorv2 import ( "testing" @@ -21,10 +21,10 @@ func Test_v2_Subtract(t *testing.T) { stream.EmitKLineClosed(types.KLine{Close: fixedpoint.NewFromFloat(19_000.0 + i)}) } - t.Logf("fastEMA: %+v", fastEMA.slice) - t.Logf("slowEMA: %+v", slowEMA.slice) + t.Logf("fastEMA: %+v", fastEMA.Slice) + t.Logf("slowEMA: %+v", slowEMA.Slice) assert.Equal(t, len(subtract.a), len(subtract.b)) - assert.Equal(t, len(subtract.a), len(subtract.slice)) - assert.InDelta(t, subtract.slice[0], subtract.a[0]-subtract.b[0], 0.0001) + assert.Equal(t, len(subtract.a), len(subtract.Slice)) + assert.InDelta(t, subtract.Slice[0], subtract.a[0]-subtract.b[0], 0.0001) } diff --git a/pkg/indicator/v2_tr.go b/pkg/indicator/v2/tr.go similarity index 89% rename from pkg/indicator/v2_tr.go rename to pkg/indicator/v2/tr.go index 98f4bdc7b0..e33044cf28 100644 --- a/pkg/indicator/v2_tr.go +++ b/pkg/indicator/v2/tr.go @@ -1,4 +1,4 @@ -package indicator +package indicatorv2 import ( "math" @@ -9,7 +9,7 @@ import ( // This TRStream calculates the ATR first type TRStream struct { // embedded struct - *Float64Series + *types.Float64Series // private states previousClose float64 @@ -17,7 +17,7 @@ type TRStream struct { func TR2(source KLineSubscription) *TRStream { s := &TRStream{ - Float64Series: NewFloat64Series(), + Float64Series: types.NewFloat64Series(), } source.AddSubscriber(func(k types.KLine) { diff --git a/pkg/indicator/v2_tr_test.go b/pkg/indicator/v2/tr_test.go similarity index 99% rename from pkg/indicator/v2_tr_test.go rename to pkg/indicator/v2/tr_test.go index 1067529c9a..82afd8269e 100644 --- a/pkg/indicator/v2_tr_test.go +++ b/pkg/indicator/v2/tr_test.go @@ -1,4 +1,4 @@ -package indicator +package indicatorv2 import ( "encoding/json" diff --git a/pkg/indicator/v2_cma.go b/pkg/indicator/v2_cma.go deleted file mode 100644 index 9bc2c89945..0000000000 --- a/pkg/indicator/v2_cma.go +++ /dev/null @@ -1,23 +0,0 @@ -package indicator - -type CMAStream struct { - *Float64Series -} - -func CMA2(source Float64Source) *CMAStream { - s := &CMAStream{ - Float64Series: NewFloat64Series(), - } - s.Bind(source, s) - return s -} - -func (s *CMAStream) Calculate(x float64) float64 { - l := float64(s.slice.Length()) - cma := (s.slice.Last(0)*l + x) / (l + 1.) - return cma -} - -func (s *CMAStream) Truncate() { - s.slice.Truncate(MaxNumOfEWMA) -} diff --git a/pkg/indicator/v2_pivothigh.go b/pkg/indicator/v2_pivothigh.go deleted file mode 100644 index 9e41438163..0000000000 --- a/pkg/indicator/v2_pivothigh.go +++ /dev/null @@ -1,27 +0,0 @@ -package indicator - -import ( - "github.com/c9s/bbgo/pkg/datatype/floats" -) - -type PivotHighStream struct { - *Float64Series - rawValues floats.Slice - window, rightWindow int -} - -func PivotHigh2(source Float64Source, window, rightWindow int) *PivotHighStream { - s := &PivotHighStream{ - Float64Series: NewFloat64Series(), - window: window, - rightWindow: rightWindow, - } - - s.Subscribe(source, func(x float64) { - s.rawValues.Push(x) - if low, ok := calculatePivotHigh(s.rawValues, s.window, s.rightWindow); ok { - s.PushAndEmit(low) - } - }) - return s -} diff --git a/pkg/indicator/v2_pivotlow.go b/pkg/indicator/v2_pivotlow.go deleted file mode 100644 index 1fa78e054c..0000000000 --- a/pkg/indicator/v2_pivotlow.go +++ /dev/null @@ -1,27 +0,0 @@ -package indicator - -import ( - "github.com/c9s/bbgo/pkg/datatype/floats" -) - -type PivotLowStream struct { - *Float64Series - rawValues floats.Slice - window, rightWindow int -} - -func PivotLow2(source Float64Source, window, rightWindow int) *PivotLowStream { - s := &PivotLowStream{ - Float64Series: NewFloat64Series(), - window: window, - rightWindow: rightWindow, - } - - s.Subscribe(source, func(x float64) { - s.rawValues.Push(x) - if low, ok := calculatePivotLow(s.rawValues, s.window, s.rightWindow); ok { - s.PushAndEmit(low) - } - }) - return s -} diff --git a/pkg/indicator/volatility.go b/pkg/indicator/volatility.go index 3c5efce962..aeb9979bea 100644 --- a/pkg/indicator/volatility.go +++ b/pkg/indicator/volatility.go @@ -58,7 +58,7 @@ func (inc *Volatility) CalculateAndUpdate(allKLines []types.KLine) { var recentT = allKLines[end-(inc.Window-1) : end+1] - volatility, err := calculateVOLATILITY(recentT, inc.Window, KLineClosePriceMapper) + volatility, err := calculateVOLATILITY(recentT, inc.Window, types.KLineClosePriceMapper) if err != nil { log.WithError(err).Error("can not calculate volatility") return @@ -86,7 +86,7 @@ func (inc *Volatility) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } -func calculateVOLATILITY(klines []types.KLine, window int, priceF KLineValueMapper) (float64, error) { +func calculateVOLATILITY(klines []types.KLine, window int, priceF types.KLineValueMapper) (float64, error) { length := len(klines) if length == 0 || length < window { return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) diff --git a/pkg/indicator/vwap.go b/pkg/indicator/vwap.go index 3f2e08085c..2dbe5228ca 100644 --- a/pkg/indicator/vwap.go +++ b/pkg/indicator/vwap.go @@ -71,7 +71,7 @@ func (inc *VWAP) Length() int { var _ types.SeriesExtend = &VWAP{} func (inc *VWAP) PushK(k types.KLine) { - inc.Update(KLineTypicalPriceMapper(k), k.Volume.Float64()) + inc.Update(types.KLineTypicalPriceMapper(k), k.Volume.Float64()) } func (inc *VWAP) CalculateAndUpdate(allKLines []types.KLine) { @@ -99,7 +99,7 @@ func (inc *VWAP) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } -func calculateVWAP(klines []types.KLine, priceF KLineValueMapper, window int) float64 { +func calculateVWAP(klines []types.KLine, priceF types.KLineValueMapper, window int) float64 { vwap := VWAP{IntervalWindow: types.IntervalWindow{Window: window}} for _, k := range klines { vwap.Update(priceF(k), k.Volume.Float64()) diff --git a/pkg/indicator/vwap_test.go b/pkg/indicator/vwap_test.go index 7929b4bbae..3aab3db344 100644 --- a/pkg/indicator/vwap_test.go +++ b/pkg/indicator/vwap_test.go @@ -63,7 +63,7 @@ func Test_calculateVWAP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - priceF := KLineTypicalPriceMapper + priceF := types.KLineTypicalPriceMapper got := calculateVWAP(tt.kLines, priceF, tt.window) diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { diff --git a/pkg/risk/riskcontrol/circuit_break.go b/pkg/risk/riskcontrol/circuit_break.go index a89833ed8a..753d8d2367 100644 --- a/pkg/risk/riskcontrol/circuit_break.go +++ b/pkg/risk/riskcontrol/circuit_break.go @@ -4,14 +4,14 @@ import ( log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/types" ) type CircuitBreakRiskControl struct { // Since price could be fluctuated large, // use an EWMA to smooth it in running time - price *indicator.EWMAStream + price *indicatorv2.EWMAStream position *types.Position profitStats *types.ProfitStats lossThreshold fixedpoint.Value @@ -19,7 +19,7 @@ type CircuitBreakRiskControl struct { func NewCircuitBreakRiskControl( position *types.Position, - price *indicator.EWMAStream, + price *indicatorv2.EWMAStream, lossThreshold fixedpoint.Value, profitStats *types.ProfitStats) *CircuitBreakRiskControl { diff --git a/pkg/risk/riskcontrol/circuit_break_test.go b/pkg/risk/riskcontrol/circuit_break_test.go index 1ee01c59b0..0e5d00ddad 100644 --- a/pkg/risk/riskcontrol/circuit_break_test.go +++ b/pkg/risk/riskcontrol/circuit_break_test.go @@ -6,12 +6,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/indicator" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/types" ) func Test_IsHalted(t *testing.T) { - var ( price = 30000.00 realizedPnL = fixedpoint.NewFromFloat(-100.0) @@ -19,7 +18,7 @@ func Test_IsHalted(t *testing.T) { ) window := types.IntervalWindow{Window: 30, Interval: types.Interval1m} - priceEWMA := indicator.EWMA2(nil, window.Window) + priceEWMA := indicatorv2.EWMA2(nil, window.Window) priceEWMA.PushAndEmit(price) cases := []struct { diff --git a/pkg/strategy/bollmaker/dynamic_spread.go b/pkg/strategy/bollmaker/dynamic_spread.go index b6ede21569..f236c34eaa 100644 --- a/pkg/strategy/bollmaker/dynamic_spread.go +++ b/pkg/strategy/bollmaker/dynamic_spread.go @@ -7,6 +7,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/indicator" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/types" ) @@ -28,7 +29,7 @@ type DynamicSpreadSettings struct { } // Initialize dynamic spreads and preload SMAs -func (ds *DynamicSpreadSettings) Initialize(symbol string, session *bbgo.ExchangeSession, neutralBoll, defaultBoll *indicator.BOLLStream) { +func (ds *DynamicSpreadSettings) Initialize(symbol string, session *bbgo.ExchangeSession, neutralBoll, defaultBoll *indicatorv2.BOLLStream) { switch { case ds.AmpSpreadSettings != nil: ds.AmpSpreadSettings.initialize(symbol, session) @@ -164,10 +165,10 @@ type DynamicSpreadBollWidthRatioSettings struct { // A positive number. The greater factor, the sharper weighting function. Default set to 1.0 . Sensitivity float64 `json:"sensitivity"` - defaultBoll, neutralBoll *indicator.BOLLStream + defaultBoll, neutralBoll *indicatorv2.BOLLStream } -func (ds *DynamicSpreadBollWidthRatioSettings) initialize(neutralBoll, defaultBoll *indicator.BOLLStream) { +func (ds *DynamicSpreadBollWidthRatioSettings) initialize(neutralBoll, defaultBoll *indicatorv2.BOLLStream) { ds.neutralBoll = neutralBoll ds.defaultBoll = defaultBoll if ds.Sensitivity <= 0. { diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index a2233b44d4..d8f32b8c25 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -6,7 +6,7 @@ import ( "math" "sync" - "github.com/c9s/bbgo/pkg/indicator" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/util" "github.com/pkg/errors" @@ -158,10 +158,10 @@ type Strategy struct { groupID uint32 // defaultBoll is the BOLLINGER indicator we used for predicting the price. - defaultBoll *indicator.BOLLStream + defaultBoll *indicatorv2.BOLLStream // neutralBoll is the neutral price section - neutralBoll *indicator.BOLLStream + neutralBoll *indicatorv2.BOLLStream // StrategyController bbgo.StrategyController diff --git a/pkg/strategy/bollmaker/trend.go b/pkg/strategy/bollmaker/trend.go index 7e4ddcf714..0ff0cc9d39 100644 --- a/pkg/strategy/bollmaker/trend.go +++ b/pkg/strategy/bollmaker/trend.go @@ -1,6 +1,8 @@ package bollmaker -import "github.com/c9s/bbgo/pkg/indicator" +import ( + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" +) type PriceTrend string @@ -11,7 +13,7 @@ const ( UnknownTrend PriceTrend = "unknown" ) -func detectPriceTrend(inc *indicator.BOLLStream, price float64) PriceTrend { +func detectPriceTrend(inc *indicatorv2.BOLLStream, price float64) PriceTrend { if inBetween(price, inc.DownBand.Last(0), inc.UpBand.Last(0)) { return NeutralTrend } diff --git a/pkg/strategy/factorzoo/factors/price_mean_reversion.go b/pkg/strategy/factorzoo/factors/price_mean_reversion.go index fbc4c5ad4f..ed3e5bd04d 100644 --- a/pkg/strategy/factorzoo/factors/price_mean_reversion.go +++ b/pkg/strategy/factorzoo/factors/price_mean_reversion.go @@ -82,13 +82,13 @@ func (inc *PMR) PushK(k types.KLine) { return } - inc.Update(indicator.KLineClosePriceMapper(k)) + inc.Update(types.KLineClosePriceMapper(k)) inc.EndTime = k.EndTime.Time() inc.EmitUpdate(inc.Last(0)) } func CalculateKLinesPMR(allKLines []types.KLine, window int) float64 { - return pmr(indicator.MapKLinePrice(allKLines, indicator.KLineClosePriceMapper), window) + return pmr(types.MapKLinePrice(allKLines, types.KLineClosePriceMapper), window) } func pmr(prices []float64, window int) float64 { diff --git a/pkg/strategy/factorzoo/factors/price_volume_divergence.go b/pkg/strategy/factorzoo/factors/price_volume_divergence.go index f531eb214d..16697bdbf0 100644 --- a/pkg/strategy/factorzoo/factors/price_volume_divergence.go +++ b/pkg/strategy/factorzoo/factors/price_volume_divergence.go @@ -89,13 +89,13 @@ func (inc *PVD) PushK(k types.KLine) { return } - inc.Update(indicator.KLineClosePriceMapper(k), indicator.KLineVolumeMapper(k)) + inc.Update(types.KLineClosePriceMapper(k), types.KLineVolumeMapper(k)) inc.EndTime = k.EndTime.Time() inc.EmitUpdate(inc.Last(0)) } func CalculateKLinesPVD(allKLines []types.KLine, window int) float64 { - return pvd(indicator.MapKLinePrice(allKLines, indicator.KLineClosePriceMapper), indicator.MapKLinePrice(allKLines, indicator.KLineVolumeMapper), window) + return pvd(types.MapKLinePrice(allKLines, types.KLineClosePriceMapper), types.MapKLinePrice(allKLines, types.KLineVolumeMapper), window) } func pvd(prices []float64, volumes []float64, window int) float64 { diff --git a/pkg/strategy/factorzoo/factors/return_rate.go b/pkg/strategy/factorzoo/factors/return_rate.go index 114e4b9d90..08baf98831 100644 --- a/pkg/strategy/factorzoo/factors/return_rate.go +++ b/pkg/strategy/factorzoo/factors/return_rate.go @@ -81,7 +81,7 @@ func (inc *RR) PushK(k types.KLine) { return } - inc.Update(indicator.KLineClosePriceMapper(k)) + inc.Update(types.KLineClosePriceMapper(k)) inc.EndTime = k.EndTime.Time() inc.EmitUpdate(inc.Last(0)) } diff --git a/pkg/strategy/fmaker/A18.go b/pkg/strategy/fmaker/A18.go index 35dc7368e4..17628e5296 100644 --- a/pkg/strategy/fmaker/A18.go +++ b/pkg/strategy/fmaker/A18.go @@ -42,7 +42,7 @@ func (inc *A18) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateA18(recentT, indicator.KLineClosePriceMapper) + val, err := calculateA18(recentT, types.KLineClosePriceMapper) if err != nil { log.WithError(err).Error("can not calculate") return diff --git a/pkg/strategy/fmaker/A2.go b/pkg/strategy/fmaker/A2.go index cf674e1a6b..b49c3e3b35 100644 --- a/pkg/strategy/fmaker/A2.go +++ b/pkg/strategy/fmaker/A2.go @@ -42,7 +42,7 @@ func (inc *A2) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateA2(recentT, KLineLowPriceMapper, KLineHighPriceMapper, indicator.KLineClosePriceMapper) + val, err := calculateA2(recentT, KLineLowPriceMapper, KLineHighPriceMapper, types.KLineClosePriceMapper) if err != nil { log.WithError(err).Error("can not calculate") return diff --git a/pkg/strategy/fmaker/A3.go b/pkg/strategy/fmaker/A3.go index 02118def87..479913c31a 100644 --- a/pkg/strategy/fmaker/A3.go +++ b/pkg/strategy/fmaker/A3.go @@ -43,7 +43,7 @@ func (inc *A3) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateA3(recentT, KLineLowPriceMapper, KLineHighPriceMapper, indicator.KLineClosePriceMapper) + val, err := calculateA3(recentT, KLineLowPriceMapper, KLineHighPriceMapper, types.KLineClosePriceMapper) if err != nil { log.WithError(err).Error("can not calculate pivots") return diff --git a/pkg/strategy/fmaker/A34.go b/pkg/strategy/fmaker/A34.go index ba60aeedd1..2714f426cd 100644 --- a/pkg/strategy/fmaker/A34.go +++ b/pkg/strategy/fmaker/A34.go @@ -42,7 +42,7 @@ func (inc *A34) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateA34(recentT, indicator.KLineClosePriceMapper) + val, err := calculateA34(recentT, types.KLineClosePriceMapper) if err != nil { log.WithError(err).Error("can not calculate pivots") return diff --git a/pkg/strategy/fmaker/R.go b/pkg/strategy/fmaker/R.go index dc4f0eabfe..7bd64f608f 100644 --- a/pkg/strategy/fmaker/R.go +++ b/pkg/strategy/fmaker/R.go @@ -46,7 +46,7 @@ func (inc *R) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateR(recentT, indicator.KLineOpenPriceMapper, indicator.KLineClosePriceMapper) + val, err := calculateR(recentT, types.KLineOpenPriceMapper, types.KLineClosePriceMapper) if err != nil { log.WithError(err).Error("can not calculate pivots") return diff --git a/pkg/strategy/fmaker/S0.go b/pkg/strategy/fmaker/S0.go index 7d2eda1589..fab32816af 100644 --- a/pkg/strategy/fmaker/S0.go +++ b/pkg/strategy/fmaker/S0.go @@ -42,7 +42,7 @@ func (inc *S0) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateS0(recentT, indicator.KLineClosePriceMapper) + val, err := calculateS0(recentT, types.KLineClosePriceMapper) if err != nil { log.WithError(err).Error("can not calculate") return diff --git a/pkg/strategy/fmaker/S1.go b/pkg/strategy/fmaker/S1.go index 68962a579e..2eaa27f3c1 100644 --- a/pkg/strategy/fmaker/S1.go +++ b/pkg/strategy/fmaker/S1.go @@ -40,7 +40,7 @@ func (inc *S1) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - correlation, err := calculateS1(recentT, inc.Window, KLineAmplitudeMapper, indicator.KLineVolumeMapper) + correlation, err := calculateS1(recentT, inc.Window, KLineAmplitudeMapper, types.KLineVolumeMapper) if err != nil { log.WithError(err).Error("can not calculate correlation") return diff --git a/pkg/strategy/fmaker/S2.go b/pkg/strategy/fmaker/S2.go index d63488a985..ea80d61abc 100644 --- a/pkg/strategy/fmaker/S2.go +++ b/pkg/strategy/fmaker/S2.go @@ -40,7 +40,7 @@ func (inc *S2) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - correlation, err := calculateS2(recentT, inc.Window, indicator.KLineOpenPriceMapper, indicator.KLineVolumeMapper) + correlation, err := calculateS2(recentT, inc.Window, types.KLineOpenPriceMapper, types.KLineVolumeMapper) if err != nil { log.WithError(err).Error("can not calculate correlation") return diff --git a/pkg/strategy/fmaker/S3.go b/pkg/strategy/fmaker/S3.go index e00f83625e..f3bd88082b 100644 --- a/pkg/strategy/fmaker/S3.go +++ b/pkg/strategy/fmaker/S3.go @@ -42,7 +42,7 @@ func (inc *S3) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateS3(recentT, indicator.KLineClosePriceMapper, indicator.KLineOpenPriceMapper) + val, err := calculateS3(recentT, types.KLineClosePriceMapper, types.KLineOpenPriceMapper) if err != nil { log.WithError(err).Error("can not calculate") return diff --git a/pkg/strategy/fmaker/S4.go b/pkg/strategy/fmaker/S4.go index b7f8cfd8ce..97e97595a9 100644 --- a/pkg/strategy/fmaker/S4.go +++ b/pkg/strategy/fmaker/S4.go @@ -42,7 +42,7 @@ func (inc *S4) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateS4(recentT, indicator.KLineClosePriceMapper) + val, err := calculateS4(recentT, types.KLineClosePriceMapper) if err != nil { log.WithError(err).Error("can not calculate") return diff --git a/pkg/strategy/fmaker/S5.go b/pkg/strategy/fmaker/S5.go index 8fd2edfb75..77e7070e0d 100644 --- a/pkg/strategy/fmaker/S5.go +++ b/pkg/strategy/fmaker/S5.go @@ -42,7 +42,7 @@ func (inc *S5) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateS5(recentT, indicator.KLineVolumeMapper) + val, err := calculateS5(recentT, types.KLineVolumeMapper) if err != nil { log.WithError(err).Error("can not calculate pivots") return diff --git a/pkg/strategy/fmaker/S6.go b/pkg/strategy/fmaker/S6.go index 8c7f73c16a..5e6bac7892 100644 --- a/pkg/strategy/fmaker/S6.go +++ b/pkg/strategy/fmaker/S6.go @@ -42,7 +42,7 @@ func (inc *S6) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateS6(recentT, indicator.KLineHighPriceMapper, indicator.KLineLowPriceMapper, indicator.KLineClosePriceMapper, indicator.KLineVolumeMapper) + val, err := calculateS6(recentT, types.KLineHighPriceMapper, types.KLineLowPriceMapper, types.KLineClosePriceMapper, types.KLineVolumeMapper) if err != nil { log.WithError(err).Error("can not calculate") return diff --git a/pkg/strategy/fmaker/S7.go b/pkg/strategy/fmaker/S7.go index 048078fe27..e8e060d7a5 100644 --- a/pkg/strategy/fmaker/S7.go +++ b/pkg/strategy/fmaker/S7.go @@ -42,7 +42,7 @@ func (inc *S7) CalculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - val, err := calculateS7(recentT, indicator.KLineOpenPriceMapper, indicator.KLineClosePriceMapper) + val, err := calculateS7(recentT, types.KLineOpenPriceMapper, types.KLineClosePriceMapper) if err != nil { log.WithError(err).Error("can not calculate") return diff --git a/pkg/strategy/harmonic/shark.go b/pkg/strategy/harmonic/shark.go index a23d7e62e7..5071f35cb1 100644 --- a/pkg/strategy/harmonic/shark.go +++ b/pkg/strategy/harmonic/shark.go @@ -72,7 +72,7 @@ func (inc *SHARK) PushK(k types.KLine) { return } - inc.Update(indicator.KLineHighPriceMapper(k), indicator.KLineLowPriceMapper(k), indicator.KLineClosePriceMapper(k)) + inc.Update(types.KLineHighPriceMapper(k), types.KLineLowPriceMapper(k), types.KLineClosePriceMapper(k)) inc.EndTime = k.EndTime.Time() inc.EmitUpdate(inc.Last(0)) } diff --git a/pkg/strategy/irr/neg_return_rate.go b/pkg/strategy/irr/neg_return_rate.go index f16adecbef..af2b48b572 100644 --- a/pkg/strategy/irr/neg_return_rate.go +++ b/pkg/strategy/irr/neg_return_rate.go @@ -74,7 +74,7 @@ func (inc *NRR) PushK(k types.KLine) { return } - inc.Update(indicator.KLineOpenPriceMapper(k), indicator.KLineClosePriceMapper(k)) + inc.Update(types.KLineOpenPriceMapper(k), types.KLineClosePriceMapper(k)) inc.EndTime = k.EndTime.Time() inc.EmitUpdate(inc.Last(0)) } diff --git a/pkg/strategy/rsicross/strategy.go b/pkg/strategy/rsicross/strategy.go index 5710e418e7..dafab6ae7f 100644 --- a/pkg/strategy/rsicross/strategy.go +++ b/pkg/strategy/rsicross/strategy.go @@ -9,7 +9,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/strategy/common" "github.com/c9s/bbgo/pkg/types" ) @@ -52,10 +52,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se fastRsi := session.Indicators(s.Symbol).RSI(types.IntervalWindow{Interval: s.Interval, Window: s.FastWindow}) slowRsi := session.Indicators(s.Symbol).RSI(types.IntervalWindow{Interval: s.Interval, Window: s.SlowWindow}) - rsiCross := indicator.Cross(fastRsi, slowRsi) + rsiCross := indicatorv2.Cross(fastRsi, slowRsi) rsiCross.OnUpdate(func(v float64) { - switch indicator.CrossType(v) { - case indicator.CrossOver: + switch indicatorv2.CrossType(v) { + case indicatorv2.CrossOver: opts := s.OpenPositionOptions opts.Long = true @@ -69,7 +69,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se logErr(err, "unable to open position") } - case indicator.CrossUnder: + case indicatorv2.CrossUnder: if err := s.OrderExecutor.ClosePosition(ctx, fixedpoint.One); err != nil { logErr(err, "failed to close position") } diff --git a/pkg/strategy/scmaker/intensity.go b/pkg/strategy/scmaker/intensity.go index b4f3cb3836..0472241230 100644 --- a/pkg/strategy/scmaker/intensity.go +++ b/pkg/strategy/scmaker/intensity.go @@ -2,24 +2,24 @@ package scmaker import ( "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/indicator" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/types" ) type IntensityStream struct { - *indicator.Float64Series + *types.Float64Series - Buy, Sell *indicator.RMAStream + Buy, Sell *indicatorv2.RMAStream window int } -func Intensity(source indicator.KLineSubscription, window int) *IntensityStream { +func Intensity(source indicatorv2.KLineSubscription, window int) *IntensityStream { s := &IntensityStream{ - Float64Series: indicator.NewFloat64Series(), + Float64Series: types.NewFloat64Series(), window: window, - Buy: indicator.RMA2(indicator.NewFloat64Series(), window, false), - Sell: indicator.RMA2(indicator.NewFloat64Series(), window, false), + Buy: indicatorv2.RMA2(types.NewFloat64Series(), window, false), + Sell: indicatorv2.RMA2(types.NewFloat64Series(), window, false), } threshold := fixedpoint.NewFromFloat(100.0) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 6dc1dc6592..7356ed84f2 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -10,7 +10,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/indicator" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/risk/riskcontrol" "github.com/c9s/bbgo/pkg/strategy/common" "github.com/c9s/bbgo/pkg/types" @@ -73,8 +73,8 @@ type Strategy struct { liquidityScale bbgo.Scale // indicators - ewma *indicator.EWMAStream - boll *indicator.BOLLStream + ewma *indicatorv2.EWMAStream + boll *indicatorv2.BOLLStream intensity *IntensityStream positionRiskControl *riskcontrol.PositionRiskControl @@ -172,7 +172,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -func (s *Strategy) preloadKLines(inc *indicator.KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) { +func (s *Strategy) preloadKLines(inc *indicatorv2.KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) { if store, ok := session.MarketDataStore(symbol); ok { if kLinesData, ok := store.KLinesOfInterval(interval); ok { for _, k := range *kLinesData { @@ -183,23 +183,23 @@ func (s *Strategy) preloadKLines(inc *indicator.KLineStream, session *bbgo.Excha } func (s *Strategy) initializeMidPriceEMA(session *bbgo.ExchangeSession) { - kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.MidPriceEMA.Interval) - s.ewma = indicator.EWMA2(indicator.ClosePrices(kLines), s.MidPriceEMA.Window) + kLines := indicatorv2.KLines(session.MarketDataStream, s.Symbol, s.MidPriceEMA.Interval) + s.ewma = indicatorv2.EWMA2(indicatorv2.ClosePrices(kLines), s.MidPriceEMA.Window) s.preloadKLines(kLines, session, s.Symbol, s.MidPriceEMA.Interval) } func (s *Strategy) initializeIntensityIndicator(session *bbgo.ExchangeSession) { - kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.StrengthInterval) + kLines := indicatorv2.KLines(session.MarketDataStream, s.Symbol, s.StrengthInterval) s.intensity = Intensity(kLines, 10) s.preloadKLines(kLines, session, s.Symbol, s.StrengthInterval) } func (s *Strategy) initializePriceRangeBollinger(session *bbgo.ExchangeSession) { - kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.PriceRangeBollinger.Interval) - closePrices := indicator.ClosePrices(kLines) - s.boll = indicator.BOLL2(closePrices, s.PriceRangeBollinger.Window, s.PriceRangeBollinger.K) + kLines := indicatorv2.KLines(session.MarketDataStream, s.Symbol, s.PriceRangeBollinger.Interval) + closePrices := indicatorv2.ClosePrices(kLines) + s.boll = indicatorv2.BOLL(closePrices, s.PriceRangeBollinger.Window, s.PriceRangeBollinger.K) s.preloadKLines(kLines, session, s.Symbol, s.PriceRangeBollinger.Interval) } diff --git a/pkg/indicator/float64updater.go b/pkg/types/float64updater.go similarity index 86% rename from pkg/indicator/float64updater.go rename to pkg/types/float64updater.go index a9743538eb..ba3b5e03f6 100644 --- a/pkg/indicator/float64updater.go +++ b/pkg/types/float64updater.go @@ -1,4 +1,4 @@ -package indicator +package types //go:generate callbackgen -type Float64Updater type Float64Updater struct { diff --git a/pkg/types/float64updater_callbacks.go b/pkg/types/float64updater_callbacks.go new file mode 100644 index 0000000000..28b2b6f783 --- /dev/null +++ b/pkg/types/float64updater_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type Float64Updater"; DO NOT EDIT. + +package types + +import () + +func (F *Float64Updater) OnUpdate(cb func(v float64)) { + F.updateCallbacks = append(F.updateCallbacks, cb) +} + +func (F *Float64Updater) EmitUpdate(v float64) { + for _, cb := range F.updateCallbacks { + cb(v) + } +} diff --git a/pkg/types/kline.go b/pkg/types/kline.go index a17e1e94ea..e405ba94bd 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -645,3 +645,41 @@ func KLineWith(symbol string, interval Interval, callback KLineCallback) KLineCa callback(k) } } + +type KLineValueMapper func(k KLine) float64 + +func KLineOpenPriceMapper(k KLine) float64 { + return k.Open.Float64() +} + +func KLineClosePriceMapper(k KLine) float64 { + return k.Close.Float64() +} + +func KLineTypicalPriceMapper(k KLine) float64 { + return (k.High.Float64() + k.Low.Float64() + k.Close.Float64()) / 3. +} + +func KLinePriceVolumeMapper(k KLine) float64 { + return k.Close.Mul(k.Volume).Float64() +} + +func KLineVolumeMapper(k KLine) float64 { + return k.Volume.Float64() +} + +func MapKLinePrice(kLines []KLine, f KLineValueMapper) (prices []float64) { + for _, k := range kLines { + prices = append(prices, f(k)) + } + + return prices +} + +func KLineLowPriceMapper(k KLine) float64 { + return k.Low.Float64() +} + +func KLineHighPriceMapper(k KLine) float64 { + return k.High.Float64() +} diff --git a/pkg/indicator/float64series.go b/pkg/types/series_float64.go similarity index 73% rename from pkg/indicator/float64series.go rename to pkg/types/series_float64.go index 8035acb964..6dd6704aa8 100644 --- a/pkg/indicator/float64series.go +++ b/pkg/types/series_float64.go @@ -1,25 +1,24 @@ -package indicator +package types import ( "github.com/c9s/bbgo/pkg/datatype/floats" - "github.com/c9s/bbgo/pkg/types" ) type Float64Series struct { - types.SeriesBase + SeriesBase Float64Updater - slice floats.Slice + Slice floats.Slice } func NewFloat64Series(v ...float64) *Float64Series { s := &Float64Series{} - s.slice = v - s.SeriesBase.Series = s.slice + s.Slice = v + s.SeriesBase.Series = s.Slice return s } func (f *Float64Series) Last(i int) float64 { - return f.slice.Last(i) + return f.Slice.Last(i) } func (f *Float64Series) Index(i int) float64 { @@ -27,15 +26,11 @@ func (f *Float64Series) Index(i int) float64 { } func (f *Float64Series) Length() int { - return len(f.slice) -} - -func (f *Float64Series) Slice() floats.Slice { - return f.slice + return len(f.Slice) } func (f *Float64Series) PushAndEmit(x float64) { - f.slice.Push(x) + f.Slice.Push(x) f.EmitUpdate(x) } @@ -71,3 +66,22 @@ func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) { f.Subscribe(source, c) } } + +type Float64Calculator interface { + Calculate(x float64) float64 + PushAndEmit(x float64) +} + +type Float64Source interface { + Series + OnUpdate(f func(v float64)) +} + +type Float64Subscription interface { + Series + AddSubscriber(f func(v float64)) +} + +type Float64Truncator interface { + Truncate() +} From 630b0d476d4f902013dfb6413b25b601e3d4bc10 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 10 Jul 2023 17:08:14 +0800 Subject: [PATCH 1168/1392] scmaker: use dot import to use v2 indicator DSL --- pkg/strategy/scmaker/strategy.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 7356ed84f2..3abfad0816 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -10,7 +10,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" - indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" + . "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/risk/riskcontrol" "github.com/c9s/bbgo/pkg/strategy/common" "github.com/c9s/bbgo/pkg/types" @@ -73,8 +73,8 @@ type Strategy struct { liquidityScale bbgo.Scale // indicators - ewma *indicatorv2.EWMAStream - boll *indicatorv2.BOLLStream + ewma *EWMAStream + boll *BOLLStream intensity *IntensityStream positionRiskControl *riskcontrol.PositionRiskControl @@ -172,7 +172,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -func (s *Strategy) preloadKLines(inc *indicatorv2.KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) { +func (s *Strategy) preloadKLines(inc *KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) { if store, ok := session.MarketDataStore(symbol); ok { if kLinesData, ok := store.KLinesOfInterval(interval); ok { for _, k := range *kLinesData { @@ -183,23 +183,23 @@ func (s *Strategy) preloadKLines(inc *indicatorv2.KLineStream, session *bbgo.Exc } func (s *Strategy) initializeMidPriceEMA(session *bbgo.ExchangeSession) { - kLines := indicatorv2.KLines(session.MarketDataStream, s.Symbol, s.MidPriceEMA.Interval) - s.ewma = indicatorv2.EWMA2(indicatorv2.ClosePrices(kLines), s.MidPriceEMA.Window) + kLines := KLines(session.MarketDataStream, s.Symbol, s.MidPriceEMA.Interval) + s.ewma = EWMA2(ClosePrices(kLines), s.MidPriceEMA.Window) s.preloadKLines(kLines, session, s.Symbol, s.MidPriceEMA.Interval) } func (s *Strategy) initializeIntensityIndicator(session *bbgo.ExchangeSession) { - kLines := indicatorv2.KLines(session.MarketDataStream, s.Symbol, s.StrengthInterval) + kLines := KLines(session.MarketDataStream, s.Symbol, s.StrengthInterval) s.intensity = Intensity(kLines, 10) s.preloadKLines(kLines, session, s.Symbol, s.StrengthInterval) } func (s *Strategy) initializePriceRangeBollinger(session *bbgo.ExchangeSession) { - kLines := indicatorv2.KLines(session.MarketDataStream, s.Symbol, s.PriceRangeBollinger.Interval) - closePrices := indicatorv2.ClosePrices(kLines) - s.boll = indicatorv2.BOLL(closePrices, s.PriceRangeBollinger.Window, s.PriceRangeBollinger.K) + kLines := KLines(session.MarketDataStream, s.Symbol, s.PriceRangeBollinger.Interval) + closePrices := ClosePrices(kLines) + s.boll = BOLL(closePrices, s.PriceRangeBollinger.Window, s.PriceRangeBollinger.K) s.preloadKLines(kLines, session, s.Symbol, s.PriceRangeBollinger.Interval) } From 4648b5434e22bd6c9b0a5f3fc8d9b8505ba0fc2d Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Jul 2023 10:13:01 +0800 Subject: [PATCH 1169/1392] add binance markets json for test data --- testdata/binance-markets.json | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 testdata/binance-markets.json diff --git a/testdata/binance-markets.json b/testdata/binance-markets.json new file mode 100644 index 0000000000..90b58b53aa --- /dev/null +++ b/testdata/binance-markets.json @@ -0,0 +1,66 @@ +{ + "ETHUSDT": { + "symbol": "ETHUSDT", + "localSymbol": "ETHUSDT", + "pricePrecision": 8, + "volumePrecision": 8, + "quoteCurrency": "USDT", + "baseCurrency": "ETH", + "minNotional": 10, + "minAmount": 10, + "minQuantity": 0.0001, + "maxQuantity": 9000, + "stepSize": 0.0001, + "minPrice": 0.01, + "maxPrice": 1000000, + "tickSize": 0.01 + }, + "BTCUSDT": { + "symbol": "BTCUSDT", + "localSymbol": "BTCUSDT", + "pricePrecision": 8, + "volumePrecision": 8, + "quoteCurrency": "USDT", + "baseCurrency": "BTC", + "minNotional": 10, + "minAmount": 10, + "minQuantity": 1e-05, + "maxQuantity": 9000, + "stepSize": 1e-05, + "minPrice": 0.01, + "maxPrice": 1000000, + "tickSize": 0.01 + }, + "LINKUSDT": { + "symbol": "LINKUSDT", + "localSymbol": "LINKUSDT", + "pricePrecision": 8, + "volumePrecision": 8, + "quoteCurrency": "USDT", + "baseCurrency": "LINK", + "minNotional": 5, + "minAmount": 5, + "minQuantity": 0.01, + "maxQuantity": 90000, + "stepSize": 0.01, + "minPrice": 0.001, + "maxPrice": 10000, + "tickSize": 0.001 + }, + "LTCUSDT": { + "symbol": "LTCUSDT", + "localSymbol": "LTCUSDT", + "pricePrecision": 8, + "volumePrecision": 8, + "quoteCurrency": "USDT", + "baseCurrency": "LTC", + "minNotional": 5, + "minAmount": 5, + "minQuantity": 0.001, + "maxQuantity": 90000, + "stepSize": 0.001, + "minPrice": 0.01, + "maxPrice": 100000, + "tickSize": 0.01 + } +} From feb9f3a2d551bd8804efe9d07a3ba38699227efd Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 11 Jul 2023 10:21:38 +0800 Subject: [PATCH 1170/1392] improve/profitStatsTracker: use strconv instead of Sprintf() --- pkg/report/profit_report.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index ff330a131c..011b371e6b 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -7,6 +7,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" + "strconv" ) // AccumulatedProfitReport For accumulated profit report output @@ -135,15 +136,15 @@ func (r *AccumulatedProfitReport) Output() { // Output data row for i := 0; i <= r.Window-1; i++ { values := []string{ - fmt.Sprintf("%d", i+1), + strconv.Itoa(i + 1), r.symbol, - fmt.Sprintf("%f", r.accumulatedProfitPerInterval.Last(i)), - fmt.Sprintf("%f", r.profitMAPerInterval.Last(i)), - fmt.Sprintf("%f", r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow)), - fmt.Sprintf("%f", r.accumulatedFeePerInterval.Last(i)), - fmt.Sprintf("%f", r.winRatioPerInterval.Last(i)), - fmt.Sprintf("%f", r.profitFactorPerInterval.Last(i)), - fmt.Sprintf("%f", r.accumulatedTradesPerInterval.Last(i)), + strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.profitMAPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedFeePerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.winRatioPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.profitFactorPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedTradesPerInterval.Last(i), 'f', 4, 64), } for j := 0; j < len(r.strategyParameters); j++ { values = append(values, r.strategyParameters[j][1]) From 561c332541f44456e9d9acda5ca1318c9e9941c9 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 11 Jul 2023 10:24:07 +0800 Subject: [PATCH 1171/1392] improve/profitStatsTracker: rename InitOld() to InitLegacy() --- pkg/report/profit_stats_tracker.go | 6 +++--- pkg/strategy/supertrend/strategy.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/report/profit_stats_tracker.go b/pkg/report/profit_stats_tracker.go index 7330cf16f6..1f61cacd6e 100644 --- a/pkg/report/profit_stats_tracker.go +++ b/pkg/report/profit_stats_tracker.go @@ -25,8 +25,8 @@ func (p *ProfitStatsTracker) Subscribe(session *bbgo.ExchangeSession, symbol str session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: p.Interval}) } -// InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market -func (p *ProfitStatsTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { +// InitLegacy is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market +func (p *ProfitStatsTracker) InitLegacy(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { p.Market = market if *ps == nil { @@ -46,7 +46,7 @@ func (p *ProfitStatsTracker) InitOld(market types.Market, ps **types.ProfitStats // Init initialize the tracker with the given Market func (p *ProfitStatsTracker) Init(market types.Market, ts *types.TradeStats) { ps := types.NewProfitStats(p.Market) - p.InitOld(market, &ps, ts) + p.InitLegacy(market, &ps, ts) } func (p *ProfitStatsTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *core.TradeCollector) { diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index ea5764a778..a0942b0646 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -358,7 +358,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Setup profit tracker if s.ProfitStatsTracker != nil { if s.ProfitStatsTracker.CurrentProfitStats == nil { - s.ProfitStatsTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) + s.ProfitStatsTracker.InitLegacy(s.Market, &s.ProfitStats, s.TradeStats) } // Add strategy parameters to report From 66dd5507d1e3f0220d7f9dbc9c553c83778dcae2 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Jul 2023 10:26:38 +0800 Subject: [PATCH 1172/1392] rename SMA2 to just SMA --- pkg/indicator/v2/boll.go | 2 +- pkg/indicator/v2/sma.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/indicator/v2/boll.go b/pkg/indicator/v2/boll.go index eef85c513b..611f0ea346 100644 --- a/pkg/indicator/v2/boll.go +++ b/pkg/indicator/v2/boll.go @@ -26,7 +26,7 @@ type BOLLStream struct { // -> calculate stdDev -> calculate bandWidth -> get latest SMA -> upBand, downBand func BOLL(source types.Float64Source, window int, k float64) *BOLLStream { // bind these indicators before our main calculator - sma := SMA2(source, window) + sma := SMA(source, window) stdDev := StdDev(source, window) s := &BOLLStream{ diff --git a/pkg/indicator/v2/sma.go b/pkg/indicator/v2/sma.go index dd054c58c8..5afd4f8250 100644 --- a/pkg/indicator/v2/sma.go +++ b/pkg/indicator/v2/sma.go @@ -12,7 +12,7 @@ type SMAStream struct { rawValues *types.Queue } -func SMA2(source types.Float64Source, window int) *SMAStream { +func SMA(source types.Float64Source, window int) *SMAStream { s := &SMAStream{ Float64Series: types.NewFloat64Series(), window: window, From ee9a3269b6e5d6e26a54be0760d2152d7d7b5c97 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Jul 2023 10:29:49 +0800 Subject: [PATCH 1173/1392] indicator/v2: add SMA example --- pkg/indicator/v2/sma_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 pkg/indicator/v2/sma_test.go diff --git a/pkg/indicator/v2/sma_test.go b/pkg/indicator/v2/sma_test.go new file mode 100644 index 0000000000..384964ba39 --- /dev/null +++ b/pkg/indicator/v2/sma_test.go @@ -0,0 +1,22 @@ +package indicatorv2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" +) + +func TestSMA(t *testing.T) { + source := types.NewFloat64Series() + sma := SMA(source, 9) + + data := []float64{1, 2, 3, 4, 5, 6, 7, 8, 9} + for _, d := range data { + source.PushAndEmit(d) + } + + assert.InDelta(t, 5, sma.Last(0), 0.001) + assert.InDelta(t, 4.5, sma.Last(1), 0.001) +} From 0cb76c9169ce7eafd0d310bdf95758a5f8daecf4 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 11 Jul 2023 10:38:41 +0800 Subject: [PATCH 1174/1392] improve/profitStatsTracker: use CsvFormatter interface --- pkg/report/profit_report.go | 89 ++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index 011b371e6b..a564b9c5df 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -108,48 +108,67 @@ func (r *AccumulatedProfitReport) Rotate(ps *types.ProfitStats, ts *types.TradeS r.profitFactorPerInterval.Update(ts.ProfitFactor.Float64()) } +// CsvHeader returns a header slice +func (r *AccumulatedProfitReport) CsvHeader() []string { + titles := []string{ + "#", + "Symbol", + "Total Net Profit", + fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.ProfitMAWindow), + fmt.Sprintf("%s%d Net Profit", r.Interval, r.ShortTermProfitWindow), + "accumulatedFee", + "winRatio", + "profitFactor", + fmt.Sprintf("%s%d Trades", r.Interval, r.Window), + } + + for i := 0; i < len(r.strategyParameters); i++ { + titles = append(titles, r.strategyParameters[i][0]) + } + + return titles +} + +// CsvRecords returns a data slice +func (r *AccumulatedProfitReport) CsvRecords() [][]string { + var data [][]string + + for i := 0; i <= r.Window-1; i++ { + values := []string{ + strconv.Itoa(i + 1), + r.symbol, + strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.profitMAPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedFeePerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.winRatioPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.profitFactorPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedTradesPerInterval.Last(i), 'f', 4, 64), + } + for j := 0; j < len(r.strategyParameters); j++ { + values = append(values, r.strategyParameters[j][1]) + } + + data = append(data, values) + } + + return data +} + // Output Accumulated profit report to a TSV file func (r *AccumulatedProfitReport) Output() { if r.TsvReportPath != "" { + // Open specified file for appending tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath) if err != nil { panic(err) } defer tsvwiter.Close() - // Output title row - titles := []string{ - "#", - "Symbol", - "Total Net Profit", - fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.ProfitMAWindow), - fmt.Sprintf("%s%d Net Profit", r.Interval, r.ShortTermProfitWindow), - "accumulatedFee", - "winRatio", - "profitFactor", - fmt.Sprintf("%s%d Trades", r.Interval, r.Window), - } - for i := 0; i < len(r.strategyParameters); i++ { - titles = append(titles, r.strategyParameters[i][0]) - } - _ = tsvwiter.Write(titles) - - // Output data row - for i := 0; i <= r.Window-1; i++ { - values := []string{ - strconv.Itoa(i + 1), - r.symbol, - strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.profitMAPerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow), 'f', 4, 64), - strconv.FormatFloat(r.accumulatedFeePerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.winRatioPerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.profitFactorPerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.accumulatedTradesPerInterval.Last(i), 'f', 4, 64), - } - for j := 0; j < len(r.strategyParameters); j++ { - values = append(values, r.strategyParameters[j][1]) - } - _ = tsvwiter.Write(values) - } + + // Column Title + _ = tsvwiter.Write(r.CsvHeader()) + + // Output data rows + _ = tsvwiter.WriteAll(r.CsvRecords()) } } From d5e194ca80ed5357bc4bc36274e6d3ea7c11c67d Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 14:56:22 +0800 Subject: [PATCH 1175/1392] feature/profitTracker: prototype --- pkg/bbgo/order_executor_general.go | 5 +++ pkg/report/profit_tracker.go | 63 ++++++++++++++++++++++++++++++ pkg/types/profit.go | 7 ++++ 3 files changed, 75 insertions(+) create mode 100644 pkg/report/profit_tracker.go diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index b3d3bdf13a..3a7320fbe3 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -3,6 +3,7 @@ package bbgo import ( "context" "fmt" + "github.com/c9s/bbgo/pkg/report" "strings" "time" @@ -162,6 +163,10 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { }) } +func (e *GeneralOrderExecutor) BindProfitTracker(profitTracker *report.ProfitTracker) { + profitTracker.Bind(e.tradeCollector, e.session) +} + func (e *GeneralOrderExecutor) Bind() { e.activeMakerOrders.BindStream(e.session.UserDataStream) e.orderStore.BindStream(e.session.UserDataStream) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go new file mode 100644 index 0000000000..04f67acae7 --- /dev/null +++ b/pkg/report/profit_tracker.go @@ -0,0 +1,63 @@ +package report + +import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type ProfitTracker struct { + types.IntervalWindow + + market types.Market + profitStatsSlice []*types.ProfitStats + currentProfitStats **types.ProfitStats +} + +// InitOld is for backward capability. ps is the ProfitStats of the strategy, market is the strategy market +func (p *ProfitTracker) InitOld(ps **types.ProfitStats, market types.Market) { + p.market = market + + if *ps == nil { + *ps = types.NewProfitStats(p.market) + } + + p.currentProfitStats = ps + p.profitStatsSlice = append(p.profitStatsSlice, *ps) +} + +// Init initialize the tracker with the given market +func (p *ProfitTracker) Init(market types.Market) { + p.market = market + *p.currentProfitStats = types.NewProfitStats(p.market) + p.profitStatsSlice = append(p.profitStatsSlice, *p.currentProfitStats) +} + +func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo.ExchangeSession) { + // TODO: Register kline close callback + tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + p.AddProfit(*profit) + }) + + tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + + }) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(p.market.Symbol, p.Interval, func(kline types.KLine) { + + })) +} + +// Rotate the tracker to make a new ProfitStats to record the profits +func (p *ProfitTracker) Rotate() { + *p.currentProfitStats = types.NewProfitStats(p.market) + p.profitStatsSlice = append(p.profitStatsSlice, *p.currentProfitStats) + // Truncate + if len(p.profitStatsSlice) > p.Window { + p.profitStatsSlice = p.profitStatsSlice[len(p.profitStatsSlice)-p.Window:] + } +} + +func (p *ProfitTracker) AddProfit(profit types.Profit) { + (*p.currentProfitStats).AddProfit(profit) +} diff --git a/pkg/types/profit.go b/pkg/types/profit.go index 121e4bc88c..aa57b2e22d 100644 --- a/pkg/types/profit.go +++ b/pkg/types/profit.go @@ -164,6 +164,9 @@ type ProfitStats struct { TodayGrossProfit fixedpoint.Value `json:"todayGrossProfit,omitempty"` TodayGrossLoss fixedpoint.Value `json:"todayGrossLoss,omitempty"` TodaySince int64 `json:"todaySince,omitempty"` + + startTime time.Time + endTime time.Time } func NewProfitStats(market Market) *ProfitStats { @@ -182,6 +185,8 @@ func NewProfitStats(market Market) *ProfitStats { TodayGrossProfit: fixedpoint.Zero, TodayGrossLoss: fixedpoint.Zero, TodaySince: 0, + startTime: time.Now().UTC(), + endTime: time.Now().UTC(), } } @@ -223,6 +228,8 @@ func (s *ProfitStats) AddProfit(profit Profit) { s.AccumulatedGrossLoss = s.AccumulatedGrossLoss.Add(profit.Profit) s.TodayGrossLoss = s.TodayGrossLoss.Add(profit.Profit) } + + s.endTime = profit.TradedAt.UTC() } func (s *ProfitStats) AddTrade(trade Trade) { From 57cdbb1d773287237881495b98ffbe8502c52844 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 15:06:58 +0800 Subject: [PATCH 1176/1392] feature/profitTracker: add AddTrade() --- pkg/report/profit_tracker.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 04f67acae7..85b7972d73 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -34,17 +34,17 @@ func (p *ProfitTracker) Init(market types.Market) { } func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo.ExchangeSession) { - // TODO: Register kline close callback tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { p.AddProfit(*profit) }) tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - + p.AddTrade(trade) }) + // Rotate profitStats slice session.MarketDataStream.OnKLineClosed(types.KLineWith(p.market.Symbol, p.Interval, func(kline types.KLine) { - + p.Rotate() })) } @@ -61,3 +61,7 @@ func (p *ProfitTracker) Rotate() { func (p *ProfitTracker) AddProfit(profit types.Profit) { (*p.currentProfitStats).AddProfit(profit) } + +func (p *ProfitTracker) AddTrade(trade types.Trade) { + (*p.currentProfitStats).AddTrade(trade) +} From a197352c6e1dfd26a91a1c3399c0cff2721adbb3 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 15:18:15 +0800 Subject: [PATCH 1177/1392] feature/profitTracker: use profitTracker in Supertrend strategy --- pkg/report/profit_tracker.go | 29 ++++++++++++++++------------- pkg/strategy/supertrend/strategy.go | 7 +++++++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 85b7972d73..86885034e6 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -9,9 +9,10 @@ import ( type ProfitTracker struct { types.IntervalWindow - market types.Market - profitStatsSlice []*types.ProfitStats - currentProfitStats **types.ProfitStats + ProfitStatsSlice []*types.ProfitStats + CurrentProfitStats **types.ProfitStats + + market types.Market } // InitOld is for backward capability. ps is the ProfitStats of the strategy, market is the strategy market @@ -22,18 +23,20 @@ func (p *ProfitTracker) InitOld(ps **types.ProfitStats, market types.Market) { *ps = types.NewProfitStats(p.market) } - p.currentProfitStats = ps - p.profitStatsSlice = append(p.profitStatsSlice, *ps) + p.CurrentProfitStats = ps + p.ProfitStatsSlice = append(p.ProfitStatsSlice, *ps) } // Init initialize the tracker with the given market func (p *ProfitTracker) Init(market types.Market) { p.market = market - *p.currentProfitStats = types.NewProfitStats(p.market) - p.profitStatsSlice = append(p.profitStatsSlice, *p.currentProfitStats) + *p.CurrentProfitStats = types.NewProfitStats(p.market) + p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) } func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, p.market.Symbol, types.SubscribeOptions{Interval: p.Interval}) + tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { p.AddProfit(*profit) }) @@ -50,18 +53,18 @@ func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo. // Rotate the tracker to make a new ProfitStats to record the profits func (p *ProfitTracker) Rotate() { - *p.currentProfitStats = types.NewProfitStats(p.market) - p.profitStatsSlice = append(p.profitStatsSlice, *p.currentProfitStats) + *p.CurrentProfitStats = types.NewProfitStats(p.market) + p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) // Truncate - if len(p.profitStatsSlice) > p.Window { - p.profitStatsSlice = p.profitStatsSlice[len(p.profitStatsSlice)-p.Window:] + if len(p.ProfitStatsSlice) > p.Window { + p.ProfitStatsSlice = p.ProfitStatsSlice[len(p.ProfitStatsSlice)-p.Window:] } } func (p *ProfitTracker) AddProfit(profit types.Profit) { - (*p.currentProfitStats).AddProfit(profit) + (*p.CurrentProfitStats).AddProfit(profit) } func (p *ProfitTracker) AddTrade(trade types.Trade) { - (*p.currentProfitStats).AddTrade(trade) + (*p.CurrentProfitStats).AddTrade(trade) } diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 3bf569d322..57891b7a21 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -39,6 +39,8 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + ProfitTracker *report.ProfitTracker `json:"profitTracker" persistence:"profit_tracker"` + // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -328,6 +330,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats = types.NewTradeStats(s.Symbol) } + if s.ProfitTracker.CurrentProfitStats == nil { + s.ProfitTracker.InitOld(&s.ProfitStats, s.Market) + } + // Interval profit report if bbgo.IsBackTesting { startTime := s.Environment.StartTime() @@ -349,6 +355,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindProfitStats(s.ProfitStats) s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.BindProfitTracker(s.ProfitTracker) s.orderExecutor.Bind() // AccountValueCalculator From 027acfe3b5e2590e2c6b86d7bed3bbddd74effef Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 18:06:47 +0800 Subject: [PATCH 1178/1392] feature/profitTracker: integrate profit report with profit tracker --- pkg/bbgo/order_executor_general.go | 15 ++- pkg/report/profit_report.go | 181 +++++++++++----------------- pkg/report/profit_tracker.go | 57 +++++---- pkg/strategy/supertrend/strategy.go | 53 ++++---- 4 files changed, 136 insertions(+), 170 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 3a7320fbe3..9f818bb6d9 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -164,7 +164,20 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { } func (e *GeneralOrderExecutor) BindProfitTracker(profitTracker *report.ProfitTracker) { - profitTracker.Bind(e.tradeCollector, e.session) + e.session.Subscribe(types.KLineChannel, profitTracker.Market.Symbol, types.SubscribeOptions{Interval: profitTracker.Interval}) + + e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + profitTracker.AddProfit(*profit) + }) + + e.tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + profitTracker.AddTrade(trade) + }) + + // Rotate profitStats slice + e.session.MarketDataStream.OnKLineClosed(types.KLineWith(profitTracker.Market.Symbol, profitTracker.Interval, func(kline types.KLine) { + profitTracker.Rotate() + })) } func (e *GeneralOrderExecutor) Bind() { diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index 9d39b32786..c338a99217 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -2,7 +2,6 @@ package report import ( "fmt" - "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/data/tsv" "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -12,134 +11,104 @@ import ( // AccumulatedProfitReport For accumulated profit report output type AccumulatedProfitReport struct { - // AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades - AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"` + // ProfitMAWindow Accumulated profit SMA window + ProfitMAWindow int `json:"ProfitMAWindow"` - // IntervalWindow interval window, in days - IntervalWindow int `json:"intervalWindow"` - - // NumberOfInterval How many intervals to output to TSV - NumberOfInterval int `json:"NumberOfInterval"` + // ShortTermProfitWindow The window to sum up the short-term profit + ShortTermProfitWindow int `json:"shortTermProfitWindow"` // TsvReportPath The path to output report to TsvReportPath string `json:"tsvReportPath"` - // AccumulatedDailyProfitWindow The window to sum up the daily profit, in days - AccumulatedDailyProfitWindow int `json:"accumulatedDailyProfitWindow"` + symbol string - Symbol string + types.IntervalWindow // Accumulated profit - accumulatedProfit fixedpoint.Value - accumulatedProfitPerDay floats.Slice - previousAccumulatedProfit fixedpoint.Value + accumulatedProfit fixedpoint.Value + accumulatedProfitPerInterval floats.Slice // Accumulated profit MA - accumulatedProfitMA *indicator.SMA - accumulatedProfitMAPerDay floats.Slice + profitMA *indicator.SMA + profitMAPerInterval floats.Slice - // Daily profit - dailyProfit floats.Slice + // Profit of each interval + ProfitPerInterval floats.Slice // Accumulated fee - accumulatedFee fixedpoint.Value - accumulatedFeePerDay floats.Slice + accumulatedFee fixedpoint.Value + accumulatedFeePerInterval floats.Slice // Win ratio - winRatioPerDay floats.Slice + winRatioPerInterval floats.Slice // Profit factor - profitFactorPerDay floats.Slice + profitFactorPerInterval floats.Slice // Trade number - dailyTrades floats.Slice - accumulatedTrades int - previousAccumulatedTrades int + accumulatedTrades int + accumulatedTradesPerInterval floats.Slice // Extra values - extraValues [][2]string + strategyParameters [][2]string } -func (r *AccumulatedProfitReport) Initialize(Symbol string, session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor, TradeStats *types.TradeStats) { - r.Symbol = Symbol +func (r *AccumulatedProfitReport) Initialize(symbol string, interval types.Interval, window int) { + r.symbol = symbol + r.Interval = interval + r.Window = window - if r.AccumulatedProfitMAWindow <= 0 { - r.AccumulatedProfitMAWindow = 60 - } - if r.IntervalWindow <= 0 { - r.IntervalWindow = 7 - } - if r.AccumulatedDailyProfitWindow <= 0 { - r.AccumulatedDailyProfitWindow = 7 - } - if r.NumberOfInterval <= 0 { - r.NumberOfInterval = 1 + if r.ProfitMAWindow <= 0 { + r.ProfitMAWindow = 60 } - r.accumulatedProfitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1d, Window: r.AccumulatedProfitMAWindow}} - - session.Subscribe(types.KLineChannel, r.Symbol, types.SubscribeOptions{Interval: types.Interval1d}) - - // Record profit - orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) { - if profit == nil { - return - } - - r.RecordProfit(profit.Profit) - }) - // Record trade - orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - r.RecordTrade(trade.Fee) - }) + if r.Window <= 0 { + r.Window = 7 + } - // Record daily status - session.MarketDataStream.OnKLineClosed(types.KLineWith(r.Symbol, types.Interval1d, func(kline types.KLine) { - r.DailyUpdate(TradeStats) - })) -} + if r.ShortTermProfitWindow <= 0 { + r.ShortTermProfitWindow = 7 + } -func (r *AccumulatedProfitReport) AddExtraValue(valueAndTitle [2]string) { - r.extraValues = append(r.extraValues, valueAndTitle) + r.profitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: r.Interval, Window: r.ProfitMAWindow}} } -func (r *AccumulatedProfitReport) RecordProfit(profit fixedpoint.Value) { - r.accumulatedProfit = r.accumulatedProfit.Add(profit) +func (r *AccumulatedProfitReport) AddStrategyParameter(title string, value string) { + r.strategyParameters = append(r.strategyParameters, [2]string{title, value}) } -func (r *AccumulatedProfitReport) RecordTrade(fee fixedpoint.Value) { - r.accumulatedFee = r.accumulatedFee.Add(fee) +func (r *AccumulatedProfitReport) AddTrade(trade types.Trade) { + r.accumulatedFee = r.accumulatedFee.Add(trade.Fee) r.accumulatedTrades += 1 } -func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { - // Daily profit - r.dailyProfit.Update(r.accumulatedProfit.Sub(r.previousAccumulatedProfit).Float64()) - r.previousAccumulatedProfit = r.accumulatedProfit - +func (r *AccumulatedProfitReport) Rotate(ps *types.ProfitStats, ts *types.TradeStats) { // Accumulated profit - r.accumulatedProfitPerDay.Update(r.accumulatedProfit.Float64()) + r.accumulatedProfit.Add(ps.AccumulatedNetProfit) + r.accumulatedProfitPerInterval.Update(r.accumulatedProfit.Float64()) - // Accumulated profit MA - r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) - r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last(0)) + // Profit of each interval + r.ProfitPerInterval.Update(ps.AccumulatedNetProfit.Float64()) + + // Profit MA + r.profitMA.Update(r.accumulatedProfit.Float64()) + r.profitMAPerInterval.Update(r.profitMA.Last(0)) // Accumulated Fee - r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) + r.accumulatedFeePerInterval.Update(r.accumulatedFee.Float64()) + + // Trades + r.accumulatedTradesPerInterval.Update(float64(r.accumulatedTrades)) // Win ratio - r.winRatioPerDay.Update(tradeStats.WinningRatio.Float64()) + r.winRatioPerInterval.Update(ts.WinningRatio.Float64()) // Profit factor - r.profitFactorPerDay.Update(tradeStats.ProfitFactor.Float64()) - - // Daily trades - r.dailyTrades.Update(float64(r.accumulatedTrades - r.previousAccumulatedTrades)) - r.previousAccumulatedTrades = r.accumulatedTrades + r.profitFactorPerInterval.Update(ts.ProfitFactor.Float64()) } // Output Accumulated profit report to a TSV file -func (r *AccumulatedProfitReport) Output(symbol string) { +func (r *AccumulatedProfitReport) Output() { if r.TsvReportPath != "" { tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath) if err != nil { @@ -150,46 +119,34 @@ func (r *AccumulatedProfitReport) Output(symbol string) { titles := []string{ "#", "Symbol", - "accumulatedProfit", - "accumulatedProfitMA", - fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), + "Total Net Profit", + fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.Window), + fmt.Sprintf("%s %d Net Profit", r.Interval, r.ShortTermProfitWindow), "accumulatedFee", - "accumulatedNetProfit", "winRatio", "profitFactor", - "60D trades", + fmt.Sprintf("%s %d Trades", r.Interval, r.Window), } - for i := 0; i < len(r.extraValues); i++ { - titles = append(titles, r.extraValues[i][0]) + for i := 0; i < len(r.strategyParameters); i++ { + titles = append(titles, r.strategyParameters[i][0]) } _ = tsvwiter.Write(titles) // Output data row - for i := 0; i <= r.NumberOfInterval-1; i++ { - accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i) - accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit) - accumulatedProfitMA := r.accumulatedProfitMAPerDay.Index(r.IntervalWindow * i) - accumulatedProfitMAStr := fmt.Sprintf("%f", accumulatedProfitMA) - intervalAccumulatedProfit := r.dailyProfit.Tail(r.AccumulatedDailyProfitWindow+r.IntervalWindow*i).Sum() - r.dailyProfit.Tail(r.IntervalWindow*i).Sum() - intervalAccumulatedProfitStr := fmt.Sprintf("%f", intervalAccumulatedProfit) - accumulatedFee := fmt.Sprintf("%f", r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) - accumulatedNetProfit := fmt.Sprintf("%f", accumulatedProfit-r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) - winRatio := fmt.Sprintf("%f", r.winRatioPerDay.Index(r.IntervalWindow*i)) - profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i)) - trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum() - tradesStr := fmt.Sprintf("%f", trades) + for i := 0; i <= r.Window-1; i++ { values := []string{ fmt.Sprintf("%d", i+1), - symbol, accumulatedProfitStr, - accumulatedProfitMAStr, - intervalAccumulatedProfitStr, - accumulatedFee, - accumulatedNetProfit, - winRatio, profitFactor, - tradesStr, + r.symbol, + fmt.Sprintf("%f", r.accumulatedProfitPerInterval.Last(i)), + fmt.Sprintf("%f", r.profitMAPerInterval.Last(i)), + fmt.Sprintf("%f", r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow)), + fmt.Sprintf("%f", r.accumulatedFeePerInterval.Last(i)), + fmt.Sprintf("%f", r.winRatioPerInterval.Last(i)), + fmt.Sprintf("%f", r.profitFactorPerInterval.Last(i)), + fmt.Sprintf("%f", r.accumulatedTradesPerInterval.Last(i)), } - for j := 0; j < len(r.extraValues); j++ { - values = append(values, r.extraValues[j][1]) + for j := 0; j < len(r.strategyParameters); j++ { + values = append(values, r.strategyParameters[j][1]) } _ = tsvwiter.Write(values) } diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 86885034e6..2bfad3338a 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -1,64 +1,59 @@ package report import ( - "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) type ProfitTracker struct { types.IntervalWindow + // Accumulated profit report + AccumulatedProfitReport *AccumulatedProfitReport `json:"accumulatedProfitReport"` + + Market types.Market + ProfitStatsSlice []*types.ProfitStats CurrentProfitStats **types.ProfitStats - market types.Market + tradeStats *types.TradeStats } -// InitOld is for backward capability. ps is the ProfitStats of the strategy, market is the strategy market -func (p *ProfitTracker) InitOld(ps **types.ProfitStats, market types.Market) { - p.market = market +// InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market +func (p *ProfitTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { + p.Market = market if *ps == nil { - *ps = types.NewProfitStats(p.market) + *ps = types.NewProfitStats(p.Market) } + p.tradeStats = ts + p.CurrentProfitStats = ps p.ProfitStatsSlice = append(p.ProfitStatsSlice, *ps) -} -// Init initialize the tracker with the given market -func (p *ProfitTracker) Init(market types.Market) { - p.market = market - *p.CurrentProfitStats = types.NewProfitStats(p.market) - p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.Initialize(p.Market.Symbol, p.Interval, p.Window) + } } -func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, p.market.Symbol, types.SubscribeOptions{Interval: p.Interval}) - - tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { - p.AddProfit(*profit) - }) - - tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - p.AddTrade(trade) - }) - - // Rotate profitStats slice - session.MarketDataStream.OnKLineClosed(types.KLineWith(p.market.Symbol, p.Interval, func(kline types.KLine) { - p.Rotate() - })) +// Init initialize the tracker with the given Market +func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { + ps := types.NewProfitStats(p.Market) + p.InitOld(market, &ps, ts) } // Rotate the tracker to make a new ProfitStats to record the profits func (p *ProfitTracker) Rotate() { - *p.CurrentProfitStats = types.NewProfitStats(p.market) + *p.CurrentProfitStats = types.NewProfitStats(p.Market) p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) // Truncate if len(p.ProfitStatsSlice) > p.Window { p.ProfitStatsSlice = p.ProfitStatsSlice[len(p.ProfitStatsSlice)-p.Window:] } + + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.Rotate(*p.CurrentProfitStats, p.tradeStats) + } } func (p *ProfitTracker) AddProfit(profit types.Profit) { @@ -67,4 +62,8 @@ func (p *ProfitTracker) AddProfit(profit types.Profit) { func (p *ProfitTracker) AddTrade(trade types.Trade) { (*p.CurrentProfitStats).AddTrade(trade) + + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.AddTrade(trade) + } } diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 57891b7a21..946dcb11c7 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -39,8 +39,6 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` - ProfitTracker *report.ProfitTracker `json:"profitTracker" persistence:"profit_tracker"` - // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -104,8 +102,7 @@ type Strategy struct { // StrategyController bbgo.StrategyController - // Accumulated profit report - AccumulatedProfitReport *report.AccumulatedProfitReport `json:"accumulatedProfitReport"` + ProfitTracker *report.ProfitTracker `json:"profitTracker" persistence:"profit_tracker"` } func (s *Strategy) ID() string { @@ -330,8 +327,23 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats = types.NewTradeStats(s.Symbol) } - if s.ProfitTracker.CurrentProfitStats == nil { - s.ProfitTracker.InitOld(&s.ProfitStats, s.Market) + if s.ProfitTracker != nil { + if s.ProfitTracker.CurrentProfitStats == nil { + s.ProfitTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) + } + + // Add strategy parameters to report + if s.ProfitTracker.AccumulatedProfitReport != nil { + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) + } } // Interval profit report @@ -361,25 +373,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // AccountValueCalculator s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency) - // Accumulated profit report - if bbgo.IsBackTesting { - if s.AccumulatedProfitReport == nil { - s.AccumulatedProfitReport = &report.AccumulatedProfitReport{} - } - s.AccumulatedProfitReport.Initialize(s.Symbol, session, s.orderExecutor, s.TradeStats) - - // Add strategy parameters to report - s.AccumulatedProfitReport.AddExtraValue([2]string{"window", fmt.Sprintf("%d", s.Window)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)}) - } - // For drawing profitSlice := floats.Slice{1., 1.} price, _ := session.LastPrice(s.Symbol) @@ -527,10 +520,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - if bbgo.IsBackTesting { - // Output accumulated profit report - defer s.AccumulatedProfitReport.Output(s.Symbol) + // Output profit report + if s.ProfitTracker != nil { + if s.ProfitTracker.AccumulatedProfitReport != nil { + s.ProfitTracker.AccumulatedProfitReport.Output() + } + } + if bbgo.IsBackTesting { // Draw graph if s.DrawGraph { if err := s.Draw(&profitSlice, &cumProfitSlice); err != nil { From 5513330816ae5d1ddbec85f1e06fc53e87cbacce Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 18:32:44 +0800 Subject: [PATCH 1179/1392] feature/profitTracker: fix bugs --- config/supertrend.yaml | 8 ++++++++ pkg/bbgo/order_executor_general.go | 4 ++++ pkg/report/profit_report.go | 10 +++++----- pkg/report/profit_tracker.go | 9 +++++---- pkg/strategy/supertrend/strategy.go | 8 +++++--- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index e51af4c98a..6b20dd7a3f 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -112,3 +112,11 @@ exchangeStrategies: # If true, looking for lower lows in long position and higher highs in short position. If false, looking for # higher highs in long position and lower lows in short position oppositeDirectionAsPosition: false + + profitTracker: + Interval: 1d + Window: 30 + accumulatedProfitReport: + profitMAWindow: 60 + shortTermProfitWindow: 14 + tsvReportPath: res.tsv diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 9f818bb6d9..1d896b0021 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -167,6 +167,10 @@ func (e *GeneralOrderExecutor) BindProfitTracker(profitTracker *report.ProfitTra e.session.Subscribe(types.KLineChannel, profitTracker.Market.Symbol, types.SubscribeOptions{Interval: profitTracker.Interval}) e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + if profit == nil { + return + } + profitTracker.AddProfit(*profit) }) diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index c338a99217..ff330a131c 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -12,7 +12,7 @@ import ( // AccumulatedProfitReport For accumulated profit report output type AccumulatedProfitReport struct { // ProfitMAWindow Accumulated profit SMA window - ProfitMAWindow int `json:"ProfitMAWindow"` + ProfitMAWindow int `json:"profitMAWindow"` // ShortTermProfitWindow The window to sum up the short-term profit ShortTermProfitWindow int `json:"shortTermProfitWindow"` @@ -84,7 +84,7 @@ func (r *AccumulatedProfitReport) AddTrade(trade types.Trade) { func (r *AccumulatedProfitReport) Rotate(ps *types.ProfitStats, ts *types.TradeStats) { // Accumulated profit - r.accumulatedProfit.Add(ps.AccumulatedNetProfit) + r.accumulatedProfit = r.accumulatedProfit.Add(ps.AccumulatedNetProfit) r.accumulatedProfitPerInterval.Update(r.accumulatedProfit.Float64()) // Profit of each interval @@ -120,12 +120,12 @@ func (r *AccumulatedProfitReport) Output() { "#", "Symbol", "Total Net Profit", - fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.Window), - fmt.Sprintf("%s %d Net Profit", r.Interval, r.ShortTermProfitWindow), + fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.ProfitMAWindow), + fmt.Sprintf("%s%d Net Profit", r.Interval, r.ShortTermProfitWindow), "accumulatedFee", "winRatio", "profitFactor", - fmt.Sprintf("%s %d Trades", r.Interval, r.Window), + fmt.Sprintf("%s%d Trades", r.Interval, r.Window), } for i := 0; i < len(r.strategyParameters); i++ { titles = append(titles, r.strategyParameters[i][0]) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 2bfad3338a..faac2b2e86 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -44,16 +44,17 @@ func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { // Rotate the tracker to make a new ProfitStats to record the profits func (p *ProfitTracker) Rotate() { + // Update report + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.Rotate(*p.CurrentProfitStats, p.tradeStats) + } + *p.CurrentProfitStats = types.NewProfitStats(p.Market) p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) // Truncate if len(p.ProfitStatsSlice) > p.Window { p.ProfitStatsSlice = p.ProfitStatsSlice[len(p.ProfitStatsSlice)-p.Window:] } - - if p.AccumulatedProfitReport != nil { - p.AccumulatedProfitReport.Rotate(*p.CurrentProfitStats, p.tradeStats) - } } func (p *ProfitTracker) AddProfit(profit types.Profit) { diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 946dcb11c7..7e8fb87f84 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -39,6 +39,8 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + ProfitTracker *report.ProfitTracker `json:"profitTracker"` + // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -101,8 +103,6 @@ type Strategy struct { // StrategyController bbgo.StrategyController - - ProfitTracker *report.ProfitTracker `json:"profitTracker" persistence:"profit_tracker"` } func (s *Strategy) ID() string { @@ -367,7 +367,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindProfitStats(s.ProfitStats) s.orderExecutor.BindTradeStats(s.TradeStats) - s.orderExecutor.BindProfitTracker(s.ProfitTracker) + if s.ProfitTracker != nil { + s.orderExecutor.BindProfitTracker(s.ProfitTracker) + } s.orderExecutor.Bind() // AccountValueCalculator From bfeb43fc1c9143ea008aa5a7b4d3803d59f8092f Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 15:09:09 +0800 Subject: [PATCH 1180/1392] fix/profitTracker: fix typo in config --- config/supertrend.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 6b20dd7a3f..673d3f47e8 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -114,8 +114,8 @@ exchangeStrategies: oppositeDirectionAsPosition: false profitTracker: - Interval: 1d - Window: 30 + interval: 1d + window: 30 accumulatedProfitReport: profitMAWindow: 60 shortTermProfitWindow: 14 From 80170e0397e9189d10bc75b5eaf80238ecdfdc97 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 15:20:00 +0800 Subject: [PATCH 1181/1392] improve/profitTracker: do not bind in order executor --- pkg/bbgo/order_executor_general.go | 22 --------------- pkg/report/profit_tracker.go | 23 ++++++++++++++++ pkg/strategy/supertrend/strategy.go | 42 ++++++++++++++--------------- 3 files changed, 44 insertions(+), 43 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 1d896b0021..b3d3bdf13a 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -3,7 +3,6 @@ package bbgo import ( "context" "fmt" - "github.com/c9s/bbgo/pkg/report" "strings" "time" @@ -163,27 +162,6 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { }) } -func (e *GeneralOrderExecutor) BindProfitTracker(profitTracker *report.ProfitTracker) { - e.session.Subscribe(types.KLineChannel, profitTracker.Market.Symbol, types.SubscribeOptions{Interval: profitTracker.Interval}) - - e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { - if profit == nil { - return - } - - profitTracker.AddProfit(*profit) - }) - - e.tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - profitTracker.AddTrade(trade) - }) - - // Rotate profitStats slice - e.session.MarketDataStream.OnKLineClosed(types.KLineWith(profitTracker.Market.Symbol, profitTracker.Interval, func(kline types.KLine) { - profitTracker.Rotate() - })) -} - func (e *GeneralOrderExecutor) Bind() { e.activeMakerOrders.BindStream(e.session.UserDataStream) e.orderStore.BindStream(e.session.UserDataStream) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index faac2b2e86..7be3c86252 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -1,6 +1,8 @@ package report import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -42,6 +44,27 @@ func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { p.InitOld(market, &ps, ts) } +func (p *ProfitTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) { + session.Subscribe(types.KLineChannel, p.Market.Symbol, types.SubscribeOptions{Interval: p.Interval}) + + tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + if profit == nil { + return + } + + p.AddProfit(*profit) + }) + + tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + p.AddTrade(trade) + }) + + // Rotate profitStats slice + session.MarketDataStream.OnKLineClosed(types.KLineWith(p.Market.Symbol, p.Interval, func(kline types.KLine) { + p.Rotate() + })) +} + // Rotate the tracker to make a new ProfitStats to record the profits func (p *ProfitTracker) Rotate() { // Update report diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 7e8fb87f84..d5181e89c0 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -327,25 +327,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats = types.NewTradeStats(s.Symbol) } - if s.ProfitTracker != nil { - if s.ProfitTracker.CurrentProfitStats == nil { - s.ProfitTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) - } - - // Add strategy parameters to report - if s.ProfitTracker.AccumulatedProfitReport != nil { - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) - } - } - // Interval profit report if bbgo.IsBackTesting { startTime := s.Environment.StartTime() @@ -367,10 +348,29 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindProfitStats(s.ProfitStats) s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.Bind() + + // Setup profit tracker if s.ProfitTracker != nil { - s.orderExecutor.BindProfitTracker(s.ProfitTracker) + if s.ProfitTracker.CurrentProfitStats == nil { + s.ProfitTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) + } + + // Add strategy parameters to report + if s.ProfitTracker.AccumulatedProfitReport != nil { + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) + } + + s.ProfitTracker.Bind(s.session, s.orderExecutor.TradeCollector()) } - s.orderExecutor.Bind() // AccountValueCalculator s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency) From bcbb27de79d5eae26d03dfc0cdf7fc8563ca1737 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 15:26:57 +0800 Subject: [PATCH 1182/1392] improve/profitTracker: subscribe kline in strategy Subscribe() --- pkg/report/profit_tracker.go | 6 ++++-- pkg/strategy/supertrend/strategy.go | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 7be3c86252..95e83341f0 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -20,6 +20,10 @@ type ProfitTracker struct { tradeStats *types.TradeStats } +func (p *ProfitTracker) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, p.Market.Symbol, types.SubscribeOptions{Interval: p.Interval}) +} + // InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market func (p *ProfitTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { p.Market = market @@ -45,8 +49,6 @@ func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { } func (p *ProfitTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) { - session.Subscribe(types.KLineChannel, p.Market.Symbol, types.SubscribeOptions{Interval: p.Interval}) - tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { if profit == nil { return diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index d5181e89c0..40eead9b08 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -130,6 +130,11 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LinearRegression.Interval}) s.ExitMethods.SetAndSubscribe(session, s) + + // Profit tracker + if s.ProfitTracker != nil { + s.ProfitTracker.Subscribe(session) + } } // Position control From ae7ae27d8226576f9c6e399ac690bc5874f17066 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 15:37:19 +0800 Subject: [PATCH 1183/1392] improve/profitStatsTracker: rename ProfitTracker to ProfitStatsTracker --- config/supertrend.yaml | 2 +- pkg/report/profit_tracker.go | 16 ++++++------ pkg/strategy/supertrend/strategy.go | 40 ++++++++++++++--------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 673d3f47e8..99815245bd 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -113,7 +113,7 @@ exchangeStrategies: # higher highs in long position and lower lows in short position oppositeDirectionAsPosition: false - profitTracker: + profitStatsTracker: interval: 1d window: 30 accumulatedProfitReport: diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 95e83341f0..9a01710103 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -6,7 +6,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -type ProfitTracker struct { +type ProfitStatsTracker struct { types.IntervalWindow // Accumulated profit report @@ -20,12 +20,12 @@ type ProfitTracker struct { tradeStats *types.TradeStats } -func (p *ProfitTracker) Subscribe(session *bbgo.ExchangeSession) { +func (p *ProfitStatsTracker) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, p.Market.Symbol, types.SubscribeOptions{Interval: p.Interval}) } // InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market -func (p *ProfitTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { +func (p *ProfitStatsTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { p.Market = market if *ps == nil { @@ -43,12 +43,12 @@ func (p *ProfitTracker) InitOld(market types.Market, ps **types.ProfitStats, ts } // Init initialize the tracker with the given Market -func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { +func (p *ProfitStatsTracker) Init(market types.Market, ts *types.TradeStats) { ps := types.NewProfitStats(p.Market) p.InitOld(market, &ps, ts) } -func (p *ProfitTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) { +func (p *ProfitStatsTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) { tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { if profit == nil { return @@ -68,7 +68,7 @@ func (p *ProfitTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo } // Rotate the tracker to make a new ProfitStats to record the profits -func (p *ProfitTracker) Rotate() { +func (p *ProfitStatsTracker) Rotate() { // Update report if p.AccumulatedProfitReport != nil { p.AccumulatedProfitReport.Rotate(*p.CurrentProfitStats, p.tradeStats) @@ -82,11 +82,11 @@ func (p *ProfitTracker) Rotate() { } } -func (p *ProfitTracker) AddProfit(profit types.Profit) { +func (p *ProfitStatsTracker) AddProfit(profit types.Profit) { (*p.CurrentProfitStats).AddProfit(profit) } -func (p *ProfitTracker) AddTrade(trade types.Trade) { +func (p *ProfitStatsTracker) AddTrade(trade types.Trade) { (*p.CurrentProfitStats).AddTrade(trade) if p.AccumulatedProfitReport != nil { diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 40eead9b08..da904a064e 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -39,7 +39,7 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` - ProfitTracker *report.ProfitTracker `json:"profitTracker"` + ProfitStatsTracker *report.ProfitStatsTracker `json:"profitStatsTracker"` // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -132,8 +132,8 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { s.ExitMethods.SetAndSubscribe(session, s) // Profit tracker - if s.ProfitTracker != nil { - s.ProfitTracker.Subscribe(session) + if s.ProfitStatsTracker != nil { + s.ProfitStatsTracker.Subscribe(session) } } @@ -356,25 +356,25 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.Bind() // Setup profit tracker - if s.ProfitTracker != nil { - if s.ProfitTracker.CurrentProfitStats == nil { - s.ProfitTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) + if s.ProfitStatsTracker != nil { + if s.ProfitStatsTracker.CurrentProfitStats == nil { + s.ProfitStatsTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) } // Add strategy parameters to report - if s.ProfitTracker.AccumulatedProfitReport != nil { - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) - s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) + if s.ProfitStatsTracker.AccumulatedProfitReport != nil { + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) } - s.ProfitTracker.Bind(s.session, s.orderExecutor.TradeCollector()) + s.ProfitStatsTracker.Bind(s.session, s.orderExecutor.TradeCollector()) } // AccountValueCalculator @@ -528,9 +528,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se defer wg.Done() // Output profit report - if s.ProfitTracker != nil { - if s.ProfitTracker.AccumulatedProfitReport != nil { - s.ProfitTracker.AccumulatedProfitReport.Output() + if s.ProfitStatsTracker != nil { + if s.ProfitStatsTracker.AccumulatedProfitReport != nil { + s.ProfitStatsTracker.AccumulatedProfitReport.Output() } } From 2ccce12cbf179a5de1365ee00ca8010fa9e9411a Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 15:50:18 +0800 Subject: [PATCH 1184/1392] improve/profitStatsTracker: temporarily remove lines relate to time in profit stats --- .../{profit_tracker.go => profit_stats_tracker.go} | 0 pkg/types/profit.go | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) rename pkg/report/{profit_tracker.go => profit_stats_tracker.go} (100%) diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_stats_tracker.go similarity index 100% rename from pkg/report/profit_tracker.go rename to pkg/report/profit_stats_tracker.go diff --git a/pkg/types/profit.go b/pkg/types/profit.go index aa57b2e22d..c7742e0b8f 100644 --- a/pkg/types/profit.go +++ b/pkg/types/profit.go @@ -165,8 +165,8 @@ type ProfitStats struct { TodayGrossLoss fixedpoint.Value `json:"todayGrossLoss,omitempty"` TodaySince int64 `json:"todaySince,omitempty"` - startTime time.Time - endTime time.Time + //StartTime time.Time + //EndTime time.Time } func NewProfitStats(market Market) *ProfitStats { @@ -185,8 +185,8 @@ func NewProfitStats(market Market) *ProfitStats { TodayGrossProfit: fixedpoint.Zero, TodayGrossLoss: fixedpoint.Zero, TodaySince: 0, - startTime: time.Now().UTC(), - endTime: time.Now().UTC(), + //StartTime: time.Now().UTC(), + //EndTime: time.Now().UTC(), } } @@ -229,7 +229,7 @@ func (s *ProfitStats) AddProfit(profit Profit) { s.TodayGrossLoss = s.TodayGrossLoss.Add(profit.Profit) } - s.endTime = profit.TradedAt.UTC() + //s.EndTime = profit.TradedAt.UTC() } func (s *ProfitStats) AddTrade(trade Trade) { From 4c1639cf007929f9e51116299af9e01d8a690cf0 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 16:01:20 +0800 Subject: [PATCH 1185/1392] fix/profitStatsTracker: market is initiated after strategy Subscribe() --- pkg/report/profit_stats_tracker.go | 4 ++-- pkg/strategy/supertrend/strategy.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/report/profit_stats_tracker.go b/pkg/report/profit_stats_tracker.go index 9a01710103..9ad4c636b9 100644 --- a/pkg/report/profit_stats_tracker.go +++ b/pkg/report/profit_stats_tracker.go @@ -20,8 +20,8 @@ type ProfitStatsTracker struct { tradeStats *types.TradeStats } -func (p *ProfitStatsTracker) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, p.Market.Symbol, types.SubscribeOptions{Interval: p.Interval}) +func (p *ProfitStatsTracker) Subscribe(session *bbgo.ExchangeSession, symbol string) { + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: p.Interval}) } // InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index da904a064e..ea5764a778 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -133,7 +133,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // Profit tracker if s.ProfitStatsTracker != nil { - s.ProfitStatsTracker.Subscribe(session) + s.ProfitStatsTracker.Subscribe(session, s.Symbol) } } From 2a80d708af31cbaf32d7fd8514b49de9cecdeb47 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 10 Jul 2023 16:33:27 +0800 Subject: [PATCH 1186/1392] ref/profitStatsTracker: TradeCollector is move to core pkg --- pkg/report/profit_stats_tracker.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/report/profit_stats_tracker.go b/pkg/report/profit_stats_tracker.go index 9ad4c636b9..7330cf16f6 100644 --- a/pkg/report/profit_stats_tracker.go +++ b/pkg/report/profit_stats_tracker.go @@ -2,6 +2,7 @@ package report import ( "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -48,7 +49,7 @@ func (p *ProfitStatsTracker) Init(market types.Market, ts *types.TradeStats) { p.InitOld(market, &ps, ts) } -func (p *ProfitStatsTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *bbgo.TradeCollector) { +func (p *ProfitStatsTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *core.TradeCollector) { tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { if profit == nil { return From 928a77cb8b63120192442d5477f338b90c4d607d Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 11 Jul 2023 10:21:38 +0800 Subject: [PATCH 1187/1392] improve/profitStatsTracker: use strconv instead of Sprintf() --- pkg/report/profit_report.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index ff330a131c..011b371e6b 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -7,6 +7,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" + "strconv" ) // AccumulatedProfitReport For accumulated profit report output @@ -135,15 +136,15 @@ func (r *AccumulatedProfitReport) Output() { // Output data row for i := 0; i <= r.Window-1; i++ { values := []string{ - fmt.Sprintf("%d", i+1), + strconv.Itoa(i + 1), r.symbol, - fmt.Sprintf("%f", r.accumulatedProfitPerInterval.Last(i)), - fmt.Sprintf("%f", r.profitMAPerInterval.Last(i)), - fmt.Sprintf("%f", r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow)), - fmt.Sprintf("%f", r.accumulatedFeePerInterval.Last(i)), - fmt.Sprintf("%f", r.winRatioPerInterval.Last(i)), - fmt.Sprintf("%f", r.profitFactorPerInterval.Last(i)), - fmt.Sprintf("%f", r.accumulatedTradesPerInterval.Last(i)), + strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.profitMAPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedFeePerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.winRatioPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.profitFactorPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedTradesPerInterval.Last(i), 'f', 4, 64), } for j := 0; j < len(r.strategyParameters); j++ { values = append(values, r.strategyParameters[j][1]) From 1a90cd0322cc7779d7c3aebf8c7a608392babaeb Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 11 Jul 2023 10:24:07 +0800 Subject: [PATCH 1188/1392] improve/profitStatsTracker: rename InitOld() to InitLegacy() --- pkg/report/profit_stats_tracker.go | 6 +++--- pkg/strategy/supertrend/strategy.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/report/profit_stats_tracker.go b/pkg/report/profit_stats_tracker.go index 7330cf16f6..1f61cacd6e 100644 --- a/pkg/report/profit_stats_tracker.go +++ b/pkg/report/profit_stats_tracker.go @@ -25,8 +25,8 @@ func (p *ProfitStatsTracker) Subscribe(session *bbgo.ExchangeSession, symbol str session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: p.Interval}) } -// InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market -func (p *ProfitStatsTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { +// InitLegacy is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market +func (p *ProfitStatsTracker) InitLegacy(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { p.Market = market if *ps == nil { @@ -46,7 +46,7 @@ func (p *ProfitStatsTracker) InitOld(market types.Market, ps **types.ProfitStats // Init initialize the tracker with the given Market func (p *ProfitStatsTracker) Init(market types.Market, ts *types.TradeStats) { ps := types.NewProfitStats(p.Market) - p.InitOld(market, &ps, ts) + p.InitLegacy(market, &ps, ts) } func (p *ProfitStatsTracker) Bind(session *bbgo.ExchangeSession, tradeCollector *core.TradeCollector) { diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index ea5764a778..a0942b0646 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -358,7 +358,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Setup profit tracker if s.ProfitStatsTracker != nil { if s.ProfitStatsTracker.CurrentProfitStats == nil { - s.ProfitStatsTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) + s.ProfitStatsTracker.InitLegacy(s.Market, &s.ProfitStats, s.TradeStats) } // Add strategy parameters to report From 6e5497230447f5d502ea957d7cc69b7946144c9c Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 11 Jul 2023 10:38:41 +0800 Subject: [PATCH 1189/1392] improve/profitStatsTracker: use CsvFormatter interface --- pkg/report/profit_report.go | 89 ++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index 011b371e6b..a564b9c5df 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -108,48 +108,67 @@ func (r *AccumulatedProfitReport) Rotate(ps *types.ProfitStats, ts *types.TradeS r.profitFactorPerInterval.Update(ts.ProfitFactor.Float64()) } +// CsvHeader returns a header slice +func (r *AccumulatedProfitReport) CsvHeader() []string { + titles := []string{ + "#", + "Symbol", + "Total Net Profit", + fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.ProfitMAWindow), + fmt.Sprintf("%s%d Net Profit", r.Interval, r.ShortTermProfitWindow), + "accumulatedFee", + "winRatio", + "profitFactor", + fmt.Sprintf("%s%d Trades", r.Interval, r.Window), + } + + for i := 0; i < len(r.strategyParameters); i++ { + titles = append(titles, r.strategyParameters[i][0]) + } + + return titles +} + +// CsvRecords returns a data slice +func (r *AccumulatedProfitReport) CsvRecords() [][]string { + var data [][]string + + for i := 0; i <= r.Window-1; i++ { + values := []string{ + strconv.Itoa(i + 1), + r.symbol, + strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.profitMAPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedFeePerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.winRatioPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.profitFactorPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedTradesPerInterval.Last(i), 'f', 4, 64), + } + for j := 0; j < len(r.strategyParameters); j++ { + values = append(values, r.strategyParameters[j][1]) + } + + data = append(data, values) + } + + return data +} + // Output Accumulated profit report to a TSV file func (r *AccumulatedProfitReport) Output() { if r.TsvReportPath != "" { + // Open specified file for appending tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath) if err != nil { panic(err) } defer tsvwiter.Close() - // Output title row - titles := []string{ - "#", - "Symbol", - "Total Net Profit", - fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.ProfitMAWindow), - fmt.Sprintf("%s%d Net Profit", r.Interval, r.ShortTermProfitWindow), - "accumulatedFee", - "winRatio", - "profitFactor", - fmt.Sprintf("%s%d Trades", r.Interval, r.Window), - } - for i := 0; i < len(r.strategyParameters); i++ { - titles = append(titles, r.strategyParameters[i][0]) - } - _ = tsvwiter.Write(titles) - - // Output data row - for i := 0; i <= r.Window-1; i++ { - values := []string{ - strconv.Itoa(i + 1), - r.symbol, - strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.profitMAPerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow), 'f', 4, 64), - strconv.FormatFloat(r.accumulatedFeePerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.winRatioPerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.profitFactorPerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.accumulatedTradesPerInterval.Last(i), 'f', 4, 64), - } - for j := 0; j < len(r.strategyParameters); j++ { - values = append(values, r.strategyParameters[j][1]) - } - _ = tsvwiter.Write(values) - } + + // Column Title + _ = tsvwiter.Write(r.CsvHeader()) + + // Output data rows + _ = tsvwiter.WriteAll(r.CsvRecords()) } } From e161deba251319a87819a9baf6ea6dbf3d12578b Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 11 Jul 2023 11:13:13 +0800 Subject: [PATCH 1190/1392] improve/profitStatsTracker: use SMA v2 --- pkg/report/profit_report.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index a564b9c5df..1ae3b58f30 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -5,7 +5,7 @@ import ( "github.com/c9s/bbgo/pkg/data/tsv" "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/indicator" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/types" "strconv" ) @@ -27,10 +27,10 @@ type AccumulatedProfitReport struct { // Accumulated profit accumulatedProfit fixedpoint.Value - accumulatedProfitPerInterval floats.Slice + accumulatedProfitPerInterval *types.Float64Series // Accumulated profit MA - profitMA *indicator.SMA + profitMA *indicatorv2.SMAStream profitMAPerInterval floats.Slice // Profit of each interval @@ -71,7 +71,8 @@ func (r *AccumulatedProfitReport) Initialize(symbol string, interval types.Inter r.ShortTermProfitWindow = 7 } - r.profitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: r.Interval, Window: r.ProfitMAWindow}} + r.accumulatedProfitPerInterval = types.NewFloat64Series() + r.profitMA = indicatorv2.SMA2(r.accumulatedProfitPerInterval, r.ProfitMAWindow) } func (r *AccumulatedProfitReport) AddStrategyParameter(title string, value string) { @@ -86,13 +87,12 @@ func (r *AccumulatedProfitReport) AddTrade(trade types.Trade) { func (r *AccumulatedProfitReport) Rotate(ps *types.ProfitStats, ts *types.TradeStats) { // Accumulated profit r.accumulatedProfit = r.accumulatedProfit.Add(ps.AccumulatedNetProfit) - r.accumulatedProfitPerInterval.Update(r.accumulatedProfit.Float64()) + r.accumulatedProfitPerInterval.PushAndEmit(r.accumulatedProfit.Float64()) // Profit of each interval r.ProfitPerInterval.Update(ps.AccumulatedNetProfit.Float64()) // Profit MA - r.profitMA.Update(r.accumulatedProfit.Float64()) r.profitMAPerInterval.Update(r.profitMA.Last(0)) // Accumulated Fee From ce481ba52d26a5ffbef47c980256a4dd0ab0a6bd Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Jul 2023 10:13:13 +0800 Subject: [PATCH 1191/1392] rewrite cci indicator in v2 indicator --- pkg/indicator/cci.go | 10 +++++- pkg/indicator/cci_test.go | 9 ++--- pkg/indicator/v2/cci.go | 65 ++++++++++++++++++++++++++++++++++++ pkg/indicator/v2/cci_test.go | 39 ++++++++++++++++++++++ pkg/indicator/v2/const.go | 5 +++ pkg/indicator/v2/price.go | 4 +++ pkg/indicator/v2/stoch.go | 4 +-- pkg/types/kline.go | 9 +++-- pkg/types/series_float64.go | 4 +++ 9 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 pkg/indicator/v2/cci.go create mode 100644 pkg/indicator/v2/cci_test.go create mode 100644 pkg/indicator/v2/const.go diff --git a/pkg/indicator/cci.go b/pkg/indicator/cci.go index f11f337138..571d206ac1 100644 --- a/pkg/indicator/cci.go +++ b/pkg/indicator/cci.go @@ -11,6 +11,12 @@ import ( // Refer URL: http://www.andrewshamlet.net/2017/07/08/python-tutorial-cci // with modification of ddof=0 to let standard deviation to be divided by N instead of N-1 // +// CCI = (Typical Price - n-period SMA of TP) / (Constant x Mean Deviation) +// +// Typical Price (TP) = (High + Low + Close)/3 +// +// Constant = .015 +// // The Commodity Channel Index (CCI) is a technical analysis indicator that is used to identify potential overbought or oversold conditions // in a security's price. It was originally developed for use in commodity markets, but can be applied to any security that has a sufficient // amount of price data. The CCI is calculated by taking the difference between the security's typical price (the average of its high, low, and @@ -43,16 +49,18 @@ func (inc *CCI) Update(value float64) { } inc.Input.Push(value) - tp := inc.TypicalPrice.Last(0) - inc.Input.Index(inc.Window) + value + tp := inc.TypicalPrice.Last(0) - inc.Input.Last(inc.Window) + value inc.TypicalPrice.Push(tp) if len(inc.Input) < inc.Window { return } + ma := tp / float64(inc.Window) inc.MA.Push(ma) if len(inc.MA) > MaxNumOfEWMA { inc.MA = inc.MA[MaxNumOfEWMATruncateSize-1:] } + md := 0. for i := 0; i < inc.Window; i++ { diff := inc.Input.Last(i) - ma diff --git a/pkg/indicator/cci_test.go b/pkg/indicator/cci_test.go index 8d93947ef5..ae056e3130 100644 --- a/pkg/indicator/cci_test.go +++ b/pkg/indicator/cci_test.go @@ -4,8 +4,9 @@ import ( "encoding/json" "testing" - "github.com/c9s/bbgo/pkg/types" "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" ) /* @@ -19,7 +20,7 @@ print(cci) func Test_CCI(t *testing.T) { var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) var input []float64 - var Delta = 4.3e-2 + var delta = 4.3e-2 if err := json.Unmarshal(randomPrices, &input); err != nil { panic(err) } @@ -30,8 +31,8 @@ func Test_CCI(t *testing.T) { } last := cci.Last(0) - assert.InDelta(t, 93.250481, last, Delta) - assert.InDelta(t, 81.813449, cci.Index(1), Delta) + assert.InDelta(t, 93.250481, last, delta) + assert.InDelta(t, 81.813449, cci.Index(1), delta) assert.Equal(t, 50-16+1, cci.Length()) }) } diff --git a/pkg/indicator/v2/cci.go b/pkg/indicator/v2/cci.go new file mode 100644 index 0000000000..77ca9e0bf0 --- /dev/null +++ b/pkg/indicator/v2/cci.go @@ -0,0 +1,65 @@ +package indicatorv2 + +import ( + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Commodity Channel Index +// Refer URL: http://www.andrewshamlet.net/2017/07/08/python-tutorial-cci +// with modification of ddof=0 to let standard deviation to be divided by N instead of N-1 +// +// CCI = (Typical Price - n-period SMA of TP) / (Constant x Mean Deviation) +// +// Typical Price (TP) = (High + Low + Close)/3 +// +// Constant = .015 +// +// The Commodity Channel Index (CCI) is a technical analysis indicator that is used to identify potential overbought or oversold conditions +// in a security's price. It was originally developed for use in commodity markets, but can be applied to any security that has a sufficient +// amount of price data. The CCI is calculated by taking the difference between the security's typical price (the average of its high, low, and +// closing prices) and its moving average, and then dividing the result by the mean absolute deviation of the typical price. This resulting value +// is then plotted as a line on the price chart, with values above +100 indicating overbought conditions and values below -100 indicating +// oversold conditions. The CCI can be used by traders to identify potential entry and exit points for trades, or to confirm other technical +// analysis signals. + +type CCIStream struct { + *types.Float64Series + + TypicalPrice *types.Float64Series + + source types.Float64Source + window int +} + +func CCI(source types.Float64Source, window int) *CCIStream { + s := &CCIStream{ + Float64Series: types.NewFloat64Series(), + TypicalPrice: types.NewFloat64Series(), + source: source, + window: window, + } + s.Bind(source, s) + return s +} + +func (s *CCIStream) Calculate(value float64) float64 { + var tp = value + if s.TypicalPrice.Length() > 0 { + tp = s.TypicalPrice.Last(0) - s.source.Last(s.window) + value + } + + s.TypicalPrice.Push(tp) + + ma := tp / float64(s.window) + md := 0. + for i := 0; i < s.window; i++ { + diff := s.source.Last(i) - ma + md += diff * diff + } + + md = math.Sqrt(md / float64(s.window)) + cci := (value - ma) / (0.015 * md) + return cci +} diff --git a/pkg/indicator/v2/cci_test.go b/pkg/indicator/v2/cci_test.go new file mode 100644 index 0000000000..a662553ba2 --- /dev/null +++ b/pkg/indicator/v2/cci_test.go @@ -0,0 +1,39 @@ +package indicatorv2 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +/* +python: + +import pandas as pd +s = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]) +cci = pd.Series((s - s.rolling(16).mean()) / (0.015 * s.rolling(16).std(ddof=0)), name="CCI") +print(cci) +*/ +func Test_CCI(t *testing.T) { + var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []float64 + var delta = 4.3e-2 + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + t.Run("random_case", func(t *testing.T) { + price := Price(nil, nil) + cci := CCI(price, 16) + for _, value := range input { + price.PushAndEmit(value) + } + + t.Logf("cci: %+v", cci.Slice) + + last := cci.Last(0) + assert.InDelta(t, 93.250481, last, delta) + assert.InDelta(t, 81.813449, cci.Index(1), delta) + assert.Equal(t, 50, cci.Length(), "length") + }) +} diff --git a/pkg/indicator/v2/const.go b/pkg/indicator/v2/const.go new file mode 100644 index 0000000000..7ae2f29140 --- /dev/null +++ b/pkg/indicator/v2/const.go @@ -0,0 +1,5 @@ +package indicatorv2 + +import "github.com/c9s/bbgo/pkg/fixedpoint" + +var three = fixedpoint.NewFromInt(3) diff --git a/pkg/indicator/v2/price.go b/pkg/indicator/v2/price.go index 7f261a14ed..6f48439e04 100644 --- a/pkg/indicator/v2/price.go +++ b/pkg/indicator/v2/price.go @@ -65,3 +65,7 @@ func OpenPrices(source KLineSubscription) *PriceStream { func Volumes(source KLineSubscription) *PriceStream { return Price(source, types.KLineVolumeMapper) } + +func HLC3(source KLineSubscription) *PriceStream { + return Price(source, types.KLineHLC3Mapper) +} diff --git a/pkg/indicator/v2/stoch.go b/pkg/indicator/v2/stoch.go index 808169e431..fe0d300867 100644 --- a/pkg/indicator/v2/stoch.go +++ b/pkg/indicator/v2/stoch.go @@ -47,8 +47,8 @@ func Stoch(source KLineSubscription, window, dPeriod int) *StochStream { lowest := s.lowPrices.Slice.Tail(s.window).Min() highest := s.highPrices.Slice.Tail(s.window).Max() - var k float64 = 50.0 - var d float64 = 0.0 + var k = 50.0 + var d = 0.0 if highest != lowest { k = 100.0 * (kLine.Close.Float64() - lowest) / (highest - lowest) diff --git a/pkg/types/kline.go b/pkg/types/kline.go index e405ba94bd..6bf863c30b 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -10,14 +10,15 @@ import ( "github.com/c9s/bbgo/pkg/style" ) +var Two = fixedpoint.NewFromInt(2) +var Three = fixedpoint.NewFromInt(3) + type Direction int const DirectionUp = 1 const DirectionNone = 0 const DirectionDown = -1 -var Two = fixedpoint.NewFromInt(2) - type KLineOrWindow interface { GetInterval() string Direction() Direction @@ -668,6 +669,10 @@ func KLineVolumeMapper(k KLine) float64 { return k.Volume.Float64() } +func KLineHLC3Mapper(k KLine) float64 { + return k.High.Add(k.Low).Add(k.Close).Div(Three).Float64() +} + func MapKLinePrice(kLines []KLine, f KLineValueMapper) (prices []float64) { for _, k := range kLines { prices = append(prices, f(k)) diff --git a/pkg/types/series_float64.go b/pkg/types/series_float64.go index 6dd6704aa8..f12e05d584 100644 --- a/pkg/types/series_float64.go +++ b/pkg/types/series_float64.go @@ -29,6 +29,10 @@ func (f *Float64Series) Length() int { return len(f.Slice) } +func (f *Float64Series) Push(x float64) { + f.Slice.Push(x) +} + func (f *Float64Series) PushAndEmit(x float64) { f.Slice.Push(x) f.EmitUpdate(x) From b1c1caa6afbc8293d90df04754d0a878765a0500 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 11 Jul 2023 10:23:54 +0800 Subject: [PATCH 1192/1392] tri: load test data from static file --- pkg/strategy/tri/strategy_test.go | 8 ++------ pkg/util/json.go | 23 +++++++++++++++++++++++ testdata/binance-markets.json | 16 ++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/tri/strategy_test.go b/pkg/strategy/tri/strategy_test.go index cf6e6d44b5..b15d6638f1 100644 --- a/pkg/strategy/tri/strategy_test.go +++ b/pkg/strategy/tri/strategy_test.go @@ -3,24 +3,20 @@ package tri import ( - "context" "fmt" "testing" "github.com/stretchr/testify/assert" - "github.com/c9s/bbgo/pkg/cache" - "github.com/c9s/bbgo/pkg/exchange/binance" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" ) var markets = make(types.MarketMap) func init() { - var err error - markets, err = cache.LoadExchangeMarketsWithCache(context.Background(), &binance.Exchange{}) - if err != nil { + if err := util.ReadJsonFile("../../../testdata/binance-markets.json", &markets); err != nil { panic(err) } } diff --git a/pkg/util/json.go b/pkg/util/json.go index 1e7740967c..b11dd89b62 100644 --- a/pkg/util/json.go +++ b/pkg/util/json.go @@ -2,7 +2,9 @@ package util import ( "encoding/json" + "io" "io/ioutil" + "os" ) func WriteJsonFile(p string, obj interface{}) error { @@ -13,3 +15,24 @@ func WriteJsonFile(p string, obj interface{}) error { return ioutil.WriteFile(p, out, 0644) } + +func ReadJsonFile(file string, obj interface{}) error { + f, err := os.Open(file) + if err != nil { + return err + } + + defer f.Close() + + byteResult, err := io.ReadAll(f) + if err != nil { + return err + } + + err = json.Unmarshal([]byte(byteResult), obj) + if err != nil { + return err + } + + return nil +} diff --git a/testdata/binance-markets.json b/testdata/binance-markets.json index 90b58b53aa..d8059ed0a5 100644 --- a/testdata/binance-markets.json +++ b/testdata/binance-markets.json @@ -62,5 +62,21 @@ "minPrice": 0.01, "maxPrice": 100000, "tickSize": 0.01 + }, + "ETHBTC": { + "symbol": "ETHBTC", + "localSymbol": "ETHBTC", + "pricePrecision": 8, + "volumePrecision": 8, + "quoteCurrency": "BTC", + "baseCurrency": "ETH", + "minNotional": 0.0001, + "minAmount": 0.0001, + "minQuantity": 0.0001, + "maxQuantity": 100000, + "stepSize": 0.0001, + "minPrice": 1e-05, + "maxPrice": 922327, + "tickSize": 1e-05 } } From 7781d5c70f566a02387251869d3462254093080b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Jul 2023 15:01:15 +0800 Subject: [PATCH 1193/1392] autoborrow: few improvements: - return debt once and update the account - add alert slack mentions --- pkg/strategy/autoborrow/strategy.go | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 59e352122c..bea62b4c78 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -3,6 +3,7 @@ package autoborrow import ( "context" "fmt" + "strings" "time" "github.com/sirupsen/logrus" @@ -56,6 +57,8 @@ type Strategy struct { MaxMarginLevel fixedpoint.Value `json:"maxMarginLevel"` AutoRepayWhenDeposit bool `json:"autoRepayWhenDeposit"` + MarginLevelAlertSlackMentions []string `json:"marginLevelAlertSlackMentions"` + Assets []MarginAsset `json:"assets"` ExchangeSession *bbgo.ExchangeSession @@ -94,6 +97,10 @@ func (s *Strategy) tryToRepayAnyDebt(ctx context.Context) { } toRepay := fixedpoint.Min(b.Available, b.Debt()) + if toRepay.IsZero() { + continue + } + bbgo.Notify(&MarginAction{ Exchange: s.ExchangeSession.ExchangeName, Action: "Repay", @@ -107,6 +114,8 @@ func (s *Strategy) tryToRepayAnyDebt(ctx context.Context) { if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), b.Currency, toRepay); err != nil { log.WithError(err).Errorf("margin repay error") } + + return } } @@ -204,16 +213,24 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { ) // if margin ratio is too low, do not borrow - if curMarginLevel.Compare(minMarginLevel) < 0 { - log.Infof("current margin level %f < min margin level %f, skip autoborrow", curMarginLevel.Float64(), minMarginLevel.Float64()) - bbgo.Notify("Warning!!! %s Current Margin Level %f < Minimal Margin Level %f", + for maxTries := 5; account.MarginLevel.Compare(minMarginLevel) < 0 && maxTries > 0; maxTries-- { + log.Infof("current margin level %f < min margin level %f, skip autoborrow", account.MarginLevel.Float64(), minMarginLevel.Float64()) + + bbgo.Notify("Warning!!! %s Current Margin Level %f < Minimal Margin Level %f "+strings.Join(s.MarginLevelAlertSlackMentions, " "), s.ExchangeSession.Name, - curMarginLevel.Float64(), + account.MarginLevel.Float64(), minMarginLevel.Float64(), account.Balances().Debts(), ) + s.tryToRepayAnyDebt(ctx) - return + + // update account info after the repay + account, err = s.ExchangeSession.UpdateAccount(ctx) + if err != nil { + log.WithError(err).Errorf("can not update account") + return + } } balances := account.Balances() @@ -282,7 +299,7 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { Action: "Borrow", Asset: marginAsset.Asset, Amount: toBorrow, - MarginLevel: curMarginLevel, + MarginLevel: account.MarginLevel, MinMarginLevel: minMarginLevel, }) log.Infof("sending borrow request %f %s", toBorrow.Float64(), marginAsset.Asset) From d6ade1f2fd28468ee2c781c19b0465e2fb141c52 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Jul 2023 15:07:51 +0800 Subject: [PATCH 1194/1392] autoborrow: use context timeout handling --- pkg/strategy/autoborrow/strategy.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index bea62b4c78..a648cc2b58 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -225,6 +225,12 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { s.tryToRepayAnyDebt(ctx) + select { + case <-ctx.Done(): + return + case <-time.After(time.Second * 5): + } + // update account info after the repay account, err = s.ExchangeSession.UpdateAccount(ctx) if err != nil { From baf431d7b628e69e5d8cc8356a208119e51873ea Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Jul 2023 16:17:22 +0800 Subject: [PATCH 1195/1392] riskcontrol: log on release position order --- pkg/risk/riskcontrol/position.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go index 6a6eebb467..2714238582 100644 --- a/pkg/risk/riskcontrol/position.go +++ b/pkg/risk/riskcontrol/position.go @@ -36,14 +36,16 @@ func NewPositionRiskControl(orderExecutor bbgo.OrderExecutorExtended, hardLimit, control.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) { pos := orderExecutor.Position() - createdOrders, err := orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{ + submitOrder := types.SubmitOrder{ Symbol: pos.Symbol, Market: pos.Market, Side: side, Type: types.OrderTypeMarket, Quantity: quantity, - }) + } + log.Infof("submitting order: %+v", submitOrder) + createdOrders, err := orderExecutor.SubmitOrders(context.Background(), submitOrder) if err != nil { log.WithError(err).Errorf("failed to submit orders") return From 885c58f77e11d58661f35389c5acdef251b87375 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Jul 2023 16:47:51 +0800 Subject: [PATCH 1196/1392] core/tradecollector: reduce critical section --- pkg/core/tradecollector.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go index e981fff56c..29eb60f4ce 100644 --- a/pkg/core/tradecollector.go +++ b/pkg/core/tradecollector.go @@ -123,32 +123,42 @@ func (c *TradeCollector) Process() bool { key := trade.Key() c.mu.Lock() - defer c.mu.Unlock() // if it's already done, remove the trade from the trade store if _, done := c.doneTrades[key]; done { + c.mu.Unlock() return true } - if c.orderStore.Exists(trade.OrderID) { - if c.position != nil { + if c.position != nil { + if c.orderStore.Exists(trade.OrderID) { + var p types.Profit profit, netProfit, madeProfit := c.position.AddTrade(trade) if madeProfit { - p := c.position.NewProfit(trade, profit, netProfit) - c.EmitTrade(trade, profit, netProfit) + p = c.position.NewProfit(trade, profit, netProfit) + } + + c.doneTrades[key] = struct{}{} + c.mu.Unlock() + + c.EmitTrade(trade, profit, netProfit) + if !p.Profit.IsZero() { c.EmitProfit(trade, &p) - } else { - c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) - c.EmitProfit(trade, nil) } + positionChanged = true - } else { - c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + return true } - c.doneTrades[key] = struct{}{} - return true + } else { + if c.orderStore.Exists(trade.OrderID) { + c.doneTrades[key] = struct{}{} + c.mu.Unlock() + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + return true + } } + return false }) From b9616a08056ee22cac2ae4d73ab263fc6691884a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Jul 2023 17:16:46 +0800 Subject: [PATCH 1197/1392] add TradeCollector.Process() log message --- pkg/bbgo/order_executor_general.go | 3 ++- pkg/bbgo/session.go | 6 ++++-- pkg/cmd/backtest.go | 3 ++- pkg/core/tradecollector.go | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index b3d3bdf13a..6c95b64954 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -203,9 +203,10 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. orderCreateCallback := func(createdOrder types.Order) { e.orderStore.Add(createdOrder) e.activeMakerOrders.Add(createdOrder) - e.tradeCollector.Process() } + defer e.tradeCollector.Process() + if e.maxRetries == 0 { createdOrders, _, err := BatchPlaceOrder(ctx, e.session.Exchange, orderCreateCallback, formattedOrders...) return createdOrders, err diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 0b9961ec61..6a361a9d51 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -385,9 +385,11 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ session.Trades[symbol] = &types.TradeSlice{Trades: trades} session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { - if trade.Symbol == symbol { - session.Trades[symbol].Append(trade) + if trade.Symbol != symbol { + return } + + session.Trades[symbol].Append(trade) }) position := &types.Position{ diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 025bec0209..6dbfb8d1e0 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -529,7 +529,8 @@ var BacktestCmd = &cobra.Command{ profitFactor := tradeState.ProfitFactor winningRatio := tradeState.WinningRatio intervalProfits := tradeState.IntervalProfits[types.Interval1d] - symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Trades, intervalProfits, profitFactor, winningRatio) + + symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), intervalProfits, profitFactor, winningRatio) if err != nil { return err } diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go index 29eb60f4ce..4bf1ddaa69 100644 --- a/pkg/core/tradecollector.go +++ b/pkg/core/tradecollector.go @@ -117,6 +117,7 @@ func (c *TradeCollector) setDone(key types.TradeKey) { // if we have the order in the order store, then the trade will be considered for the position. // profit will also be calculated. func (c *TradeCollector) Process() bool { + logrus.Debugf("TradeCollector.Process()") positionChanged := false c.tradeStore.Filter(func(trade types.Trade) bool { From 11f94575049a46d0d43b3e80d053a0552352bd3d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Jul 2023 17:44:00 +0800 Subject: [PATCH 1198/1392] readme: add the built-in strategy table --- README.md | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d1b2163726..847a44359a 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,38 @@ the implementation. ![bbgo backtest report](assets/screenshots/backtest-report.jpg) +## Built-in Strategies + +| strategy | description | type | backtest support | +|-------------|-----------------------------------------------------------------------------------------------------------------------------------------|------------|------------------| +| grid | the first generation grid strategy, it provides more flexibility, but you need to prepare inventories | maker | | +| grid2 | the second generation grid strategy, it can convert your quote asset into a grid, supports base+quote mode | maker | | +| bollgrid | strategy implements a basic grid strategy with the built-in bollinger indicator | maker | | +| xmaker | cross exchange market making strategy, it hedges your inventory risk on the other side | maker | no | +| xnav | this strategy helps you record the current net asset value | | | +| xalign | this strategy aligns your balance position automatically | | | +| xfunding | a funding rate fee strategy | | | +| autoborrow | this strategy uses margin to borrow assets, to help you keep the minimal balance | tool | no | +| pivotshort | this strategy finds the pivot low and entry the trade when the price breaks the previous low | long/short | | +| schedule | this strategy buy/sell with a fixed quantity periodically, you can use this as a single DCA, or to refill the fee asset like BNB. | tool | +| irr | this strategy opens the position based on the predicated return rate | long/short | | +| bollmaker | this strategy holds a long-term long/short position, places maker orders on both side, uses bollinger band to control the position size | maker | | +| wall | this strategy creates wall (large amount order) on the order book | | | +| scmaker | this market making strategy is desgiend for stable coin markets, like USDC/USDT | | | +| drift | | long/short | | +| rsicross | this strategy opens a long position when the fast rsi cross over the slow rsi, this is a demo strategy for using the v2 indicator | long/short | | +| marketcap | this strategy implements a strategy that rebalances the portfolio based on the market capitalization | rebalance | no | +| supertrend | this strategy uses DEMA and Supertrend indicator to open the long/short position | long/short | | +| trendtrader | this strategy opens long/short position based on the trendline breakout | | | +| elliottwave | | | | +| ewoDgtrd | | | | +| fixedmaker | | | | +| factoryzoo | | | | +| fmaker | | | | +| linregmaker | a linear regression based market maker | | | + + + ## Supported Exchanges - Binance Spot Exchange (and binance.us) @@ -234,7 +266,8 @@ bbgo pnl --exchange binance --asset BTC --since "2019-01-01" ### Synchronize System Time With Binance -BBGO provides the script for UNIX systems/subsystems to synchronize date with Binance. `jq` and `bc` are required to be installed in previous. +BBGO provides the script for UNIX systems/subsystems to synchronize date with Binance. `jq` and `bc` are required to be +installed in previous. To install the dependencies in Ubuntu, try the following commands: ```bash @@ -346,8 +379,6 @@ Check out the strategy directory [strategy](pkg/strategy) for all built-in strat - `pricealert` strategy demonstrates how to use the notification system [pricealert](pkg/strategy/pricealert). See [document](./doc/strategy/pricealert.md). -- `xpuremaker` strategy demonstrates how to maintain the orderbook and submit maker - orders [xpuremaker](pkg/strategy/xpuremaker) - `buyandhold` strategy demonstrates how to subscribe kline events and submit market order [buyandhold](pkg/strategy/pricedrop) - `bollgrid` strategy implements a basic grid strategy with the built-in bollinger @@ -360,8 +391,8 @@ Check out the strategy directory [strategy](pkg/strategy) for all built-in strat - `support` strategy uses K-lines with high volume as support [support](pkg/strategy/support). See [document](./doc/strategy/support.md). - `flashcrash` strategy implements a strategy that catches the flashcrash [flashcrash](pkg/strategy/flashcrash) -- `marketcap` strategy implements a strategy that rebalances the portfolio based on the - market capitalization [marketcap](pkg/strategy/marketcap). See [document](./doc/strategy/marketcap.md). +- `marketcap` strategy implements a strategy that rebalances the portfolio based on the market + capitalization [marketcap](pkg/strategy/marketcap). See [document](./doc/strategy/marketcap.md). - `pivotshort` - shorting focused strategy. - `irr` - return rate strategy. - `drift` - drift strategy. From c2568ab22cb1357fb15c287fc0359c8f55592412 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 12 Jul 2023 17:44:29 +0800 Subject: [PATCH 1199/1392] doc: add bitget to the exchange list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 847a44359a..97afa55a6e 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ the implementation. - OKEx Spot Exchange - Kucoin Spot Exchange - MAX Spot Exchange (located in Taiwan) +- Bitget (In Progress) ## Documentation and General Topics From a9d0242a9d2a9b1c400876c3c39914e44b826744 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 14 Jul 2023 13:19:46 +0800 Subject: [PATCH 1200/1392] strategy/autoborrow: add margin level alert --- pkg/notifier/slacknotifier/slack.go | 9 +-- pkg/strategy/autoborrow/strategy.go | 95 +++++++++++++++++++++++++---- pkg/types/duration.go | 1 + pkg/types/slack.go | 7 +++ 4 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 pkg/types/slack.go diff --git a/pkg/notifier/slacknotifier/slack.go b/pkg/notifier/slacknotifier/slack.go index 4b775532f6..f1aee5840d 100644 --- a/pkg/notifier/slacknotifier/slack.go +++ b/pkg/notifier/slacknotifier/slack.go @@ -21,10 +21,6 @@ type notifyTask struct { Opts []slack.MsgOption } -type slackAttachmentCreator interface { - SlackAttachment() slack.Attachment -} - type Notifier struct { client *slack.Client channel string @@ -59,6 +55,7 @@ func (n *Notifier) worker() { case task := <-n.taskC: limiter.Wait(ctx) + _, _, err := n.client.PostMessageContext(ctx, task.Channel, task.Opts...) if err != nil { log.WithError(err). @@ -86,7 +83,7 @@ func filterSlackAttachments(args []interface{}) (slackAttachments []slack.Attach slackAttachments = append(slackAttachments, a) - case slackAttachmentCreator: + case types.SlackAttachmentCreator: if firstAttachmentOffset == -1 { firstAttachmentOffset = idx } @@ -132,7 +129,7 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{} case slack.Attachment: opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a}, slackAttachments...)...)) - case slackAttachmentCreator: + case types.SlackAttachmentCreator: // convert object to slack attachment (if supported) opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a.SlackAttachment()}, slackAttachments...)...)) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index a648cc2b58..fd53a80b33 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -23,24 +23,60 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } -/** -- on: binance - autoborrow: +/* + - on: binance + autoborrow: interval: 30m repayWhenDeposit: true # minMarginLevel for triggering auto borrow minMarginLevel: 1.5 assets: - - asset: ETH - low: 3.0 - maxQuantityPerBorrow: 1.0 - maxTotalBorrow: 10.0 - - asset: USDT - low: 1000.0 - maxQuantityPerBorrow: 100.0 - maxTotalBorrow: 10.0 + + - asset: ETH + low: 3.0 + maxQuantityPerBorrow: 1.0 + maxTotalBorrow: 10.0 + + - asset: USDT + low: 1000.0 + maxQuantityPerBorrow: 100.0 + maxTotalBorrow: 10.0 */ +type MarginAlert struct { + CurrentMarginLevel fixedpoint.Value + MinimalMarginLevel fixedpoint.Value + SlackMentions []string + SessionName string +} + +func (m *MarginAlert) SlackAttachment() slack.Attachment { + return slack.Attachment{ + Color: "red", + Title: fmt.Sprintf("Margin Level Alert: %s session - current margin level %f < required margin level %f", + m.SessionName, m.CurrentMarginLevel.Float64(), m.MinimalMarginLevel.Float64()), + Text: strings.Join(m.SlackMentions, " "), + Fields: []slack.AttachmentField{ + { + Title: "Session", + Value: m.SessionName, + Short: true, + }, + { + Title: "Current Margin Level", + Value: m.CurrentMarginLevel.String(), + Short: true, + }, + { + Title: "Minimal Margin Level", + Value: m.MinimalMarginLevel.String(), + Short: true, + }, + }, + // Footer: "", + // FooterIcon: "", + } +} type MarginAsset struct { Asset string `json:"asset"` @@ -57,7 +93,9 @@ type Strategy struct { MaxMarginLevel fixedpoint.Value `json:"maxMarginLevel"` AutoRepayWhenDeposit bool `json:"autoRepayWhenDeposit"` - MarginLevelAlertSlackMentions []string `json:"marginLevelAlertSlackMentions"` + MarginLevelAlertInterval types.Duration `json:"marginLevelAlertInterval"` + MarginLevelAlertMinMargin fixedpoint.Value `json:"marginLevelAlertMinMargin"` + MarginLevelAlertSlackMentions []string `json:"marginLevelAlertSlackMentions"` Assets []MarginAsset `json:"assets"` @@ -510,6 +548,39 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } + if !s.MarginLevelAlertMinMargin.IsZero() { + alertInterval := time.Minute * 5 + if s.MarginLevelAlertInterval > 0 { + alertInterval = s.MarginLevelAlertInterval.Duration() + } + + go s.marginAlertWorker(ctx, alertInterval) + } + go s.run(ctx, s.Interval.Duration()) return nil } + +func (s *Strategy) marginAlertWorker(ctx context.Context, alertInterval time.Duration) { + go func() { + ticker := time.NewTicker(alertInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + account := s.ExchangeSession.GetAccount() + if account.MarginLevel.Compare(s.MarginLevelAlertMinMargin) <= 0 { + bbgo.Notify(&MarginAlert{ + CurrentMarginLevel: account.MarginLevel, + MinimalMarginLevel: s.MarginLevelAlertMinMargin, + SlackMentions: s.MarginLevelAlertSlackMentions, + SessionName: s.ExchangeSession.Name, + }) + bbgo.Notify(account.Balances().Debts()) + } + } + } + }() +} diff --git a/pkg/types/duration.go b/pkg/types/duration.go index be3a35a965..c376f983fc 100644 --- a/pkg/types/duration.go +++ b/pkg/types/duration.go @@ -89,6 +89,7 @@ func ParseSimpleDuration(s string) (*SimpleDuration, error) { return nil, errors.Wrapf(ErrNotSimpleDuration, "input %q is not a simple duration", s) } +// Duration type Duration time.Duration func (d *Duration) Duration() time.Duration { diff --git a/pkg/types/slack.go b/pkg/types/slack.go new file mode 100644 index 0000000000..322220ada3 --- /dev/null +++ b/pkg/types/slack.go @@ -0,0 +1,7 @@ +package types + +import "github.com/slack-go/slack" + +type SlackAttachmentCreator interface { + SlackAttachment() slack.Attachment +} From c7ee7a94969963de67dddba283de472c5f8ca0b5 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 14 Jul 2023 13:21:40 +0800 Subject: [PATCH 1201/1392] add marginLevelAlertInterval to the config example --- config/autoborrow.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/config/autoborrow.yaml b/config/autoborrow.yaml index 6b94802f2c..d86020dd17 100644 --- a/config/autoborrow.yaml +++ b/config/autoborrow.yaml @@ -7,18 +7,26 @@ exchangeStrategies: # minMarginRatio for triggering auto borrow # we trigger auto borrow only when the margin ratio is above the number - minMarginLevel: 1.5 + minMarginLevel: 3.0 # maxMarginRatio for stop auto-repay # if the margin ratio is high enough, we don't have the urge to repay - maxMarginLevel: 10.0 + maxMarginLevel: 20.0 + + marginLevelAlertInterval: 5m + marginLevelAlertMinMargin: 2.0 + marginLevelAlertSlackMentions: + - '<@USER_ID>' + - '' assets: - asset: ETH low: 3.0 maxQuantityPerBorrow: 1.0 maxTotalBorrow: 10.0 + - asset: USDT low: 1000.0 maxQuantityPerBorrow: 100.0 maxTotalBorrow: 10.0 + From f8051b3f2b415b5dfecdd1d0d2d747bb77b9ecf8 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 14 Jul 2023 13:22:42 +0800 Subject: [PATCH 1202/1392] autoborrow: fix margin warning format --- config/autoborrow.yaml | 2 +- pkg/strategy/autoborrow/strategy.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/autoborrow.yaml b/config/autoborrow.yaml index d86020dd17..b651ae8b05 100644 --- a/config/autoborrow.yaml +++ b/config/autoborrow.yaml @@ -29,4 +29,4 @@ exchangeStrategies: low: 1000.0 maxQuantityPerBorrow: 100.0 maxTotalBorrow: 10.0 - + diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index fd53a80b33..836b65653b 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -254,7 +254,7 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { for maxTries := 5; account.MarginLevel.Compare(minMarginLevel) < 0 && maxTries > 0; maxTries-- { log.Infof("current margin level %f < min margin level %f, skip autoborrow", account.MarginLevel.Float64(), minMarginLevel.Float64()) - bbgo.Notify("Warning!!! %s Current Margin Level %f < Minimal Margin Level %f "+strings.Join(s.MarginLevelAlertSlackMentions, " "), + bbgo.Notify("Warning!!! %s Current Margin Level %f < Minimal Margin Level %f", s.ExchangeSession.Name, account.MarginLevel.Float64(), minMarginLevel.Float64(), From bc4eae5e3978c238333526bcd1cc93ea6fbf77cd Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 17 Jul 2023 11:19:10 +0800 Subject: [PATCH 1203/1392] improve/supertrend: Switch of outputting patameters in profit report --- config/supertrend.yaml | 1 + pkg/strategy/supertrend/strategy.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 99815245bd..fe427a9b6e 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -120,3 +120,4 @@ exchangeStrategies: profitMAWindow: 60 shortTermProfitWindow: 14 tsvReportPath: res.tsv + trackParameters: false diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index a0942b0646..2cb789c52a 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -39,7 +39,9 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + // ProfitStatsTracker tracks profit related status and generates report ProfitStatsTracker *report.ProfitStatsTracker `json:"profitStatsTracker"` + TrackParameters bool `json:"trackParameters"` // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -362,7 +364,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } // Add strategy parameters to report - if s.ProfitStatsTracker.AccumulatedProfitReport != nil { + if s.TrackParameters && s.ProfitStatsTracker.AccumulatedProfitReport != nil { s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) From e5254e6446fffa13e077bca467a3ec8c59f5aa67 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 17 Jul 2023 11:45:37 +0800 Subject: [PATCH 1204/1392] improve/linregmaker: add profit report --- config/linregmaker.yaml | 9 +++++++ pkg/strategy/linregmaker/strategy.go | 40 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/config/linregmaker.yaml b/config/linregmaker.yaml index 49e765e2ec..644e213ecd 100644 --- a/config/linregmaker.yaml +++ b/config/linregmaker.yaml @@ -175,3 +175,12 @@ exchangeStrategies: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) - roiStopLoss: percentage: 30% + + profitStatsTracker: + interval: 1d + window: 30 + accumulatedProfitReport: + profitMAWindow: 60 + shortTermProfitWindow: 14 + tsvReportPath: res.tsv + trackParameters: false diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index fcc4b4f713..e7e2ce029d 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -3,6 +3,8 @@ package linregmaker import ( "context" "fmt" + "github.com/c9s/bbgo/pkg/report" + "os" "sync" "github.com/c9s/bbgo/pkg/risk/dynamicrisk" @@ -135,6 +137,10 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + // ProfitStatsTracker tracks profit related status and generates report + ProfitStatsTracker *report.ProfitStatsTracker `json:"profitStatsTracker"` + TrackParameters bool `json:"trackParameters"` + Environment *bbgo.Environment StandardIndicatorSet *bbgo.StandardIndicatorSet Market types.Market @@ -242,6 +248,11 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { if len(s.DynamicQuantityDecrease) > 0 { s.DynamicQuantityDecrease.Initialize(s.Symbol, session) } + + // Profit tracker + if s.ProfitStatsTracker != nil { + s.ProfitStatsTracker.Subscribe(session, s.Symbol) + } } func (s *Strategy) CurrentPosition() *types.Position { @@ -664,6 +675,27 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.ExitMethods.Bind(session, s.orderExecutor) + // Setup profit tracker + if s.ProfitStatsTracker != nil { + if s.ProfitStatsTracker.CurrentProfitStats == nil { + s.ProfitStatsTracker.InitLegacy(s.Market, &s.ProfitStats, s.TradeStats) + } + + // Add strategy parameters to report + if s.TrackParameters && s.ProfitStatsTracker.AccumulatedProfitReport != nil { + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("ReverseEMAWindow", fmt.Sprintf("%d", s.ReverseEMA.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FastLinRegInterval", fmt.Sprintf("%s", s.FastLinReg.Interval)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("SlowLinRegWindow", fmt.Sprintf("%d", s.SlowLinReg.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("SlowLinRegInterval", fmt.Sprintf("%s", s.SlowLinReg.Interval)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FasterDecreaseRatio", fmt.Sprintf("%f", s.FasterDecreaseRatio)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("NeutralBollingerWindow", fmt.Sprintf("%d", s.NeutralBollinger.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("NeutralBollingerBandWidth", fmt.Sprintf("%f", s.NeutralBollinger.BandWidth)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("Spread", fmt.Sprintf("%f", s.Spread)) + } + + s.ProfitStatsTracker.Bind(s.session, s.orderExecutor.TradeCollector()) + } + // Indicators initialized by StandardIndicatorSet must be initialized in Run() // Initialize ReverseEMA s.ReverseEMA = s.StandardIndicatorSet.EWMA(s.ReverseEMA.IntervalWindow) @@ -812,7 +844,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() + // Output profit report + if s.ProfitStatsTracker != nil { + if s.ProfitStatsTracker.AccumulatedProfitReport != nil { + s.ProfitStatsTracker.AccumulatedProfitReport.Output() + } + } + _ = s.orderExecutor.GracefulCancel(ctx) + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) }) return nil From 08d8519e67e6f4180056a3f2d340d0f6187c3959 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 17 Jul 2023 12:10:48 +0800 Subject: [PATCH 1205/1392] improve/profitStatsTracker: use SMA instead of SMA2 --- pkg/report/profit_report.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index 1ae3b58f30..8861117498 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -72,7 +72,7 @@ func (r *AccumulatedProfitReport) Initialize(symbol string, interval types.Inter } r.accumulatedProfitPerInterval = types.NewFloat64Series() - r.profitMA = indicatorv2.SMA2(r.accumulatedProfitPerInterval, r.ProfitMAWindow) + r.profitMA = indicatorv2.SMA(r.accumulatedProfitPerInterval, r.ProfitMAWindow) } func (r *AccumulatedProfitReport) AddStrategyParameter(title string, value string) { From 192d958adc76f93d9309494748dfe27ec6a9977d Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Mon, 17 Jul 2023 12:22:09 +0800 Subject: [PATCH 1206/1392] improve/linregmaker: use strconv --- pkg/strategy/linregmaker/strategy.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index e7e2ce029d..2ebbc17564 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/c9s/bbgo/pkg/report" "os" + "strconv" "sync" "github.com/c9s/bbgo/pkg/risk/dynamicrisk" @@ -683,14 +684,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Add strategy parameters to report if s.TrackParameters && s.ProfitStatsTracker.AccumulatedProfitReport != nil { - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("ReverseEMAWindow", fmt.Sprintf("%d", s.ReverseEMA.Window)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FastLinRegInterval", fmt.Sprintf("%s", s.FastLinReg.Interval)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("SlowLinRegWindow", fmt.Sprintf("%d", s.SlowLinReg.Window)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("SlowLinRegInterval", fmt.Sprintf("%s", s.SlowLinReg.Interval)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FasterDecreaseRatio", fmt.Sprintf("%f", s.FasterDecreaseRatio)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("NeutralBollingerWindow", fmt.Sprintf("%d", s.NeutralBollinger.Window)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("NeutralBollingerBandWidth", fmt.Sprintf("%f", s.NeutralBollinger.BandWidth)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("Spread", fmt.Sprintf("%f", s.Spread)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("ReverseEMAWindow", strconv.Itoa(s.ReverseEMA.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FastLinRegInterval", s.FastLinReg.Interval.String()) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("SlowLinRegWindow", strconv.Itoa(s.SlowLinReg.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("SlowLinRegInterval", s.SlowLinReg.Interval.String()) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FasterDecreaseRatio", s.FasterDecreaseRatio.String()) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("NeutralBollingerWindow", strconv.Itoa(s.NeutralBollinger.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("NeutralBollingerBandWidth", strconv.FormatFloat(s.NeutralBollinger.BandWidth, 'f', 4, 64)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("Spread", s.Spread.Percentage()) } s.ProfitStatsTracker.Bind(s.session, s.orderExecutor.TradeCollector()) From 844bd8be87e3059aa122f0d068bf135f404d8b2a Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 17 Jul 2023 16:38:42 +0800 Subject: [PATCH 1207/1392] bitget: add account transfers request --- .../binanceapi/cancel_replace_request.go | 3 +- pkg/exchange/bitget/bitgetapi/client_test.go | 10 + .../get_account_transfers_request.go | 44 ++++ ...et_account_transfers_request_requestgen.go | 193 ++++++++++++++++++ .../get_order_detail_request_requestgen.go | 8 +- 5 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 pkg/exchange/bitget/bitgetapi/get_account_transfers_request.go create mode 100644 pkg/exchange/bitget/bitgetapi/get_account_transfers_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/cancel_replace_request.go b/pkg/exchange/binance/binanceapi/cancel_replace_request.go index 4494a204ac..39490dc950 100644 --- a/pkg/exchange/binance/binanceapi/cancel_replace_request.go +++ b/pkg/exchange/binance/binanceapi/cancel_replace_request.go @@ -2,8 +2,9 @@ package binanceapi import ( "github.com/adshao/go-binance/v2" - "github.com/c9s/bbgo/pkg/types" "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/types" ) type CancelReplaceSpotOrderData struct { diff --git a/pkg/exchange/bitget/bitgetapi/client_test.go b/pkg/exchange/bitget/bitgetapi/client_test.go index 2472a59543..90cd69980a 100644 --- a/pkg/exchange/bitget/bitgetapi/client_test.go +++ b/pkg/exchange/bitget/bitgetapi/client_test.go @@ -67,4 +67,14 @@ func TestClient(t *testing.T) { assert.NoError(t, err) t.Logf("assets: %+v", assets) }) + + t.Run("GetAccountTransfersRequest", func(t *testing.T) { + req := client.NewGetAccountTransfersRequest() + req.CoinId(1) + req.FromType(AccountExchange) + transfers, err := req.Do(ctx) + + assert.NoError(t, err) + t.Logf("transfers: %+v", transfers) + }) } diff --git a/pkg/exchange/bitget/bitgetapi/get_account_transfers_request.go b/pkg/exchange/bitget/bitgetapi/get_account_transfers_request.go new file mode 100644 index 0000000000..d0a1d63da3 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_account_transfers_request.go @@ -0,0 +1,44 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type AccountType string + +const ( + AccountExchange AccountType = "EXCHANGE" + AccountContract AccountType = "CONTRACT" +) + +type Transfer struct { + CTime types.MillisecondTimestamp `json:"cTime"` + CoinId string `json:"coinId"` + CoinName string `json:"coinName"` + GroupType string `json:"groupType"` + BizType string `json:"bizType"` + Quantity fixedpoint.Value `json:"quantity"` + Balance fixedpoint.Value `json:"balance"` + Fees fixedpoint.Value `json:"fees"` + BillId string `json:"billId"` +} + +//go:generate GetRequest -url "/api/spot/v1/account/transferRecords" -type GetAccountTransfersRequest -responseDataType []Transfer +type GetAccountTransfersRequest struct { + client requestgen.AuthenticatedAPIClient + + coinId int `param:"coinId"` + fromType AccountType `param:"fromType"` + after string `param:"after"` + before string `param:"before"` +} + +func (c *RestClient) NewGetAccountTransfersRequest() *GetAccountTransfersRequest { + return &GetAccountTransfersRequest{client: c} +} diff --git a/pkg/exchange/bitget/bitgetapi/get_account_transfers_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_account_transfers_request_requestgen.go new file mode 100644 index 0000000000..6757b59ea1 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/get_account_transfers_request_requestgen.go @@ -0,0 +1,193 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/spot/v1/account/transferRecords -type GetAccountTransfersRequest -responseDataType []Transfer"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetAccountTransfersRequest) CoinId(coinId int) *GetAccountTransfersRequest { + g.coinId = coinId + return g +} + +func (g *GetAccountTransfersRequest) FromType(fromType AccountType) *GetAccountTransfersRequest { + g.fromType = fromType + return g +} + +func (g *GetAccountTransfersRequest) After(after string) *GetAccountTransfersRequest { + g.after = after + return g +} + +func (g *GetAccountTransfersRequest) Before(before string) *GetAccountTransfersRequest { + g.before = before + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetAccountTransfersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetAccountTransfersRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check coinId field -> json key coinId + coinId := g.coinId + + // assign parameter of coinId + params["coinId"] = coinId + // check fromType field -> json key fromType + fromType := g.fromType + + // TEMPLATE check-valid-values + switch fromType { + case AccountExchange, AccountContract: + params["fromType"] = fromType + + default: + return nil, fmt.Errorf("fromType value %v is invalid", fromType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of fromType + params["fromType"] = fromType + // check after field -> json key after + after := g.after + + // assign parameter of after + params["after"] = after + // check before field -> json key before + before := g.before + + // assign parameter of before + params["before"] = before + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetAccountTransfersRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetAccountTransfersRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetAccountTransfersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetAccountTransfersRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetAccountTransfersRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetAccountTransfersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetAccountTransfersRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetAccountTransfersRequest) Do(ctx context.Context) ([]Transfer, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/spot/v1/account/transferRecords" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []Transfer + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/bitget/bitgetapi/get_order_detail_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/get_order_detail_request_requestgen.go index 70055fe7cb..d048ae5f11 100644 --- a/pkg/exchange/bitget/bitgetapi/get_order_detail_request_requestgen.go +++ b/pkg/exchange/bitget/bitgetapi/get_order_detail_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Data -url /api/spot/v1/trade/orderInfo -type GetOrderDetailRequest -responseDataType .OrderDetail"; DO NOT EDIT. +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Data -url /api/spot/v1/trade/orderInfo -type GetOrderDetailRequest -responseDataType []OrderDetail"; DO NOT EDIT. package bitgetapi @@ -145,7 +145,7 @@ func (g *GetOrderDetailRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } -func (g *GetOrderDetailRequest) Do(ctx context.Context) (*OrderDetail, error) { +func (g *GetOrderDetailRequest) Do(ctx context.Context) ([]OrderDetail, error) { params, err := g.GetParameters() if err != nil { @@ -169,9 +169,9 @@ func (g *GetOrderDetailRequest) Do(ctx context.Context) (*OrderDetail, error) { if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } - var data OrderDetail + var data []OrderDetail if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } - return &data, nil + return data, nil } From 84ec32060157f3893140311c32a80599a703756b Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 18 Jul 2023 10:54:39 +0800 Subject: [PATCH 1208/1392] autoborrow: show debt and total for debt ratio --- pkg/strategy/autoborrow/strategy.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 836b65653b..b09ee24a8a 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -180,7 +180,9 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { } // debt / total - debtRatio := b.Debt().Div(b.Total()) + debt := b.Debt() + total := b.Total() + debtRatio := debt.Div(total) if marginAsset.MinDebtRatio.IsZero() { marginAsset.MinDebtRatio = fixedpoint.One } @@ -189,7 +191,7 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { continue } - log.Infof("checking debtRatio: session = %s asset = %s, debtRatio = %f", s.ExchangeSession.Name, marginAsset.Asset, debtRatio.Float64()) + log.Infof("checking debtRatio: session = %s asset = %s, debt = %f, total = %f, debtRatio = %f", s.ExchangeSession.Name, marginAsset.Asset, debt.Float64(), total.Float64(), debtRatio.Float64()) // if debt is greater than total, skip repay if b.Debt().Compare(b.Total()) > 0 { From b9734bca0c33a3567d0e340b69269b925240b8bf Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 18 Jul 2023 10:56:42 +0800 Subject: [PATCH 1209/1392] fix/linregmaker: missing line --- pkg/strategy/linregmaker/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 2ebbc17564..6400bfb927 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -685,6 +685,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Add strategy parameters to report if s.TrackParameters && s.ProfitStatsTracker.AccumulatedProfitReport != nil { s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("ReverseEMAWindow", strconv.Itoa(s.ReverseEMA.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FastLinRegWindow", strconv.Itoa(s.FastLinReg.Window)) s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FastLinRegInterval", s.FastLinReg.Interval.String()) s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("SlowLinRegWindow", strconv.Itoa(s.SlowLinReg.Window)) s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("SlowLinRegInterval", s.SlowLinReg.Interval.String()) From 1773c8d1558ae6d7d74eea4597e10d1b41cf3286 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 18 Jul 2023 11:00:02 +0800 Subject: [PATCH 1210/1392] fix/linregmaker: use float64() to output parameters --- pkg/strategy/linregmaker/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index 6400bfb927..996d040d7f 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -689,10 +689,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FastLinRegInterval", s.FastLinReg.Interval.String()) s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("SlowLinRegWindow", strconv.Itoa(s.SlowLinReg.Window)) s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("SlowLinRegInterval", s.SlowLinReg.Interval.String()) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FasterDecreaseRatio", s.FasterDecreaseRatio.String()) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("FasterDecreaseRatio", strconv.FormatFloat(s.FasterDecreaseRatio.Float64(), 'f', 4, 64)) s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("NeutralBollingerWindow", strconv.Itoa(s.NeutralBollinger.Window)) s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("NeutralBollingerBandWidth", strconv.FormatFloat(s.NeutralBollinger.BandWidth, 'f', 4, 64)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("Spread", s.Spread.Percentage()) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("Spread", strconv.FormatFloat(s.Spread.Float64(), 'f', 4, 64)) } s.ProfitStatsTracker.Bind(s.session, s.orderExecutor.TradeCollector()) From 3144b640ee380cdd10917c3dc71addbfe2097a02 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 18 Jul 2023 11:01:21 +0800 Subject: [PATCH 1211/1392] autoborrow: update account after repaying the debts --- pkg/strategy/autoborrow/strategy.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index b09ee24a8a..0f9129b675 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -165,7 +165,6 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { } minMarginLevel := s.MinMarginLevel - curMarginLevel := account.MarginLevel balances := account.Balances() if len(balances) == 0 { @@ -219,13 +218,19 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { Action: fmt.Sprintf("Repay for Debt Ratio %f", debtRatio.Float64()), Asset: b.Currency, Amount: toRepay, - MarginLevel: curMarginLevel, + MarginLevel: account.MarginLevel, MinMarginLevel: minMarginLevel, }) if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), b.Currency, toRepay); err != nil { log.WithError(err).Errorf("margin repay error") } + + if accountUpdate, err2 := s.ExchangeSession.UpdateAccount(ctx); err2 != nil { + log.WithError(err).Errorf("unable to update account") + } else { + account = accountUpdate + } } } From a0145934ece0b443c478a273c828e76f45e290ba Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 18 Jul 2023 11:04:03 +0800 Subject: [PATCH 1212/1392] autoborrow: show min debt ratio in the message --- pkg/strategy/autoborrow/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 0f9129b675..b3aa3c65f3 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -215,7 +215,7 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { bbgo.Notify(&MarginAction{ Exchange: s.ExchangeSession.ExchangeName, - Action: fmt.Sprintf("Repay for Debt Ratio %f", debtRatio.Float64()), + Action: fmt.Sprintf("Repay for Debt Ratio %f < Min Debt Ratio %f", debtRatio.Float64(), marginAsset.MinDebtRatio.Float64()), Asset: b.Currency, Amount: toRepay, MarginLevel: account.MarginLevel, From e6958f44f0e770b087c1720a00162f54cbf3a9af Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 18 Jul 2023 11:04:43 +0800 Subject: [PATCH 1213/1392] autoborrow: fix log message --- pkg/strategy/autoborrow/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index b3aa3c65f3..472987fbb3 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -194,14 +194,14 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { // if debt is greater than total, skip repay if b.Debt().Compare(b.Total()) > 0 { - log.Infof("%s debt %f is less than total %f", marginAsset.Asset, b.Debt().Float64(), b.Total().Float64()) + log.Infof("%s debt %f is greater than total %f", marginAsset.Asset, b.Debt().Float64(), b.Total().Float64()) continue } // the current debt ratio is less than the minimal ratio, // we need to repay and reduce the debt if debtRatio.Compare(marginAsset.MinDebtRatio) > 0 { - log.Infof("%s debt ratio %f is less than min debt ratio %f, skip", marginAsset.Asset, debtRatio.Float64(), marginAsset.MinDebtRatio.Float64()) + log.Infof("%s debt ratio %f is greater than min debt ratio %f, skip", marginAsset.Asset, debtRatio.Float64(), marginAsset.MinDebtRatio.Float64()) continue } From 8f62665cfd6c07c9d5e97555ea0f24e49ca5487c Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 18 Jul 2023 11:08:22 +0800 Subject: [PATCH 1214/1392] autoborrow: add another skip log --- pkg/strategy/autoborrow/strategy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 472987fbb3..9a983f64f3 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -186,7 +186,8 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { marginAsset.MinDebtRatio = fixedpoint.One } - if b.Total().Compare(marginAsset.Low) <= 0 { + if total.Compare(marginAsset.Low) <= 0 { + log.Infof("%s total %f is less than margin asset low %f, skip repay", marginAsset.Asset, total.Float64(), marginAsset.Low.Float64()) continue } From d99aa1f01380fbe2fca2872538f6ccd3886235dc Mon Sep 17 00:00:00 2001 From: gx578007 Date: Tue, 18 Jul 2023 15:48:27 +0800 Subject: [PATCH 1215/1392] FIX: [grid2] fix upper pin --- pkg/strategy/grid2/grid.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index e5f9978c99..84e646733b 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -52,7 +52,12 @@ func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) [] } // this makes sure there is no error at the upper price - pins = append(pins, Pin(upper)) + pp := math.Round(upper.Float64()*pow10*10.0) / 10.0 + pp = math.Trunc(pp) / pow10 + + pps := strconv.FormatFloat(pp, 'f', prec, 64) + upperPrice := fixedpoint.MustNewFromString(pps) + pins = append(pins, Pin(upperPrice)) return pins } From bded2edaf2cb3fbbc4a41516e2b5fb978bff004a Mon Sep 17 00:00:00 2001 From: gx578007 Date: Tue, 18 Jul 2023 15:48:27 +0800 Subject: [PATCH 1216/1392] FIX: [grid2] fix upper pin --- pkg/strategy/grid2/grid.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index e5f9978c99..84e646733b 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -52,7 +52,12 @@ func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) [] } // this makes sure there is no error at the upper price - pins = append(pins, Pin(upper)) + pp := math.Round(upper.Float64()*pow10*10.0) / 10.0 + pp = math.Trunc(pp) / pow10 + + pps := strconv.FormatFloat(pp, 'f', prec, 64) + upperPrice := fixedpoint.MustNewFromString(pps) + pins = append(pins, Pin(upperPrice)) return pins } From 93d10eba5a89e1e08e37512f6fb81979e4896f70 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 18 Jul 2023 14:31:32 +0800 Subject: [PATCH 1217/1392] autoborrow: improve logging details --- pkg/strategy/autoborrow/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 9a983f64f3..d39356155a 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -187,15 +187,15 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { } if total.Compare(marginAsset.Low) <= 0 { - log.Infof("%s total %f is less than margin asset low %f, skip repay", marginAsset.Asset, total.Float64(), marginAsset.Low.Float64()) + log.Infof("%s total %f is less than margin asset low %f, skip early repay", marginAsset.Asset, total.Float64(), marginAsset.Low.Float64()) continue } log.Infof("checking debtRatio: session = %s asset = %s, debt = %f, total = %f, debtRatio = %f", s.ExchangeSession.Name, marginAsset.Asset, debt.Float64(), total.Float64(), debtRatio.Float64()) // if debt is greater than total, skip repay - if b.Debt().Compare(b.Total()) > 0 { - log.Infof("%s debt %f is greater than total %f", marginAsset.Asset, b.Debt().Float64(), b.Total().Float64()) + if debt.Compare(total) > 0 { + log.Infof("%s debt %f is greater than total %f, skip early repay", marginAsset.Asset, debt.Float64(), total.Float64()) continue } From 1dae711d332e5ddac1230f83d924abc4ad3ece60 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 19 Jul 2023 17:33:12 +0800 Subject: [PATCH 1218/1392] fix trade collector race condition and infinite iterate --- pkg/bbgo/persistence.go | 4 +-- pkg/core/tradecollector.go | 61 ++++++++++++++++--------------------- pkg/core/tradestore.go | 2 ++ pkg/dynamic/iterate.go | 6 ++-- pkg/dynamic/iterate_test.go | 6 ++-- 5 files changed, 37 insertions(+), 42 deletions(-) diff --git a/pkg/bbgo/persistence.go b/pkg/bbgo/persistence.go index 34b8205b5d..04e13bdcbd 100644 --- a/pkg/bbgo/persistence.go +++ b/pkg/bbgo/persistence.go @@ -43,7 +43,7 @@ func Sync(ctx context.Context, obj interface{}) { } func loadPersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error { - return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, field reflect.StructField, value reflect.Value) error { + return dynamic.IterateFieldsByTag(obj, "persistence", true, func(tag string, field reflect.StructField, value reflect.Value) error { log.Debugf("[loadPersistenceFields] loading value into field %v, tag = %s, original value = %v", field, tag, value) newValueInf := dynamic.NewTypeValueInterface(value.Type()) @@ -71,7 +71,7 @@ func loadPersistenceFields(obj interface{}, id string, persistence service.Persi } func storePersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error { - return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { + return dynamic.IterateFieldsByTag(obj, "persistence", true, func(tag string, ft reflect.StructField, fv reflect.Value) error { log.Debugf("[storePersistenceFields] storing value from field %v, tag = %s, original value = %v", ft, tag, fv) inf := fv.Interface() diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go index 4bf1ddaa69..e844d35d57 100644 --- a/pkg/core/tradecollector.go +++ b/pkg/core/tradecollector.go @@ -107,12 +107,6 @@ func (c *TradeCollector) Recover(ctx context.Context, ex types.ExchangeTradeHist return nil } -func (c *TradeCollector) setDone(key types.TradeKey) { - c.mu.Lock() - c.doneTrades[key] = struct{}{} - c.mu.Unlock() -} - // Process filters the received trades and see if there are orders matching the trades // if we have the order in the order store, then the trade will be considered for the position. // profit will also be calculated. @@ -120,48 +114,47 @@ func (c *TradeCollector) Process() bool { logrus.Debugf("TradeCollector.Process()") positionChanged := false + var trades []types.Trade + + // if it's already done, remove the trade from the trade store + c.mu.Lock() c.tradeStore.Filter(func(trade types.Trade) bool { key := trade.Key() - c.mu.Lock() - - // if it's already done, remove the trade from the trade store + // remove done trades if _, done := c.doneTrades[key]; done { - c.mu.Unlock() return true } - if c.position != nil { - if c.orderStore.Exists(trade.OrderID) { - var p types.Profit - profit, netProfit, madeProfit := c.position.AddTrade(trade) - if madeProfit { - p = c.position.NewProfit(trade, profit, netProfit) - } - - c.doneTrades[key] = struct{}{} - c.mu.Unlock() + // if it's the trade we're looking for, add it to the list and mark it as done + if c.orderStore.Exists(trade.OrderID) { + trades = append(trades, trade) + c.doneTrades[key] = struct{}{} + return true + } - c.EmitTrade(trade, profit, netProfit) - if !p.Profit.IsZero() { - c.EmitProfit(trade, &p) - } + return false + }) + c.mu.Unlock() - positionChanged = true - return true + for _, trade := range trades { + var p types.Profit + if c.position != nil { + profit, netProfit, madeProfit := c.position.AddTrade(trade) + if madeProfit { + p = c.position.NewProfit(trade, profit, netProfit) } + positionChanged = true + c.EmitTrade(trade, profit, netProfit) } else { - if c.orderStore.Exists(trade.OrderID) { - c.doneTrades[key] = struct{}{} - c.mu.Unlock() - c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) - return true - } + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) } - return false - }) + if !p.Profit.IsZero() { + c.EmitProfit(trade, &p) + } + } if positionChanged && c.position != nil { c.EmitPositionUpdate(c.position) diff --git a/pkg/core/tradestore.go b/pkg/core/tradestore.go index 98ce886e2a..485f820dcf 100644 --- a/pkg/core/tradestore.go +++ b/pkg/core/tradestore.go @@ -60,6 +60,7 @@ func (s *TradeStore) Clear() { type TradeFilter func(trade types.Trade) bool +// Filter filters the trades by a given TradeFilter function func (s *TradeStore) Filter(filter TradeFilter) { s.Lock() var trades = make(map[uint64]types.Trade) @@ -72,6 +73,7 @@ func (s *TradeStore) Filter(filter TradeFilter) { s.Unlock() } +// GetOrderTrades finds the trades match order id matches to the given order func (s *TradeStore) GetOrderTrades(o types.Order) (trades []types.Trade) { s.Lock() for _, t := range s.trades { diff --git a/pkg/dynamic/iterate.go b/pkg/dynamic/iterate.go index 8063698ba6..9c932d6056 100644 --- a/pkg/dynamic/iterate.go +++ b/pkg/dynamic/iterate.go @@ -56,7 +56,7 @@ func isStructPtr(tpe reflect.Type) bool { return tpe.Kind() == reflect.Ptr && tpe.Elem().Kind() == reflect.Struct } -func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error { +func IterateFieldsByTag(obj interface{}, tagName string, children bool, cb StructFieldIterator) error { sv := reflect.ValueOf(obj) st := reflect.TypeOf(obj) @@ -86,9 +86,9 @@ func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) continue } - if isStructPtr(ft.Type) && !fv.IsNil() { + if children && isStructPtr(ft.Type) && !fv.IsNil() { // recursive iterate the struct field - if err := IterateFieldsByTag(fv.Interface(), tagName, cb); err != nil { + if err := IterateFieldsByTag(fv.Interface(), tagName, false, cb); err != nil { return fmt.Errorf("unable to iterate struct fields over the type %v: %v", ft, err) } } diff --git a/pkg/dynamic/iterate_test.go b/pkg/dynamic/iterate_test.go index 11a42d1c00..2fb502b674 100644 --- a/pkg/dynamic/iterate_test.go +++ b/pkg/dynamic/iterate_test.go @@ -75,7 +75,7 @@ func TestIterateFieldsByTag(t *testing.T) { collectedTags := []string{} cnt := 0 - err := IterateFieldsByTag(&a, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { + err := IterateFieldsByTag(&a, "persistence", false, func(tag string, ft reflect.StructField, fv reflect.Value) error { cnt++ collectedTags = append(collectedTags, tag) return nil @@ -101,7 +101,7 @@ func TestIterateFieldsByTag(t *testing.T) { collectedTags := []string{} cnt := 0 - err := IterateFieldsByTag(&a, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { + err := IterateFieldsByTag(&a, "persistence", false, func(tag string, ft reflect.StructField, fv reflect.Value) error { cnt++ collectedTags = append(collectedTags, tag) return nil @@ -119,7 +119,7 @@ func TestIterateFieldsByTag(t *testing.T) { collectedTags := []string{} cnt := 0 - err := IterateFieldsByTag(a, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { + err := IterateFieldsByTag(a, "persistence", false, func(tag string, ft reflect.StructField, fv reflect.Value) error { cnt++ collectedTags = append(collectedTags, tag) return nil From f1a105cc06ad73e21d86cf895753735a3e34ee53 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 20 Jul 2023 12:19:31 +0800 Subject: [PATCH 1219/1392] fix iterate test --- pkg/dynamic/iterate_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/dynamic/iterate_test.go b/pkg/dynamic/iterate_test.go index 2fb502b674..4cbf02ecc9 100644 --- a/pkg/dynamic/iterate_test.go +++ b/pkg/dynamic/iterate_test.go @@ -75,7 +75,7 @@ func TestIterateFieldsByTag(t *testing.T) { collectedTags := []string{} cnt := 0 - err := IterateFieldsByTag(&a, "persistence", false, func(tag string, ft reflect.StructField, fv reflect.Value) error { + err := IterateFieldsByTag(&a, "persistence", true, func(tag string, ft reflect.StructField, fv reflect.Value) error { cnt++ collectedTags = append(collectedTags, tag) return nil @@ -101,7 +101,7 @@ func TestIterateFieldsByTag(t *testing.T) { collectedTags := []string{} cnt := 0 - err := IterateFieldsByTag(&a, "persistence", false, func(tag string, ft reflect.StructField, fv reflect.Value) error { + err := IterateFieldsByTag(&a, "persistence", true, func(tag string, ft reflect.StructField, fv reflect.Value) error { cnt++ collectedTags = append(collectedTags, tag) return nil @@ -119,7 +119,7 @@ func TestIterateFieldsByTag(t *testing.T) { collectedTags := []string{} cnt := 0 - err := IterateFieldsByTag(a, "persistence", false, func(tag string, ft reflect.StructField, fv reflect.Value) error { + err := IterateFieldsByTag(a, "persistence", true, func(tag string, ft reflect.StructField, fv reflect.Value) error { cnt++ collectedTags = append(collectedTags, tag) return nil From a3c16a41176efa2606bdbd0042af528968041298 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 20 Jul 2023 12:44:57 +0800 Subject: [PATCH 1220/1392] bbgo: use backoff for graceful cancel --- pkg/bbgo/order_executor_general.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 6c95b64954..2e6601b3b4 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -15,6 +15,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/util/backoff" ) var ErrExceededSubmitOrderRetryLimit = errors.New("exceeded submit order retry limit") @@ -429,15 +430,10 @@ func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context return nil } - if err := activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil { - // Retry once - if err = activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil { - return errors.Wrap(err, "graceful cancel error") - } - } + defer e.tradeCollector.Process() - e.tradeCollector.Process() - return nil + op := func() error { return activeOrders.GracefulCancel(ctx, e.session.Exchange) } + return backoff.RetryGeneral(ctx, op) } // GracefulCancel cancels all active maker orders if orders are not given, otherwise cancel all the given orders From a45c241b9b7c0490f6904988224e32d3803b8747 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 20 Jul 2023 17:05:53 +0800 Subject: [PATCH 1221/1392] types: turn off network error log --- pkg/types/stream.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/types/stream.go b/pkg/types/stream.go index c53472ac35..4cc5e63b3d 100644 --- a/pkg/types/stream.go +++ b/pkg/types/stream.go @@ -214,13 +214,13 @@ func (s *StandardStream) Read(ctx context.Context, conn *websocket.Conn, cancel return case net.Error: - log.WithError(err).Error("websocket read network error") + log.WithError(err).Warn("websocket read network error") _ = conn.Close() s.Reconnect() return default: - log.WithError(err).Error("unexpected websocket error") + log.WithError(err).Warn("unexpected websocket error") _ = conn.Close() s.Reconnect() return From 2fa2e846ec86019f7e1baef9fcb9590a8b648cdb Mon Sep 17 00:00:00 2001 From: chechia Date: Thu, 20 Jul 2023 17:59:54 +0800 Subject: [PATCH 1222/1392] FEATURE: add log formatter flag in chart value --- charts/bbgo/Chart.yaml | 2 +- charts/bbgo/templates/deployment.yaml | 4 ++++ charts/bbgo/values.yaml | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/charts/bbgo/Chart.yaml b/charts/bbgo/Chart.yaml index f932e0c34a..92d9fcbb41 100644 --- a/charts/bbgo/Chart.yaml +++ b/charts/bbgo/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.3.4 +version: 0.3.5 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/bbgo/templates/deployment.yaml b/charts/bbgo/templates/deployment.yaml index 7809e4756c..e12b974dc5 100644 --- a/charts/bbgo/templates/deployment.yaml +++ b/charts/bbgo/templates/deployment.yaml @@ -54,6 +54,10 @@ spec: {{- if .Values.webserver.enabled }} - "--enable-webserver" {{- end }} + {{- if .Values.logFormatter.enabled }} + - "--log-formatter" + - {{ .Values.logFormatter.format | quote }} + {{- end }} {{- if .Values.grpc.enabled }} - "--enable-grpc" - "--grpc-bind" diff --git a/charts/bbgo/values.yaml b/charts/bbgo/values.yaml index 2cd8b419c6..4c8c40dfc7 100644 --- a/charts/bbgo/values.yaml +++ b/charts/bbgo/values.yaml @@ -78,6 +78,10 @@ metrics: enabled: false port: 9090 +logFormatter: + enabled: false + format: json + grpc: enabled: false port: 50051 From cba5663fac69fda27b07ccd66b66868b97fbea97 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Fri, 21 Jul 2023 17:05:19 +0800 Subject: [PATCH 1223/1392] add unit test for okex exchange --- pkg/exchange/okex/okexapi/client_test.go | 76 ++++++++++++++++++++++++ pkg/testutil/auth.go | 13 ++++ 2 files changed, 89 insertions(+) create mode 100644 pkg/exchange/okex/okexapi/client_test.go diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go new file mode 100644 index 0000000000..f2c9d3e68b --- /dev/null +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -0,0 +1,76 @@ +package okexapi + +import ( + "context" + "os" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/testutil" +) + +func getTestClientOrSkip(t *testing.T) *RestClient { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + + key, secret, passphrase, ok := testutil.IntegrationTestWithPassphraseConfigured(t, "OKEX") + if !ok { + t.SkipNow() + return nil + } + + client := NewClient() + client.Auth(key, secret, passphrase) + return client +} + +func TestClient_GetInstrumentsRequest(t *testing.T) { + client := NewClient() + ctx := context.Background() + + ser := PublicDataService{client: client} + req := ser.NewGetInstrumentsRequest() + + instruments, err := req. + InstrumentType(InstrumentTypeSpot). + Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, instruments) + t.Logf("instruments: %+v", instruments) +} + +func TestClient_GetFundingRateRequest(t *testing.T) { + client := NewClient() + ctx := context.Background() + ser := PublicDataService{client: client} + req := ser.NewGetFundingRate() + + instrument, err := req. + InstrumentID("BTC-USDT-SWAP"). + Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, instrument) + t.Logf("instrument: %+v", instrument) +} + +func TestClient_PlaceOrderRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + ser := TradeService{client: client} + req := ser.NewPlaceOrderRequest() + + order, err := req. + InstrumentID("XTZ-BTC"). + TradeMode("cash"). + Side(SideTypeSell). + OrderType(OrderTypeLimit). + Price("0.001"). + Quantity("0.01"). + Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, order) + t.Logf("order: %+v", order) // Right now account has no money +} diff --git a/pkg/testutil/auth.go b/pkg/testutil/auth.go index 164207e295..8e5bd43c7c 100644 --- a/pkg/testutil/auth.go +++ b/pkg/testutil/auth.go @@ -23,3 +23,16 @@ func IntegrationTestConfigured(t *testing.T, prefix string) (key, secret string, return key, secret, ok } + +func IntegrationTestWithPassphraseConfigured(t *testing.T, prefix string) (key, secret, passphrase string, ok bool) { + var hasKey, hasSecret, hasPassphrase bool + key, hasKey = os.LookupEnv(prefix + "_API_KEY") + secret, hasSecret = os.LookupEnv(prefix + "_API_SECRET") + passphrase, hasPassphrase = os.LookupEnv(prefix + "_API_PASSPHRASE") + ok = hasKey && hasSecret && hasPassphrase && os.Getenv("TEST_"+prefix) == "1" + if ok { + t.Logf(prefix+" api integration test enabled, key = %s, secret = %s, passphrase= %s", maskSecret(key), maskSecret(secret), maskSecret(passphrase)) + } + + return key, secret, passphrase, ok +} From b250bf94bcfa6ae004c1ce8d9c38ac6cd463ed37 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 20 Jul 2023 18:02:20 +0800 Subject: [PATCH 1224/1392] rsicross: add more conditions to rsicross --- config/rsicross.yaml | 6 ++++-- pkg/strategy/rsicross/strategy.go | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/config/rsicross.yaml b/config/rsicross.yaml index 7ad1de4353..c9108be4e8 100644 --- a/config/rsicross.yaml +++ b/config/rsicross.yaml @@ -16,8 +16,10 @@ exchangeStrategies: rsicross: symbol: BTCUSDT interval: 5m - fastWindow: 3 - slowWindow: 12 + fastWindow: 6 + slowWindow: 18 + openBelow: 30.0 + closeAbove: 60.0 quantity: 0.1 diff --git a/pkg/strategy/rsicross/strategy.go b/pkg/strategy/rsicross/strategy.go index dafab6ae7f..eb6c055eb2 100644 --- a/pkg/strategy/rsicross/strategy.go +++ b/pkg/strategy/rsicross/strategy.go @@ -26,10 +26,12 @@ type Strategy struct { Environment *bbgo.Environment Market types.Market - Symbol string `json:"symbol"` - Interval types.Interval `json:"interval"` - SlowWindow int `json:"slowWindow"` - FastWindow int `json:"fastWindow"` + Symbol string `json:"symbol"` + Interval types.Interval `json:"interval"` + SlowWindow int `json:"slowWindow"` + FastWindow int `json:"fastWindow"` + OpenBelow fixedpoint.Value `json:"openBelow"` + CloseAbove fixedpoint.Value `json:"closeAbove"` bbgo.OpenPositionOptions } @@ -56,6 +58,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se rsiCross.OnUpdate(func(v float64) { switch indicatorv2.CrossType(v) { case indicatorv2.CrossOver: + if s.OpenBelow.Sign() > 0 && fastRsi.Last(0) > s.OpenBelow.Float64() { + return + } + opts := s.OpenPositionOptions opts.Long = true @@ -70,6 +76,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } case indicatorv2.CrossUnder: + if s.CloseAbove.Sign() > 0 && fastRsi.Last(0) < s.CloseAbove.Float64() { + return + } + if err := s.OrderExecutor.ClosePosition(ctx, fixedpoint.One); err != nil { logErr(err, "failed to close position") } From 8c02383df4bdca43a8c7c87971168a794960fbf9 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 20 Jul 2023 18:06:30 +0800 Subject: [PATCH 1225/1392] update rsicross config --- config/rsicross.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/config/rsicross.yaml b/config/rsicross.yaml index c9108be4e8..09744385f1 100644 --- a/config/rsicross.yaml +++ b/config/rsicross.yaml @@ -19,8 +19,7 @@ exchangeStrategies: fastWindow: 6 slowWindow: 18 openBelow: 30.0 - closeAbove: 60.0 - + closeAbove: 70.0 quantity: 0.1 ### RISK CONTROLS @@ -41,7 +40,7 @@ exchangeStrategies: backtest: startTime: "2022-01-01" - endTime: "2022-02-01" + endTime: "2022-03-01" symbols: - BTCUSDT sessions: [binance] @@ -52,4 +51,4 @@ backtest: takerFeeRate: 0.075% balances: BTC: 0.0 - USDT: 10000.0 + USDT: 10_000.0 From 461735e04390f6fbc9e9af4f38b061ca017607ac Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 Jul 2023 11:36:04 +0800 Subject: [PATCH 1226/1392] grid2: add remove duplicated pins and pull out filter price prec func --- pkg/strategy/grid2/grid.go | 45 +++++++++++++++++++++-------- pkg/strategy/grid2/grid_test.go | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 84e646733b..06587b9977 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -35,28 +35,49 @@ type Grid struct { type Pin fixedpoint.Value +// filterPrice filters price with the given precision +func filterPrice(p fixedpoint.Value, prec int) fixedpoint.Value { + var pow10 = math.Pow10(prec) + pp := math.Round(p.Float64()*pow10*10.0) / 10.0 + pp = math.Trunc(pp) / pow10 + + pps := strconv.FormatFloat(pp, 'f', prec, 64) + price := fixedpoint.MustNewFromString(pps) + return price +} + +func removeDuplicatedPins(pins []Pin) []Pin { + var buckets = map[string]struct{}{} + var out []Pin + + for _, pin := range pins { + p := fixedpoint.Value(pin) + + if _, exists := buckets[p.String()]; exists { + continue + } else { + out = append(out, pin) + } + + buckets[p.String()] = struct{}{} + } + + return out +} + func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) []Pin { var pins []Pin // tickSize number is like 0.01, 0.1, 0.001 var ts = tickSize.Float64() var prec = int(math.Round(math.Log10(ts) * -1.0)) - var pow10 = math.Pow10(prec) for p := lower; p.Compare(upper.Sub(spread)) <= 0; p = p.Add(spread) { - pp := math.Round(p.Float64()*pow10*10.0) / 10.0 - pp = math.Trunc(pp) / pow10 - - pps := strconv.FormatFloat(pp, 'f', prec, 64) - price := fixedpoint.MustNewFromString(pps) + price := filterPrice(p, prec) pins = append(pins, Pin(price)) } // this makes sure there is no error at the upper price - pp := math.Round(upper.Float64()*pow10*10.0) / 10.0 - pp = math.Trunc(pp) / pow10 - - pps := strconv.FormatFloat(pp, 'f', prec, 64) - upperPrice := fixedpoint.MustNewFromString(pps) + upperPrice := filterPrice(upper, prec) pins = append(pins, Pin(upperPrice)) return pins @@ -94,7 +115,7 @@ func (g *Grid) CalculateGeometricPins() { return nil } - g.addPins(g.calculator()) + g.addPins(removeDuplicatedPins(g.calculator())) } func (g *Grid) CalculateArithmeticPins() { diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 8176ad8d00..ac47f30722 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -214,3 +214,54 @@ func Test_calculateArithmeticPins(t *testing.T) { }) } } + +func Test_filterPrice1(t *testing.T) { + type args struct { + p fixedpoint.Value + prec int + } + tests := []struct { + name string + args args + want string + }{ + { + name: "basic", + args: args{p: number("31.2222"), prec: 3}, + want: "31.222", + }, + { + name: "roundup", + args: args{p: number("31.22295"), prec: 3}, + want: "31.223", + }, + { + name: "roundup2", + args: args{p: number("31.22290"), prec: 3}, + want: "31.222", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rst := filterPrice(tt.args.p, tt.args.prec) + assert.Equalf(t, tt.want, rst.String(), "filterPrice(%v, %v)", tt.args.p, tt.args.prec) + }) + } +} + +func Test_removeDuplicatedPins(t *testing.T) { + pins := []Pin{ + Pin(number("31.222")), + Pin(number("31.222")), + Pin(number("31.223")), + Pin(number("31.224")), + Pin(number("31.224")), + } + out := removeDuplicatedPins(pins) + assert.Equal(t, []Pin{ + Pin(number("31.222")), + Pin(number("31.223")), + Pin(number("31.224")), + }, out) + +} From df1067d309eb6ca553fe5f56457313a78dfed7b1 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 Jul 2023 11:45:30 +0800 Subject: [PATCH 1227/1392] grid2: simplify removeDuplicatedPins --- pkg/strategy/grid2/grid.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 06587b9977..8156382af6 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -55,10 +55,9 @@ func removeDuplicatedPins(pins []Pin) []Pin { if _, exists := buckets[p.String()]; exists { continue - } else { - out = append(out, pin) } + out = append(out, pin) buckets[p.String()] = struct{}{} } From aac288426cf524edf19fcccd890e0cdc8ba97190 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 Jul 2023 11:47:03 +0800 Subject: [PATCH 1228/1392] update github workflow branch patterns --- .github/workflows/go.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 7413c6e128..a0e21c855c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -4,7 +4,9 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] + branches: + - "main" + - "v*" jobs: build: From 941067670e9d5a618c4a8762d1fe902d9e43714c Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 Jul 2023 17:29:16 +0800 Subject: [PATCH 1229/1392] xmaker: pull out trade recover go routine --- pkg/core/tradecollector.go | 54 ++++++++++++++------------------- pkg/strategy/xmaker/strategy.go | 52 ++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go index e844d35d57..a782a8126a 100644 --- a/pkg/core/tradecollector.go +++ b/pkg/core/tradecollector.go @@ -89,6 +89,8 @@ func (c *TradeCollector) Emit() { } func (c *TradeCollector) Recover(ctx context.Context, ex types.ExchangeTradeHistoryService, symbol string, from time.Time) error { + logrus.Debugf("recovering %s trades...", symbol) + trades, err := ex.QueryTrades(ctx, symbol, &types.TradeQueryOptions{ StartTime: &from, }) @@ -98,8 +100,8 @@ func (c *TradeCollector) Recover(ctx context.Context, ex types.ExchangeTradeHist } for _, td := range trades { - logrus.Debugf("processing trade: %s", td.String()) - if c.ProcessTrade(td) { + logrus.Debugf("checking trade: %s", td.String()) + if c.processTrade(td) { logrus.Infof("recovered trade: %s", td.String()) c.EmitRecover(td) } @@ -178,45 +180,33 @@ func (c *TradeCollector) processTrade(trade types.Trade) bool { return false } - if c.orderStore.Exists(trade.OrderID) { - if c.position != nil { - profit, netProfit, madeProfit := c.position.AddTrade(trade) - if madeProfit { - p := c.position.NewProfit(trade, profit, netProfit) - c.EmitTrade(trade, profit, netProfit) - c.EmitProfit(trade, &p) - } else { - c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) - c.EmitProfit(trade, nil) - } - c.EmitPositionUpdate(c.position) + if !c.orderStore.Exists(trade.OrderID) { + return false + } + + if c.position != nil { + profit, netProfit, madeProfit := c.position.AddTrade(trade) + if madeProfit { + p := c.position.NewProfit(trade, profit, netProfit) + c.EmitTrade(trade, profit, netProfit) + c.EmitProfit(trade, &p) } else { c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + c.EmitProfit(trade, nil) } - - c.doneTrades[key] = struct{}{} - return true + c.EmitPositionUpdate(c.position) + } else { + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) } - return false + + c.doneTrades[key] = struct{}{} + return true } // return true when the given trade is added // return false when the given trade is not added func (c *TradeCollector) ProcessTrade(trade types.Trade) bool { - key := trade.Key() - // if it's already done, remove the trade from the trade store - c.mu.Lock() - if _, done := c.doneTrades[key]; done { - return false - } - c.mu.Unlock() - - if c.processTrade(trade) { - return true - } - - c.tradeStore.Add(trade) - return false + return c.processTrade(trade) } // Run is a goroutine executed in the background diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 2a2db7230c..01953e6af5 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -76,6 +76,11 @@ type Strategy struct { NotifyTrade bool `json:"notifyTrade"` + // RecoverTrade tries to find the missing trades via the REStful API + RecoverTrade bool `json:"recoverTrade"` + + RecoverTradeScanPeriod types.Duration `json:"recoverTradeScanPeriod"` + NumLayers int `json:"numLayers"` // Pips is the pips of the layer prices @@ -584,6 +589,38 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { s.orderStore.Add(returnOrders...) } +func (s *Strategy) tradeRecover(ctx context.Context) { + tradeScanInterval := s.RecoverTradeScanPeriod.Duration() + if tradeScanInterval == 0 { + tradeScanInterval = 30 * time.Minute + } + + tradeScanTicker := time.NewTicker(tradeScanInterval) + defer tradeScanTicker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-tradeScanTicker.C: + log.Infof("scanning trades from %s ago...", tradeScanInterval) + + if s.RecoverTrade { + startTime := time.Now().Add(-tradeScanInterval) + + if err := s.tradeCollector.Recover(ctx, s.sourceSession.Exchange.(types.ExchangeTradeHistoryService), s.Symbol, startTime); err != nil { + log.WithError(err).Errorf("query trades error") + } + + if err := s.tradeCollector.Recover(ctx, s.makerSession.Exchange.(types.ExchangeTradeHistoryService), s.Symbol, startTime); err != nil { + log.WithError(err).Errorf("query trades error") + } + } + } + } +} + func (s *Strategy) Validate() error { if s.Quantity.IsZero() || s.QuantityScale == nil { return errors.New("quantity or quantityScale can not be empty") @@ -779,6 +816,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.stopC = make(chan struct{}) + if s.RecoverTrade { + go s.tradeRecover(ctx) + } + go func() { posTicker := time.NewTicker(util.MillisecondsJitter(s.HedgeInterval.Duration(), 200)) defer posTicker.Stop() @@ -789,10 +830,6 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order reportTicker := time.NewTicker(time.Hour) defer reportTicker.Stop() - tradeScanInterval := 20 * time.Minute - tradeScanTicker := time.NewTicker(tradeScanInterval) - defer tradeScanTicker.Stop() - defer func() { if err := s.activeMakerOrders.GracefulCancel(context.Background(), s.makerSession.Exchange); err != nil { log.WithError(err).Errorf("can not cancel %s orders", s.Symbol) @@ -816,13 +853,6 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order case <-reportTicker.C: bbgo.Notify(s.ProfitStats) - case <-tradeScanTicker.C: - log.Infof("scanning trades from %s ago...", tradeScanInterval) - startTime := time.Now().Add(-tradeScanInterval) - if err := s.tradeCollector.Recover(ctx, s.sourceSession.Exchange.(types.ExchangeTradeHistoryService), s.Symbol, startTime); err != nil { - log.WithError(err).Errorf("query trades error") - } - case <-posTicker.C: // For positive position and positive covered position: // uncover position = +5 - +3 (covered position) = 2 From 70439f3fd9690f69a5f7055c2a6802ac1e719aa1 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 Jul 2023 17:30:24 +0800 Subject: [PATCH 1230/1392] xmaker: add tradeScanOverlapBufferPeriod time --- pkg/strategy/xmaker/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 01953e6af5..4a56dd4d8e 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -595,6 +595,8 @@ func (s *Strategy) tradeRecover(ctx context.Context) { tradeScanInterval = 30 * time.Minute } + tradeScanOverlapBufferPeriod := 5 * time.Minute + tradeScanTicker := time.NewTicker(tradeScanInterval) defer tradeScanTicker.Stop() @@ -607,7 +609,7 @@ func (s *Strategy) tradeRecover(ctx context.Context) { log.Infof("scanning trades from %s ago...", tradeScanInterval) if s.RecoverTrade { - startTime := time.Now().Add(-tradeScanInterval) + startTime := time.Now().Add(-tradeScanInterval).Add(-tradeScanOverlapBufferPeriod) if err := s.tradeCollector.Recover(ctx, s.sourceSession.Exchange.(types.ExchangeTradeHistoryService), s.Symbol, startTime); err != nil { log.WithError(err).Errorf("query trades error") From c13a5cdf6e42aa37394b5f9c3d882227372b98e1 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 Jul 2023 17:32:24 +0800 Subject: [PATCH 1231/1392] core: add recover logs for the recovered trade count --- pkg/core/tradecollector.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go index a782a8126a..87c59e053b 100644 --- a/pkg/core/tradecollector.go +++ b/pkg/core/tradecollector.go @@ -99,13 +99,17 @@ func (c *TradeCollector) Recover(ctx context.Context, ex types.ExchangeTradeHist return err } + cnt := 0 for _, td := range trades { logrus.Debugf("checking trade: %s", td.String()) if c.processTrade(td) { logrus.Infof("recovered trade: %s", td.String()) + cnt++ c.EmitRecover(td) } } + + logrus.Infof("%d %s trades were recovered", cnt, symbol) return nil } From fad8642a59bab0126a4e41b191ae2139fb4792f0 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 Jul 2023 17:34:09 +0800 Subject: [PATCH 1232/1392] xmaker: fix message --- pkg/strategy/xmaker/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 4a56dd4d8e..b99added28 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -811,7 +811,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order bbgo.Notify(position) }) s.tradeCollector.OnRecover(func(trade types.Trade) { - bbgo.Notify("Recover trade", trade) + bbgo.Notify("Recovered trade", trade) }) s.tradeCollector.BindStream(s.sourceSession.UserDataStream) s.tradeCollector.BindStream(s.makerSession.UserDataStream) From 2abd84aec99db062360abe653e73ef312280e5ad Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 Jul 2023 17:57:02 +0800 Subject: [PATCH 1233/1392] core: pull out RecoverTrade method --- pkg/core/tradecollector.go | 18 ++++++++++++++---- pkg/core/tradecollector_test.go | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go index 87c59e053b..695cd8a8a4 100644 --- a/pkg/core/tradecollector.go +++ b/pkg/core/tradecollector.go @@ -101,11 +101,8 @@ func (c *TradeCollector) Recover(ctx context.Context, ex types.ExchangeTradeHist cnt := 0 for _, td := range trades { - logrus.Debugf("checking trade: %s", td.String()) - if c.processTrade(td) { - logrus.Infof("recovered trade: %s", td.String()) + if c.RecoverTrade(td) { cnt++ - c.EmitRecover(td) } } @@ -113,6 +110,19 @@ func (c *TradeCollector) Recover(ctx context.Context, ex types.ExchangeTradeHist return nil } +func (c *TradeCollector) RecoverTrade(td types.Trade) bool { + logrus.Debugf("checking trade: %s", td.String()) + if c.processTrade(td) { + logrus.Infof("recovered trade: %s", td.String()) + c.EmitRecover(td) + return true + } + + // add to the trade store, and then we can recover it when an order is matched + c.tradeStore.Add(td) + return false +} + // Process filters the received trades and see if there are orders matching the trades // if we have the order in the order store, then the trade will be considered for the position. // profit will also be calculated. diff --git a/pkg/core/tradecollector_test.go b/pkg/core/tradecollector_test.go index ec29a2d639..0b1f8772fe 100644 --- a/pkg/core/tradecollector_test.go +++ b/pkg/core/tradecollector_test.go @@ -16,7 +16,7 @@ func TestTradeCollector_ShouldNotCountDuplicatedTrade(t *testing.T) { collector := NewTradeCollector(symbol, position, orderStore) assert.NotNil(t, collector) - matched := collector.ProcessTrade(types.Trade{ + matched := collector.RecoverTrade(types.Trade{ ID: 1, OrderID: 399, Exchange: types.ExchangeBinance, @@ -28,7 +28,7 @@ func TestTradeCollector_ShouldNotCountDuplicatedTrade(t *testing.T) { IsBuyer: true, }) assert.False(t, matched, "should be added to the trade store") - assert.Equal(t, 1, len(collector.tradeStore.Trades()), "should have one trade in the trade store") + assert.Equal(t, 1, len(collector.tradeStore.Trades()), "should have 1 trade in the trade store") orderStore.Add(types.Order{ SubmitOrder: types.SubmitOrder{ From 3bd821261fbb2d8f3dc7aaf15d76078de67201a1 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 22 Jul 2023 18:06:53 +0800 Subject: [PATCH 1234/1392] tri: fix lint issue --- pkg/strategy/tri/profit.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/tri/profit.go b/pkg/strategy/tri/profit.go index 7ea0ca0ce1..c72f689ed9 100644 --- a/pkg/strategy/tri/profit.go +++ b/pkg/strategy/tri/profit.go @@ -1,8 +1,6 @@ package tri import ( - "fmt" - "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -16,7 +14,7 @@ type Profit struct { } func (p *Profit) PlainText() string { - var title = fmt.Sprintf("Arbitrage Profit ") + var title = "Arbitrage Profit " title += style.PnLEmojiSimple(p.Profit) + " " title += style.PnLSignString(p.Profit) + " " + p.Asset @@ -28,7 +26,7 @@ func (p *Profit) PlainText() string { func (p *Profit) SlackAttachment() slack.Attachment { var color = style.PnLColor(p.Profit) - var title = fmt.Sprintf("Triangular PnL ") + var title = "Triangular PnL " title += style.PnLEmojiSimple(p.Profit) + " " title += style.PnLSignString(p.Profit) + " " + p.Asset From 5f2ead4ffd8e68243df266d71c855b2837e6adbc Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 14:57:50 +0800 Subject: [PATCH 1235/1392] maxapi: parse fd field and optimize trade snapshot parsing --- pkg/exchange/max/maxapi/userdata.go | 50 +++++++++--------------- pkg/exchange/max/maxapi/userdata_test.go | 45 +++++++++++++++++++++ 2 files changed, 64 insertions(+), 31 deletions(-) create mode 100644 pkg/exchange/max/maxapi/userdata_test.go diff --git a/pkg/exchange/max/maxapi/userdata.go b/pkg/exchange/max/maxapi/userdata.go index 8fe3e3c5d0..000e65946a 100644 --- a/pkg/exchange/max/maxapi/userdata.go +++ b/pkg/exchange/max/maxapi/userdata.go @@ -100,10 +100,12 @@ type TradeUpdate struct { Volume string `json:"v"` Market string `json:"M"` - Fee string `json:"f"` - FeeCurrency string `json:"fc"` - Timestamp int64 `json:"T"` - UpdateTime int64 `json:"TU"` + Fee string `json:"f"` + FeeCurrency string `json:"fc"` + FeeDiscounted bool `json:"fd"` + + Timestamp int64 `json:"T"` + UpdateTime int64 `json:"TU"` OrderID uint64 `json:"oi"` @@ -128,40 +130,26 @@ func parseTradeUpdate(v *fastjson.Value) TradeUpdate { type TradeUpdateEvent struct { BaseEvent - Trades []TradeUpdate `json:"t"` } -func parseTradeUpdateEvent(v *fastjson.Value) *TradeUpdateEvent { - var e TradeUpdateEvent - e.Event = string(v.GetStringBytes("e")) - e.Timestamp = v.GetInt64("T") - - for _, tv := range v.GetArray("t") { - e.Trades = append(e.Trades, parseTradeUpdate(tv)) - } - - return &e -} - -type TradeSnapshot []TradeUpdate - type TradeSnapshotEvent struct { BaseEvent - Trades []TradeUpdate `json:"t"` } -func parseTradeSnapshotEvent(v *fastjson.Value) *TradeSnapshotEvent { - var e TradeSnapshotEvent - e.Event = string(v.GetStringBytes("e")) - e.Timestamp = v.GetInt64("T") - - for _, tv := range v.GetArray("t") { - e.Trades = append(e.Trades, parseTradeUpdate(tv)) - } +func parseTradeUpdateEvent(v *fastjson.Value) (*TradeUpdateEvent, error) { + jsonBytes := v.String() + var e TradeUpdateEvent + err := json.Unmarshal([]byte(jsonBytes), &e) + return &e, err +} - return &e +func parseTradeSnapshotEvent(v *fastjson.Value) (*TradeSnapshotEvent, error) { + jsonBytes := v.String() + var e TradeSnapshotEvent + err := json.Unmarshal([]byte(jsonBytes), &e) + return &e, err } type BalanceMessage struct { @@ -252,10 +240,10 @@ func ParseUserEvent(v *fastjson.Value) (interface{}, error) { return parseOrderUpdateEvent(v), nil case "trade_snapshot", "mwallet_trade_snapshot": - return parseTradeSnapshotEvent(v), nil + return parseTradeSnapshotEvent(v) case "trade_update", "mwallet_trade_update": - return parseTradeUpdateEvent(v), nil + return parseTradeUpdateEvent(v) case "ad_ratio_snapshot", "ad_ratio_update": return parseADRatioEvent(v) diff --git a/pkg/exchange/max/maxapi/userdata_test.go b/pkg/exchange/max/maxapi/userdata_test.go new file mode 100644 index 0000000000..af80dcd224 --- /dev/null +++ b/pkg/exchange/max/maxapi/userdata_test.go @@ -0,0 +1,45 @@ +package max + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/valyala/fastjson" +) + +func Test_parseTradeSnapshotEvent(t *testing.T) { + fv, err := fastjson.Parse(`{ + "c": "user", + "e": "trade_snapshot", + "t": [{ + "i": 68444, + "p": "21499.0", + "v": "0.2658", + "M": "ethtwd", + "T": 1521726960357, + "sd": "bid", + "f": "3.2", + "fc": "twd", + "fd": false, + "m": true, + "oi": 7423, + "ci": "client-oid-1", + "gi": 123 + }], + "T": 1591786735192 + }`) + assert.NoError(t, err) + assert.NotNil(t, fv) + + evt, err := parseTradeSnapshotEvent(fv) + assert.NoError(t, err) + assert.NotNil(t, evt) + assert.Equal(t, "trade_snapshot", evt.Event) + assert.Equal(t, int64(1591786735192), evt.Timestamp) + assert.Equal(t, 1, len(evt.Trades)) + assert.Equal(t, "bid", evt.Trades[0].Side) + assert.Equal(t, "ethtwd", evt.Trades[0].Market) + assert.Equal(t, int64(1521726960357), evt.Trades[0].Timestamp) + assert.Equal(t, "3.2", evt.Trades[0].Fee) + assert.Equal(t, "twd", evt.Trades[0].FeeCurrency) +} From 9c20215f41d39abcfabdc1d8070e372db31c686e Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 15:00:03 +0800 Subject: [PATCH 1236/1392] max: use fixedpoint.Value for field parsing --- pkg/exchange/max/convert.go | 25 +++--------------- pkg/exchange/max/maxapi/userdata.go | 32 ++++++------------------ pkg/exchange/max/maxapi/userdata_test.go | 2 +- 3 files changed, 13 insertions(+), 46 deletions(-) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 6047f5ee63..48b7687f77 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -288,36 +288,19 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) { // trade time mts := time.Unix(0, t.Timestamp*int64(time.Millisecond)) - price, err := fixedpoint.NewFromString(t.Price) - if err != nil { - return nil, err - } - - quantity, err := fixedpoint.NewFromString(t.Volume) - if err != nil { - return nil, err - } - - quoteQuantity := price.Mul(quantity) - - fee, err := fixedpoint.NewFromString(t.Fee) - if err != nil { - return nil, err - } - return &types.Trade{ ID: t.ID, OrderID: t.OrderID, Symbol: toGlobalSymbol(t.Market), Exchange: types.ExchangeMax, - Price: price, - Quantity: quantity, + Price: t.Price, + Quantity: t.Volume, Side: side, IsBuyer: side == types.SideTypeBuy, IsMaker: t.Maker, - Fee: fee, + Fee: t.Fee, FeeCurrency: toGlobalCurrency(t.FeeCurrency), - QuoteQuantity: quoteQuantity, + QuoteQuantity: t.Price.Mul(t.Volume), Time: types.Time(mts), }, nil } diff --git a/pkg/exchange/max/maxapi/userdata.go b/pkg/exchange/max/maxapi/userdata.go index 000e65946a..26ff6cf4be 100644 --- a/pkg/exchange/max/maxapi/userdata.go +++ b/pkg/exchange/max/maxapi/userdata.go @@ -94,15 +94,15 @@ func parserOrderSnapshotEvent(v *fastjson.Value) *OrderSnapshotEvent { } type TradeUpdate struct { - ID uint64 `json:"i"` - Side string `json:"sd"` - Price string `json:"p"` - Volume string `json:"v"` - Market string `json:"M"` + ID uint64 `json:"i"` + Side string `json:"sd"` + Price fixedpoint.Value `json:"p"` + Volume fixedpoint.Value `json:"v"` + Market string `json:"M"` - Fee string `json:"f"` - FeeCurrency string `json:"fc"` - FeeDiscounted bool `json:"fd"` + Fee fixedpoint.Value `json:"f"` + FeeCurrency string `json:"fc"` + FeeDiscounted bool `json:"fd"` Timestamp int64 `json:"T"` UpdateTime int64 `json:"TU"` @@ -112,22 +112,6 @@ type TradeUpdate struct { Maker bool `json:"m"` } -func parseTradeUpdate(v *fastjson.Value) TradeUpdate { - return TradeUpdate{ - ID: v.GetUint64("i"), - Side: string(v.GetStringBytes("sd")), - Price: string(v.GetStringBytes("p")), - Volume: string(v.GetStringBytes("v")), - Market: string(v.GetStringBytes("M")), - Fee: string(v.GetStringBytes("f")), - FeeCurrency: string(v.GetStringBytes("fc")), - Timestamp: v.GetInt64("T"), - UpdateTime: v.GetInt64("TU"), - OrderID: v.GetUint64("oi"), - Maker: v.GetBool("m"), - } -} - type TradeUpdateEvent struct { BaseEvent Trades []TradeUpdate `json:"t"` diff --git a/pkg/exchange/max/maxapi/userdata_test.go b/pkg/exchange/max/maxapi/userdata_test.go index af80dcd224..2967e16dfe 100644 --- a/pkg/exchange/max/maxapi/userdata_test.go +++ b/pkg/exchange/max/maxapi/userdata_test.go @@ -40,6 +40,6 @@ func Test_parseTradeSnapshotEvent(t *testing.T) { assert.Equal(t, "bid", evt.Trades[0].Side) assert.Equal(t, "ethtwd", evt.Trades[0].Market) assert.Equal(t, int64(1521726960357), evt.Trades[0].Timestamp) - assert.Equal(t, "3.2", evt.Trades[0].Fee) + assert.Equal(t, "3.2", evt.Trades[0].Fee.String()) assert.Equal(t, "twd", evt.Trades[0].FeeCurrency) } From 16c62bbcba8ccd5451143f750ccf4e012cc53400 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 15:28:11 +0800 Subject: [PATCH 1237/1392] maxapi: fix max withdrawal api --- ...withdrawal_addresses_request_requestgen.go | 36 ++++++++-------- pkg/exchange/max/maxapi/withdrawal.go | 2 +- .../maxapi/withdrawal_request_requestgen.go | 42 +++++++++---------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/pkg/exchange/max/maxapi/get_withdrawal_addresses_request_requestgen.go b/pkg/exchange/max/maxapi/get_withdrawal_addresses_request_requestgen.go index 277e5d5ef0..71f4a0c516 100644 --- a/pkg/exchange/max/maxapi/get_withdrawal_addresses_request_requestgen.go +++ b/pkg/exchange/max/maxapi/get_withdrawal_addresses_request_requestgen.go @@ -21,8 +21,8 @@ func (g *GetWithdrawalAddressesRequest) GetQueryParameters() (url.Values, error) var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -55,13 +55,13 @@ func (g *GetWithdrawalAddressesRequest) GetParametersQuery() (url.Values, error) return query, err } - for k, v := range params { - if g.isVarSlice(v) { - g.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) }) } else { - query.Add(k, fmt.Sprintf("%v", v)) + query.Add(_k, fmt.Sprintf("%v", _v)) } } @@ -86,24 +86,24 @@ func (g *GetWithdrawalAddressesRequest) GetSlugParameters() (map[string]interfac } func (g *GetWithdrawalAddressesRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } -func (g *GetWithdrawalAddressesRequest) iterateSlice(slice interface{}, f func(it interface{})) { +func (g *GetWithdrawalAddressesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) } } -func (g *GetWithdrawalAddressesRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) +func (g *GetWithdrawalAddressesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: return true @@ -118,8 +118,8 @@ func (g *GetWithdrawalAddressesRequest) GetSlugsMap() (map[string]string, error) return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil diff --git a/pkg/exchange/max/maxapi/withdrawal.go b/pkg/exchange/max/maxapi/withdrawal.go index 93d63bae50..91e10179da 100644 --- a/pkg/exchange/max/maxapi/withdrawal.go +++ b/pkg/exchange/max/maxapi/withdrawal.go @@ -36,7 +36,7 @@ import ( type WithdrawalRequest struct { client requestgen.AuthenticatedAPIClient - addressUUID string `param:"address_uuid,required"` + addressUUID string `param:"withdraw_address_uuid,required"` currency string `param:"currency,required"` amount float64 `param:"amount"` } diff --git a/pkg/exchange/max/maxapi/withdrawal_request_requestgen.go b/pkg/exchange/max/maxapi/withdrawal_request_requestgen.go index e2cfeb3f61..b555008f33 100644 --- a/pkg/exchange/max/maxapi/withdrawal_request_requestgen.go +++ b/pkg/exchange/max/maxapi/withdrawal_request_requestgen.go @@ -31,8 +31,8 @@ func (w *WithdrawalRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -41,17 +41,17 @@ func (w *WithdrawalRequest) GetQueryParameters() (url.Values, error) { // GetParameters builds and checks the parameters and return the result in a map object func (w *WithdrawalRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} - // check addressUUID field -> json key address_uuid + // check addressUUID field -> json key withdraw_address_uuid addressUUID := w.addressUUID // TEMPLATE check-required if len(addressUUID) == 0 { - return nil, fmt.Errorf("address_uuid is required, empty string given") + return nil, fmt.Errorf("withdraw_address_uuid is required, empty string given") } // END TEMPLATE check-required // assign parameter of addressUUID - params["address_uuid"] = addressUUID + params["withdraw_address_uuid"] = addressUUID // check currency field -> json key currency currency := w.currency @@ -81,13 +81,13 @@ func (w *WithdrawalRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - if w.isVarSlice(v) { - w.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) + for _k, _v := range params { + if w.isVarSlice(_v) { + w.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) }) } else { - query.Add(k, fmt.Sprintf("%v", v)) + query.Add(_k, fmt.Sprintf("%v", _v)) } } @@ -112,24 +112,24 @@ func (w *WithdrawalRequest) GetSlugParameters() (map[string]interface{}, error) } func (w *WithdrawalRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } -func (w *WithdrawalRequest) iterateSlice(slice interface{}, f func(it interface{})) { +func (w *WithdrawalRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) } } -func (w *WithdrawalRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) +func (w *WithdrawalRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: return true @@ -144,8 +144,8 @@ func (w *WithdrawalRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil From b45fdea99a0ffa6e02bc6cb67ec1f7d7f9216044 Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 21 Jul 2023 14:55:27 +0800 Subject: [PATCH 1238/1392] pkg/exchange: add get account info and instruments info api for bybit --- pkg/exchange/bybit/bybitapi/client.go | 165 ++++++++++++++ pkg/exchange/bybit/bybitapi/client_test.go | 48 +++++ .../bybitapi/get_account_info_request.go | 24 +++ .../get_account_info_request_requestgen.go | 139 ++++++++++++ .../bybitapi/get_instruments_info_request.go | 51 +++++ ...get_instruments_info_request_requestgen.go | 202 ++++++++++++++++++ pkg/exchange/bybit/bybitapi/types.go | 14 ++ pkg/exchange/bybit/exchange.go | 45 ++++ pkg/exchange/kucoin/exchange.go | 3 +- 9 files changed, 689 insertions(+), 2 deletions(-) create mode 100644 pkg/exchange/bybit/bybitapi/client.go create mode 100644 pkg/exchange/bybit/bybitapi/client_test.go create mode 100644 pkg/exchange/bybit/bybitapi/get_account_info_request.go create mode 100644 pkg/exchange/bybit/bybitapi/get_account_info_request_requestgen.go create mode 100644 pkg/exchange/bybit/bybitapi/get_instruments_info_request.go create mode 100644 pkg/exchange/bybit/bybitapi/get_instruments_info_request_requestgen.go create mode 100644 pkg/exchange/bybit/bybitapi/types.go create mode 100644 pkg/exchange/bybit/exchange.go diff --git a/pkg/exchange/bybit/bybitapi/client.go b/pkg/exchange/bybit/bybitapi/client.go new file mode 100644 index 0000000000..aed96e4196 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/client.go @@ -0,0 +1,165 @@ +package bybitapi + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/c9s/requestgen" + "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/types" +) + +const defaultHTTPTimeout = time.Second * 15 +const RestBaseURL = "https://api.bybit.com" + +// defaultRequestWindowMilliseconds specify how long an HTTP request is valid. It is also used to prevent replay attacks. +var defaultRequestWindowMilliseconds = fmt.Sprintf("%d", time.Millisecond*5000) + +type RestClient struct { + requestgen.BaseAPIClient + + key, secret string +} + +func NewClient() (*RestClient, error) { + u, err := url.Parse(RestBaseURL) + if err != nil { + return nil, err + } + + return &RestClient{ + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: &http.Client{ + Timeout: defaultHTTPTimeout, + }, + }, + }, nil +} + +func (c *RestClient) Auth(key, secret string) { + c.key = key + // pragma: allowlist secret + c.secret = secret +} + +// newAuthenticatedRequest creates new http request for authenticated routes. +func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { + if len(c.key) == 0 { + return nil, errors.New("empty api key") + } + + if len(c.secret) == 0 { + return nil, errors.New("empty api secret") + } + + rel, err := url.Parse(refURL) + if err != nil { + return nil, err + } + + if params != nil { + rel.RawQuery = params.Encode() + } + + pathURL := c.BaseURL.ResolveReference(rel) + path := pathURL.Path + if rel.RawQuery != "" { + path += "?" + rel.RawQuery + } + + t := time.Now().In(time.UTC) + timestamp := strconv.FormatInt(t.UnixMilli(), 10) + + body, err := castPayload(payload) + if err != nil { + return nil, err + } + + var signKey string + switch method { + case http.MethodPost: + signKey = timestamp + c.key + defaultRequestWindowMilliseconds + string(body) + case http.MethodGet: + signKey = timestamp + c.key + defaultRequestWindowMilliseconds + rel.RawQuery + default: + return nil, fmt.Errorf("unexpected method: %s", method) + } + + // See https://bybit-exchange.github.io/docs/v5/guide#create-a-request + // + // 1. timestamp + API key + (recv_window) + (queryString | jsonBodyString) + // 2. Use the HMAC_SHA256 or RSA_SHA256 algorithm to sign the string in step 1, and convert it to a hex + // string (HMAC_SHA256) / base64 (RSA_SHA256) to obtain the sign parameter. + // 3. Append the sign parameter to request header, and send the HTTP request. + signature := sign(signKey, c.secret) + + req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("X-BAPI-API-KEY", c.key) + req.Header.Add("X-BAPI-TIMESTAMP", timestamp) + req.Header.Add("X-BAPI-SIGN", signature) + req.Header.Add("X-BAPI-RECV-WINDOW", defaultRequestWindowMilliseconds) + return req, nil +} + +func sign(payload string, secret string) string { + var sig = hmac.New(sha256.New, []byte(secret)) + _, err := sig.Write([]byte(payload)) + if err != nil { + return "" + } + + return base64.StdEncoding.EncodeToString(sig.Sum(nil)) +} + +func castPayload(payload interface{}) ([]byte, error) { + if payload == nil { + return nil, nil + } + + switch v := payload.(type) { + case string: + return []byte(v), nil + + case []byte: + return v, nil + + } + return json.Marshal(payload) +} + +/* +sample: + +{ + "retCode": 0, + "retMsg": "OK", + "result": { + }, + "retExtInfo": {}, + "time": 1671017382656 +} +*/ + +type APIResponse struct { + RetCode uint `json:"retCode"` + RetMsg string `json:"retMsg"` + Result json.RawMessage `json:"result"` + RetExtInfo json.RawMessage `json:"retExtInfo"` + Time types.MillisecondTimestamp `json:"time"` +} diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go new file mode 100644 index 0000000000..bc89d60fc5 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -0,0 +1,48 @@ +package bybitapi + +import ( + "context" + "os" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/testutil" +) + +func getTestClientOrSkip(t *testing.T) *RestClient { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + + key, secret, ok := testutil.IntegrationTestConfigured(t, "BYBIT") + if !ok { + t.Skip("BYBIT_* env vars are not configured") + return nil + } + + client, err := NewClient() + assert.NoError(t, err) + client.Auth(key, secret) + return client +} + +func TestClient(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + t.Run("GetAccountInfoRequest", func(t *testing.T) { + req := client.NewGetAccountRequest() + accountInfo, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("accountInfo: %+v", accountInfo) + }) + + t.Run("GetInstrumentsInfoRequest", func(t *testing.T) { + req := client.NewGetInstrumentsInfoRequest() + instrumentsInfo, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("instrumentsInfo: %+v", instrumentsInfo) + }) +} diff --git a/pkg/exchange/bybit/bybitapi/get_account_info_request.go b/pkg/exchange/bybit/bybitapi/get_account_info_request.go new file mode 100644 index 0000000000..38fa9837b7 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_account_info_request.go @@ -0,0 +1,24 @@ +package bybitapi + +import "github.com/c9s/requestgen" + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +type AccountInfo struct { + MarginMode string `json:"marginMode"` + UpdatedTime string `json:"updatedTime"` + UnifiedMarginStatus int `json:"unifiedMarginStatus"` + DcpStatus string `json:"dcpStatus"` + TimeWindow int `json:"timeWindow"` + SmpGroup int `json:"smpGroup"` +} + +//go:generate GetRequest -url "/v5/account/info" -type GetAccountInfoRequest -responseDataType .AccountInfo +type GetAccountInfoRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *RestClient) NewGetAccountRequest() *GetAccountInfoRequest { + return &GetAccountInfoRequest{client: c} +} diff --git a/pkg/exchange/bybit/bybitapi/get_account_info_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_account_info_request_requestgen.go new file mode 100644 index 0000000000..8d9be7df7d --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_account_info_request_requestgen.go @@ -0,0 +1,139 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/account/info -type GetAccountInfoRequest -responseDataType .AccountInfo"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetAccountInfoRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetAccountInfoRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetAccountInfoRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetAccountInfoRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetAccountInfoRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetAccountInfoRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetAccountInfoRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetAccountInfoRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetAccountInfoRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetAccountInfoRequest) Do(ctx context.Context) (*AccountInfo, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/v5/account/info" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data AccountInfo + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bybit/bybitapi/get_instruments_info_request.go b/pkg/exchange/bybit/bybitapi/get_instruments_info_request.go new file mode 100644 index 0000000000..524eb4bcb2 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_instruments_info_request.go @@ -0,0 +1,51 @@ +package bybitapi + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +type InstrumentsInfo struct { + Category Category `json:"category"` + List []struct { + Symbol string `json:"symbol"` + BaseCoin string `json:"baseCoin"` + QuoteCoin string `json:"quoteCoin"` + Innovation string `json:"innovation"` + Status Status `json:"status"` + MarginTrading string `json:"marginTrading"` + LotSizeFilter struct { + BasePrecision fixedpoint.Value `json:"basePrecision"` + QuotePrecision fixedpoint.Value `json:"quotePrecision"` + MinOrderQty fixedpoint.Value `json:"minOrderQty"` + MaxOrderQty fixedpoint.Value `json:"maxOrderQty"` + MinOrderAmt fixedpoint.Value `json:"minOrderAmt"` + MaxOrderAmt fixedpoint.Value `json:"maxOrderAmt"` + } `json:"lotSizeFilter"` + + PriceFilter struct { + TickSize fixedpoint.Value `json:"tickSize"` + } `json:"priceFilter"` + } `json:"list"` +} + +//go:generate GetRequest -url "/v5/market/instruments-info" -type GetInstrumentsInfoRequest -responseDataType .InstrumentsInfo +type GetInstrumentsInfoRequest struct { + client requestgen.APIClient + + category Category `param:"category,query" validValues:"spot"` + symbol *string `param:"symbol,query"` + limit *uint64 `param:"limit,query"` + cursor *string `param:"cursor,query"` +} + +func (c *RestClient) NewGetInstrumentsInfoRequest() *GetInstrumentsInfoRequest { + return &GetInstrumentsInfoRequest{ + client: c, + category: CategorySpot, + } +} diff --git a/pkg/exchange/bybit/bybitapi/get_instruments_info_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_instruments_info_request_requestgen.go new file mode 100644 index 0000000000..7e9a6536ac --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_instruments_info_request_requestgen.go @@ -0,0 +1,202 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/market/instruments-info -type GetInstrumentsInfoRequest -responseDataType .InstrumentsInfo"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetInstrumentsInfoRequest) Category(category Category) *GetInstrumentsInfoRequest { + g.category = category + return g +} + +func (g *GetInstrumentsInfoRequest) Symbol(symbol string) *GetInstrumentsInfoRequest { + g.symbol = &symbol + return g +} + +func (g *GetInstrumentsInfoRequest) Limit(limit uint64) *GetInstrumentsInfoRequest { + g.limit = &limit + return g +} + +func (g *GetInstrumentsInfoRequest) Cursor(cursor string) *GetInstrumentsInfoRequest { + g.cursor = &cursor + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetInstrumentsInfoRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check category field -> json key category + category := g.category + + // TEMPLATE check-valid-values + switch category { + case "spot": + params["category"] = category + + default: + return nil, fmt.Errorf("category value %v is invalid", category) + + } + // END TEMPLATE check-valid-values + + // assign parameter of category + params["category"] = category + // check symbol field -> json key symbol + if g.symbol != nil { + symbol := *g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check cursor field -> json key cursor + if g.cursor != nil { + cursor := *g.cursor + + // assign parameter of cursor + params["cursor"] = cursor + } else { + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetInstrumentsInfoRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetInstrumentsInfoRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetInstrumentsInfoRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetInstrumentsInfoRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetInstrumentsInfoRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetInstrumentsInfoRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetInstrumentsInfoRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetInstrumentsInfoRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetInstrumentsInfoRequest) Do(ctx context.Context) (*InstrumentsInfo, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v5/market/instruments-info" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data InstrumentsInfo + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bybit/bybitapi/types.go b/pkg/exchange/bybit/bybitapi/types.go new file mode 100644 index 0000000000..d58b31c333 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/types.go @@ -0,0 +1,14 @@ +package bybitapi + +type Category string + +const ( + CategorySpot Category = "spot" +) + +type Status string + +const ( + // StatusTrading is only include the "Trading" status for `spot` category. + StatusTrading Status = "Trading" +) diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go new file mode 100644 index 0000000000..b5e125ac8f --- /dev/null +++ b/pkg/exchange/bybit/exchange.go @@ -0,0 +1,45 @@ +package bybit + +import ( + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + "github.com/c9s/bbgo/pkg/types" +) + +var log = logrus.WithFields(logrus.Fields{ + "exchange": "bybit", +}) + +type Exchange struct { + key, secret string + client *bybitapi.RestClient +} + +func New(key, secret string) (*Exchange, error) { + client, err := bybitapi.NewClient() + if err != nil { + return nil, err + } + + if len(key) > 0 && len(secret) > 0 { + client.Auth(key, secret) + } + + return &Exchange{ + key: key, + // pragma: allowlist nextline secret + secret: secret, + client: client, + }, nil +} + +func (e *Exchange) Name() types.ExchangeName { + return types.ExchangeBybit +} + +// PlatformFeeCurrency returns empty string. The platform does not support "PlatformFeeCurrency" but instead charges +// fees using the native token. +func (e *Exchange) PlatformFeeCurrency() string { + return "" +} diff --git a/pkg/exchange/kucoin/exchange.go b/pkg/exchange/kucoin/exchange.go index 47da3da98d..be2d252c65 100644 --- a/pkg/exchange/kucoin/exchange.go +++ b/pkg/exchange/kucoin/exchange.go @@ -23,7 +23,7 @@ var queryOrderLimiter = rate.NewLimiter(rate.Every(6*time.Second), 1) var ErrMissingSequence = errors.New("sequence is missing") -// OKB is the platform currency of OKEx, pre-allocate static string here +// KCS is the platform currency of Kucoin, pre-allocate static string here const KCS = "KCS" var log = logrus.WithFields(logrus.Fields{ @@ -38,7 +38,6 @@ type Exchange struct { func New(key, secret, passphrase string) *Exchange { client := kucoinapi.NewClient() - // for public access mode if len(key) > 0 && len(secret) > 0 && len(passphrase) > 0 { client.Auth(key, secret, passphrase) } From ac5e2cf7125a3f831245de7f61de0134ecb18f2b Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 21 Jul 2023 14:59:37 +0800 Subject: [PATCH 1239/1392] pkg, types: add bybit to factor and update readme --- README.md | 7 ++++++- pkg/exchange/factory.go | 4 ++++ pkg/types/exchange.go | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 97afa55a6e..534af12b30 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,8 @@ the implementation. - OKEx Spot Exchange - Kucoin Spot Exchange - MAX Spot Exchange (located in Taiwan) -- Bitget (In Progress) +- Bitget Exchange (In Progress) +- Bybit Exchange (In Progress) ## Documentation and General Topics @@ -219,6 +220,10 @@ KUCOIN_API_KEY= KUCOIN_API_SECRET= KUCOIN_API_PASSPHRASE= KUCOIN_API_KEY_VERSION=2 + +# for Bybit exchange, if you have one +BYBIT_API_KEY= +BYBIT_API_SECRET= ``` Prepare your dotenv file `.env.local` and BBGO yaml config file `bbgo.yaml`. diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index 95bd53b879..00d6db7c9f 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -7,6 +7,7 @@ import ( "github.com/c9s/bbgo/pkg/exchange/binance" "github.com/c9s/bbgo/pkg/exchange/bitget" + "github.com/c9s/bbgo/pkg/exchange/bybit" "github.com/c9s/bbgo/pkg/exchange/kucoin" "github.com/c9s/bbgo/pkg/exchange/max" "github.com/c9s/bbgo/pkg/exchange/okex" @@ -44,6 +45,9 @@ func New(n types.ExchangeName, key, secret, passphrase string) (types.ExchangeMi case types.ExchangeBitget: return bitget.New(key, secret, passphrase), nil + case types.ExchangeBybit: + return bybit.New(key, secret) + default: return nil, fmt.Errorf("unsupported exchange: %v", n) diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index 36670c35e7..70e3c13798 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -46,6 +46,7 @@ const ( ExchangeKucoin ExchangeName = "kucoin" ExchangeBitget ExchangeName = "bitget" ExchangeBacktest ExchangeName = "backtest" + ExchangeBybit ExchangeName = "bybit" ) var SupportedExchanges = []ExchangeName{ @@ -54,6 +55,7 @@ var SupportedExchanges = []ExchangeName{ ExchangeOKEx, ExchangeKucoin, ExchangeBitget, + ExchangeBybit, // note: we are not using "backtest" } From 454388488f5f9cf64a5de76e4fecf4aa3abb7fc0 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 17:03:52 +0800 Subject: [PATCH 1240/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index 998cf94394..b0f63424a5 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -58,4 +58,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index fb55c1bab9..6c50e4d61e 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -41,4 +41,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 7d3b2a2a89..afa27126f2 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -50,4 +50,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index 55c4848d5a..d87c68b3bb 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -40,4 +40,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index 625fde3675..4388fc82e3 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -39,4 +39,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index ff2bfdb1ef..a706e6992d 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -49,4 +49,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index d71de327b4..94292542d3 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -41,4 +41,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index 1d5ab2ae67..ecceec3287 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -48,4 +48,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index 024fe92d48..e52067afe3 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -42,4 +42,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index 7318bd535d..5ca93451d3 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -45,4 +45,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 629469c2cb..0677e91865 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -42,4 +42,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index 5d94ef018d..78f7f10343 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -41,4 +41,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index 94ba912301..d507028599 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -38,4 +38,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index b1db273c0f..a18244310f 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -41,4 +41,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index fa728d4c57..73822e3f11 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -41,4 +41,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index d2996cb43f..5cd5d277c9 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -41,4 +41,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index d80f06e053..fdb662ffce 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -40,4 +40,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index 1cefc37b5b..0cda733d0d 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -44,4 +44,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index 058d1fb2e7..b1dc5f60c5 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -42,4 +42,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index 77430f6309..308f659811 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -40,4 +40,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index c3206629f9..8bf81baf45 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -49,4 +49,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 9b3414c08b..70efdc0acb 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -51,4 +51,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 49f7f3df53..4c30120405 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -46,4 +46,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index d1ff8a3b45..c43dafca78 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -42,4 +42,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index c74dbb118a..a9e70be571 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -42,4 +42,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 23a16f1b1e..153d25b239 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -40,4 +40,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index 703a6d2f36..e2dd4b0a8a 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -42,4 +42,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index 40e0b8e359..cb1bd63242 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -40,4 +40,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index a7e2eed90d..c24fc84e21 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -39,4 +39,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-Jul-2023 +###### Auto generated by spf13/cobra on 24-Jul-2023 From 8d8852ec0060d323ba176f20d502ec51bd065a0e Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 17:03:52 +0800 Subject: [PATCH 1241/1392] bump version to v1.51.0 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index 06cc47fe03..ad2a65e096 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.50.1-3f771030-dev" +const Version = "v1.51.0-afc5dbb9-dev" -const VersionGitRef = "3f771030" +const VersionGitRef = "afc5dbb9" diff --git a/pkg/version/version.go b/pkg/version/version.go index 76f20909e7..016cae6c3c 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.50.1-3f771030" +const Version = "v1.51.0-afc5dbb9" -const VersionGitRef = "3f771030" +const VersionGitRef = "afc5dbb9" From bc0fd7a312e82802314b28e30d450d7cf76987af Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 17:03:53 +0800 Subject: [PATCH 1242/1392] add v1.51.0 release note --- doc/release/v1.51.0.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 doc/release/v1.51.0.md diff --git a/doc/release/v1.51.0.md b/doc/release/v1.51.0.md new file mode 100644 index 0000000000..eaae8e89a9 --- /dev/null +++ b/doc/release/v1.51.0.md @@ -0,0 +1,26 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.50.2...main) + + - [#1241](https://github.com/c9s/bbgo/pull/1241): FEATURE: [max] add fee discounted field support + - [#1242](https://github.com/c9s/bbgo/pull/1242): FIX: [max] fix MAX withdrawal address parameter name + - [#1237](https://github.com/c9s/bbgo/pull/1237): FEATURE: add new exchange Bybit GetAccountInfo/GetInstrumentsInfo api + - [#1240](https://github.com/c9s/bbgo/pull/1240): FIX: [xmaker] refactor and fix xmaker's trade recover + - [#1239](https://github.com/c9s/bbgo/pull/1239): FIX: [grid2] add remove duplicated pins and pull out filter price prec func + - [#1236](https://github.com/c9s/bbgo/pull/1236): FEATURE: add log formatter flag in chart value + - [#1235](https://github.com/c9s/bbgo/pull/1235): CHORE: turn off network error log + - [#1234](https://github.com/c9s/bbgo/pull/1234): FIX: fix infinite iterate and critical section + - [#1233](https://github.com/c9s/bbgo/pull/1233): FIX: [grid2] fix upper pin + - [#1198](https://github.com/c9s/bbgo/pull/1198): FEATURE: add ProfitStatsTracker + - [#1229](https://github.com/c9s/bbgo/pull/1229): REFACTOR: rewrite cci indicator into v2 + - [#1230](https://github.com/c9s/bbgo/pull/1230): CHORE: add SMA example as a test + - [#1226](https://github.com/c9s/bbgo/pull/1226): REFACTOR: pull out base strategy struct + - [#1228](https://github.com/c9s/bbgo/pull/1228): REFACTOR: move v2 indicators to the indicator/v2 package + - [#1227](https://github.com/c9s/bbgo/pull/1227): FEATURE: add rsicross strategy + - [#1225](https://github.com/c9s/bbgo/pull/1225): FEATURE: support nested persistence + - [#1224](https://github.com/c9s/bbgo/pull/1224): FEATURE: add google spreadsheet service support + - [#1223](https://github.com/c9s/bbgo/pull/1223): FEATURE: add google spreadsheet service + - [#1222](https://github.com/c9s/bbgo/pull/1222): FIX: [xfunding] fix transfer clean up + - [#1221](https://github.com/c9s/bbgo/pull/1221): FEATURE: add triangular arbitrate strategy as an example + - [#1220](https://github.com/c9s/bbgo/pull/1220): REFACTOR: refactor risk control with the order executor interface and mocks + - [#1219](https://github.com/c9s/bbgo/pull/1219): FEATURE: [scmaker] integrate risk control + - [#1218](https://github.com/c9s/bbgo/pull/1218): FEATURE: [scmaker] add liquiditySkew support + - [#1217](https://github.com/c9s/bbgo/pull/1217): FIX: log hhllStop higher high and lower low detection instead of notify From 106e98afaaa670114a028c61340c273a0e17dba9 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 18:05:32 +0800 Subject: [PATCH 1243/1392] autoborrow: add more logs --- pkg/strategy/autoborrow/strategy.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index d39356155a..bf99ce1eb1 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -84,7 +84,7 @@ type MarginAsset struct { MaxTotalBorrow fixedpoint.Value `json:"maxTotalBorrow"` MaxQuantityPerBorrow fixedpoint.Value `json:"maxQuantityPerBorrow"` MinQuantityPerBorrow fixedpoint.Value `json:"minQuantityPerBorrow"` - MinDebtRatio fixedpoint.Value `json:"debtRatio"` + DebtRatio fixedpoint.Value `json:"debtRatio"` } type Strategy struct { @@ -181,9 +181,11 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { // debt / total debt := b.Debt() total := b.Total() + debtRatio := debt.Div(total) - if marginAsset.MinDebtRatio.IsZero() { - marginAsset.MinDebtRatio = fixedpoint.One + + if marginAsset.DebtRatio.IsZero() { + marginAsset.DebtRatio = fixedpoint.One } if total.Compare(marginAsset.Low) <= 0 { @@ -199,24 +201,29 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { continue } - // the current debt ratio is less than the minimal ratio, - // we need to repay and reduce the debt - if debtRatio.Compare(marginAsset.MinDebtRatio) > 0 { - log.Infof("%s debt ratio %f is greater than min debt ratio %f, skip", marginAsset.Asset, debtRatio.Float64(), marginAsset.MinDebtRatio.Float64()) + // if debtRatio is lesser, means that we have more spot, we should try to repay as much as we can + if debtRatio.Compare(marginAsset.DebtRatio) > 0 { + log.Infof("%s debt ratio %f is greater than min debt ratio %f, skip", marginAsset.Asset, debtRatio.Float64(), marginAsset.DebtRatio.Float64()) continue } + log.Infof("checking repayable balance: %+v", b) + toRepay := fixedpoint.Min(b.Borrowed, b.Available) toRepay = toRepay.Sub(marginAsset.Low) if toRepay.Sign() <= 0 { - log.Warnf("%s repay amount = 0, can not repay", marginAsset.Asset) + log.Warnf("%s repay = %f, available = %f, borrowed = %f, can not repay", + marginAsset.Asset, + toRepay.Float64(), + b.Available.Float64(), + b.Borrowed.Float64()) continue } bbgo.Notify(&MarginAction{ Exchange: s.ExchangeSession.ExchangeName, - Action: fmt.Sprintf("Repay for Debt Ratio %f < Min Debt Ratio %f", debtRatio.Float64(), marginAsset.MinDebtRatio.Float64()), + Action: fmt.Sprintf("Repay for Debt Ratio %f < Minimal Debt Ratio %f", debtRatio.Float64(), marginAsset.DebtRatio.Float64()), Asset: b.Currency, Amount: toRepay, MarginLevel: account.MarginLevel, From f014213c85b785593bee3ef1e6dba15b91f3fea8 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 18:13:53 +0800 Subject: [PATCH 1244/1392] autoborrow: log balances --- pkg/strategy/autoborrow/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index bf99ce1eb1..c82e0ec50f 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -158,6 +158,8 @@ func (s *Strategy) tryToRepayAnyDebt(ctx context.Context) { } func (s *Strategy) reBalanceDebt(ctx context.Context) { + log.Infof("rebalancing debt...") + account, err := s.ExchangeSession.UpdateAccount(ctx) if err != nil { log.WithError(err).Errorf("can not update account") @@ -172,6 +174,8 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { return } + log.Infof("balances: %#v", balances) + for _, marginAsset := range s.Assets { b, ok := balances[marginAsset.Asset] if !ok { From a5a9512ef18e07e1f8b050231f646476d49b7b94 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 18:23:09 +0800 Subject: [PATCH 1245/1392] autoborrow: check available --- pkg/strategy/autoborrow/strategy.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index c82e0ec50f..dec228fa0e 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -213,8 +213,16 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { log.Infof("checking repayable balance: %+v", b) - toRepay := fixedpoint.Min(b.Borrowed, b.Available) - toRepay = toRepay.Sub(marginAsset.Low) + toRepay := b.Borrowed + if !b.Available.IsZero() { + toRepay = fixedpoint.Min(b.Borrowed, b.Available) + } else { + log.Errorf("available balance is 0: %#v", b) + } + + if !marginAsset.Low.IsZero() { + toRepay = toRepay.Sub(marginAsset.Low) + } if toRepay.Sign() <= 0 { log.Warnf("%s repay = %f, available = %f, borrowed = %f, can not repay", From 3c32acc3ede21adec6cdb17720e4db93962127c8 Mon Sep 17 00:00:00 2001 From: Edwin Date: Mon, 24 Jul 2023 17:01:34 +0800 Subject: [PATCH 1246/1392] pkg/exchange: add query market to bybit --- .../bybitapi/get_instruments_info_request.go | 51 ++++++++------- pkg/exchange/bybit/convert.go | 53 ++++++++++++++++ pkg/exchange/bybit/convert_test.go | 62 +++++++++++++++++++ pkg/exchange/bybit/exchange.go | 16 +++++ 4 files changed, 159 insertions(+), 23 deletions(-) create mode 100644 pkg/exchange/bybit/convert.go create mode 100644 pkg/exchange/bybit/convert_test.go diff --git a/pkg/exchange/bybit/bybitapi/get_instruments_info_request.go b/pkg/exchange/bybit/bybitapi/get_instruments_info_request.go index 524eb4bcb2..288223101d 100644 --- a/pkg/exchange/bybit/bybitapi/get_instruments_info_request.go +++ b/pkg/exchange/bybit/bybitapi/get_instruments_info_request.go @@ -10,27 +10,29 @@ import ( //go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result type InstrumentsInfo struct { - Category Category `json:"category"` - List []struct { - Symbol string `json:"symbol"` - BaseCoin string `json:"baseCoin"` - QuoteCoin string `json:"quoteCoin"` - Innovation string `json:"innovation"` - Status Status `json:"status"` - MarginTrading string `json:"marginTrading"` - LotSizeFilter struct { - BasePrecision fixedpoint.Value `json:"basePrecision"` - QuotePrecision fixedpoint.Value `json:"quotePrecision"` - MinOrderQty fixedpoint.Value `json:"minOrderQty"` - MaxOrderQty fixedpoint.Value `json:"maxOrderQty"` - MinOrderAmt fixedpoint.Value `json:"minOrderAmt"` - MaxOrderAmt fixedpoint.Value `json:"maxOrderAmt"` - } `json:"lotSizeFilter"` - - PriceFilter struct { - TickSize fixedpoint.Value `json:"tickSize"` - } `json:"priceFilter"` - } `json:"list"` + Category Category `json:"category"` + List []Instrument `json:"list"` +} + +type Instrument struct { + Symbol string `json:"symbol"` + BaseCoin string `json:"baseCoin"` + QuoteCoin string `json:"quoteCoin"` + Innovation string `json:"innovation"` + Status Status `json:"status"` + MarginTrading string `json:"marginTrading"` + LotSizeFilter struct { + BasePrecision fixedpoint.Value `json:"basePrecision"` + QuotePrecision fixedpoint.Value `json:"quotePrecision"` + MinOrderQty fixedpoint.Value `json:"minOrderQty"` + MaxOrderQty fixedpoint.Value `json:"maxOrderQty"` + MinOrderAmt fixedpoint.Value `json:"minOrderAmt"` + MaxOrderAmt fixedpoint.Value `json:"maxOrderAmt"` + } `json:"lotSizeFilter"` + + PriceFilter struct { + TickSize fixedpoint.Value `json:"tickSize"` + } `json:"priceFilter"` } //go:generate GetRequest -url "/v5/market/instruments-info" -type GetInstrumentsInfoRequest -responseDataType .InstrumentsInfo @@ -39,8 +41,11 @@ type GetInstrumentsInfoRequest struct { category Category `param:"category,query" validValues:"spot"` symbol *string `param:"symbol,query"` - limit *uint64 `param:"limit,query"` - cursor *string `param:"cursor,query"` + + // limit is invalid if category spot. + limit *uint64 `param:"limit,query"` + // cursor is invalid if category spot. + cursor *string `param:"cursor,query"` } func (c *RestClient) NewGetInstrumentsInfoRequest() *GetInstrumentsInfoRequest { diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go new file mode 100644 index 0000000000..0b37d7adc3 --- /dev/null +++ b/pkg/exchange/bybit/convert.go @@ -0,0 +1,53 @@ +package bybit + +import ( + "math" + + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + "github.com/c9s/bbgo/pkg/types" +) + +func toGlobalMarket(m bybitapi.Instrument) types.Market { + // sample: + //Symbol: BTCUSDT + //BaseCoin: BTC + //QuoteCoin: USDT + //Innovation: 0 + //Status: Trading + //MarginTrading: both + // + //LotSizeFilter: + //{ + // BasePrecision: 0.000001 + // QuotePrecision: 0.00000001 + // MinOrderQty: 0.000048 + // MaxOrderQty: 71.73956243 + // MinOrderAmt: 1 + // MaxOrderAmt: 2000000 + //} + // + //PriceFilter: + //{ + // TickSize: 0.01 + //} + return types.Market{ + Symbol: m.Symbol, + LocalSymbol: m.Symbol, + PricePrecision: int(math.Log10(m.LotSizeFilter.QuotePrecision.Float64())), + VolumePrecision: int(math.Log10(m.LotSizeFilter.BasePrecision.Float64())), + QuoteCurrency: m.QuoteCoin, + BaseCurrency: m.BaseCoin, + MinNotional: m.LotSizeFilter.MinOrderAmt, + MinAmount: m.LotSizeFilter.MinOrderAmt, + + // quantity + MinQuantity: m.LotSizeFilter.MinOrderQty, + MaxQuantity: m.LotSizeFilter.MaxOrderQty, + StepSize: m.LotSizeFilter.BasePrecision, + + // price + MinPrice: m.LotSizeFilter.MinOrderAmt, + MaxPrice: m.LotSizeFilter.MaxOrderAmt, + TickSize: m.PriceFilter.TickSize, + } +} diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go new file mode 100644 index 0000000000..22325b9536 --- /dev/null +++ b/pkg/exchange/bybit/convert_test.go @@ -0,0 +1,62 @@ +package bybit + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func TestToGlobalMarket(t *testing.T) { + inst := bybitapi.Instrument{ + Symbol: "BTCUSDT", + BaseCoin: "BTC", + QuoteCoin: "USDT", + Innovation: "0", + Status: bybitapi.StatusTrading, + MarginTrading: "both", + LotSizeFilter: struct { + BasePrecision fixedpoint.Value `json:"basePrecision"` + QuotePrecision fixedpoint.Value `json:"quotePrecision"` + MinOrderQty fixedpoint.Value `json:"minOrderQty"` + MaxOrderQty fixedpoint.Value `json:"maxOrderQty"` + MinOrderAmt fixedpoint.Value `json:"minOrderAmt"` + MaxOrderAmt fixedpoint.Value `json:"maxOrderAmt"` + }{ + BasePrecision: fixedpoint.NewFromFloat(0.000001), + QuotePrecision: fixedpoint.NewFromFloat(0.00000001), + MinOrderQty: fixedpoint.NewFromFloat(0.000048), + MaxOrderQty: fixedpoint.NewFromFloat(71.73956243), + MinOrderAmt: fixedpoint.NewFromInt(1), + MaxOrderAmt: fixedpoint.NewFromInt(2000000), + }, + PriceFilter: struct { + TickSize fixedpoint.Value `json:"tickSize"` + }{ + TickSize: fixedpoint.NewFromFloat(0.01), + }, + } + + exp := types.Market{ + Symbol: inst.Symbol, + LocalSymbol: inst.Symbol, + PricePrecision: int(math.Log10(inst.LotSizeFilter.QuotePrecision.Float64())), + VolumePrecision: int(math.Log10(inst.LotSizeFilter.BasePrecision.Float64())), + QuoteCurrency: inst.QuoteCoin, + BaseCurrency: inst.BaseCoin, + MinNotional: inst.LotSizeFilter.MinOrderAmt, + MinAmount: inst.LotSizeFilter.MinOrderAmt, + MinQuantity: inst.LotSizeFilter.MinOrderQty, + MaxQuantity: inst.LotSizeFilter.MaxOrderQty, + StepSize: inst.LotSizeFilter.BasePrecision, + MinPrice: inst.LotSizeFilter.MinOrderAmt, + MaxPrice: inst.LotSizeFilter.MaxOrderAmt, + TickSize: inst.PriceFilter.TickSize, + } + + assert.Equal(t, toGlobalMarket(inst), exp) +} diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index b5e125ac8f..5abd5c6453 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -1,6 +1,8 @@ package bybit import ( + "context" + "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" @@ -43,3 +45,17 @@ func (e *Exchange) Name() types.ExchangeName { func (e *Exchange) PlatformFeeCurrency() string { return "" } + +func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { + instruments, err := e.client.NewGetInstrumentsInfoRequest().Do(ctx) + if err != nil { + return nil, err + } + + marketMap := types.MarketMap{} + for _, s := range instruments.List { + marketMap.Add(toGlobalMarket(s)) + } + + return marketMap, nil +} From a2a062e95b0786d076bff98377ea1e822e9a2cbb Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 22:56:32 +0800 Subject: [PATCH 1247/1392] autoborrow: use debt instead of using b.Borrowed --- pkg/strategy/autoborrow/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index dec228fa0e..8acacb38b0 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -213,9 +213,9 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { log.Infof("checking repayable balance: %+v", b) - toRepay := b.Borrowed + toRepay := debt if !b.Available.IsZero() { - toRepay = fixedpoint.Min(b.Borrowed, b.Available) + toRepay = fixedpoint.Min(toRepay, b.Available) } else { log.Errorf("available balance is 0: %#v", b) } From bfb11653042b4de5ee35e6c0861aadc94f6b6474 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 24 Jul 2023 23:01:22 +0800 Subject: [PATCH 1248/1392] autoborrow: fix debt checking condition --- pkg/strategy/autoborrow/strategy.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 8acacb38b0..cd681981b5 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -126,7 +126,9 @@ func (s *Strategy) tryToRepayAnyDebt(ctx context.Context) { balances := account.Balances() for _, b := range balances { - if b.Borrowed.Sign() <= 0 { + debt := b.Debt() + + if debt.Sign() <= 0 { continue } @@ -134,7 +136,7 @@ func (s *Strategy) tryToRepayAnyDebt(ctx context.Context) { continue } - toRepay := fixedpoint.Min(b.Available, b.Debt()) + toRepay := fixedpoint.Min(b.Available, debt) if toRepay.IsZero() { continue } From b7c9ef798321f5526208b9ee184081735a2d8e58 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Jul 2023 00:11:08 +0800 Subject: [PATCH 1249/1392] types: add NotZero() method to filter non-zero balances --- pkg/strategy/autoborrow/strategy.go | 6 +++--- pkg/types/balance.go | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index cd681981b5..a5d98ad2b4 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -170,13 +170,13 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { minMarginLevel := s.MinMarginLevel - balances := account.Balances() + balances := account.Balances().NotZero() if len(balances) == 0 { - log.Warn("balance is empty, skip autoborrow") + log.Warn("balance is empty, skip repay") return } - log.Infof("balances: %#v", balances) + log.Infof("non-zero balances: %+v", balances) for _, marginAsset := range s.Assets { b, ok := balances[marginAsset.Asset] diff --git a/pkg/types/balance.go b/pkg/types/balance.go index dc83f65b0d..dc151ea4f1 100644 --- a/pkg/types/balance.go +++ b/pkg/types/balance.go @@ -114,6 +114,18 @@ func (m BalanceSnapshot) CsvRecords() [][]string { type BalanceMap map[string]Balance +func (m BalanceMap) NotZero() BalanceMap { + bm := make(BalanceMap) + for c, b := range m { + if b.Total().IsZero() && b.Debt().IsZero() && b.Net().IsZero() { + continue + } + + bm[c] = b + } + return bm +} + func (m BalanceMap) Debts() BalanceMap { bm := make(BalanceMap) for c, b := range m { From 4cb9ff569af74fa0415e114b9b1a062ac3523c6d Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Jul 2023 00:16:05 +0800 Subject: [PATCH 1250/1392] autoborrow: improve available balance checking --- pkg/strategy/autoborrow/strategy.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index a5d98ad2b4..5c391748a8 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -216,12 +216,14 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { log.Infof("checking repayable balance: %+v", b) toRepay := debt - if !b.Available.IsZero() { - toRepay = fixedpoint.Min(toRepay, b.Available) - } else { - log.Errorf("available balance is 0: %#v", b) + + if b.Available.IsZero() { + log.Errorf("%s available balance is 0, can not repay, balance = %+v", marginAsset.Asset, b) + continue } + toRepay = fixedpoint.Min(toRepay, b.Available) + if !marginAsset.Low.IsZero() { toRepay = toRepay.Sub(marginAsset.Low) } @@ -235,6 +237,8 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { continue } + log.Infof("%s repay %f", marginAsset.Asset, toRepay.Float64()) + bbgo.Notify(&MarginAction{ Exchange: s.ExchangeSession.ExchangeName, Action: fmt.Sprintf("Repay for Debt Ratio %f < Minimal Debt Ratio %f", debtRatio.Float64(), marginAsset.DebtRatio.Float64()), From 66912298092f10bf37fdc3171bd402ee717d66be Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Jul 2023 00:18:36 +0800 Subject: [PATCH 1251/1392] fixedpoint: fix default fixedpoint conversion --- pkg/fixedpoint/convert.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/fixedpoint/convert.go b/pkg/fixedpoint/convert.go index 4f9b8b7ebe..9a8bf36fe2 100644 --- a/pkg/fixedpoint/convert.go +++ b/pkg/fixedpoint/convert.go @@ -561,7 +561,7 @@ func (x Value) Compare(y Value) int { } func Min(a, b Value) Value { - if a < b { + if a.Compare(b) < 0 { return a } @@ -569,7 +569,7 @@ func Min(a, b Value) Value { } func Max(a, b Value) Value { - if a > b { + if a.Compare(b) > 0 { return a } From 8a3c89ba919260e1dd61c2045854f013253206c2 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Jul 2023 00:27:43 +0800 Subject: [PATCH 1252/1392] autoborrow: fix marginAsset.Low calculation --- pkg/strategy/autoborrow/strategy.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 5c391748a8..eed96abbed 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -225,7 +225,10 @@ func (s *Strategy) reBalanceDebt(ctx context.Context) { toRepay = fixedpoint.Min(toRepay, b.Available) if !marginAsset.Low.IsZero() { - toRepay = toRepay.Sub(marginAsset.Low) + extra := b.Available.Sub(marginAsset.Low) + if extra.Sign() > 0 { + toRepay = fixedpoint.Min(extra, toRepay) + } } if toRepay.Sign() <= 0 { From e41ad75776beb0dc09a089ee790996fa51470779 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Jul 2023 11:32:53 +0800 Subject: [PATCH 1253/1392] add httptesting pkg --- pkg/testing/httptesting/client.go | 68 +++++++++++++++++++ pkg/testing/httptesting/response.go | 54 +++++++++++++++ pkg/testing/httptesting/transport.go | 98 ++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 pkg/testing/httptesting/client.go create mode 100644 pkg/testing/httptesting/response.go create mode 100644 pkg/testing/httptesting/transport.go diff --git a/pkg/testing/httptesting/client.go b/pkg/testing/httptesting/client.go new file mode 100644 index 0000000000..3ff094f993 --- /dev/null +++ b/pkg/testing/httptesting/client.go @@ -0,0 +1,68 @@ +package httptesting + +import ( + "encoding/json" + "net/http" + "os" +) + +// Simplied client for testing that doesn't require multiple URLs + +type EchoSave struct { + // saveTo provides a way for tests to verify http.Request fields. + + // An http.Client's transport layer has only one method, so there's no way to + // return variables while adhering to it's interface. One solution is to use + // type casting where the caller must know the transport layer is actually + // of type "EchoSave". But a cleaner approach is to pass in the address of + // a local variable, and store the http.Request there. + + // Callers provide the address of a local variable, which is stored here. + saveTo **http.Request + content string + err error +} + +func (st *EchoSave) RoundTrip(req *http.Request) (*http.Response, error) { + if st.saveTo != nil { + // If the caller provided a local variable, update it with the latest http.Request + *st.saveTo = req + } + resp := BuildResponseString(http.StatusOK, st.content) + SetHeader(resp, "Content-Type", "application/json") + return resp, st.err +} + +func HttpClientFromFile(filename string) *http.Client { + rawBytes, err := os.ReadFile(filename) + transport := EchoSave{err: err, content: string(rawBytes)} + return &http.Client{Transport: &transport} +} + +func HttpClientWithContent(content string) *http.Client { + transport := EchoSave{content: content} + return &http.Client{Transport: &transport} +} + +func HttpClientWithError(err error) *http.Client { + transport := EchoSave{err: err} + return &http.Client{Transport: &transport} +} + +func HttpClientWithJson(jsonData interface{}) *http.Client { + jsonBytes, err := json.Marshal(jsonData) + transport := EchoSave{err: err, content: string(jsonBytes)} + return &http.Client{Transport: &transport} +} + +// "Saver" refers to saving the *http.Request in a local variable provided by the caller. +func HttpClientSaver(saved **http.Request, content string) *http.Client { + transport := EchoSave{saveTo: saved, content: content} + return &http.Client{Transport: &transport} +} + +func HttpClientSaverWithJson(saved **http.Request, jsonData interface{}) *http.Client { + jsonBytes, err := json.Marshal(jsonData) + transport := EchoSave{saveTo: saved, err: err, content: string(jsonBytes)} + return &http.Client{Transport: &transport} +} diff --git a/pkg/testing/httptesting/response.go b/pkg/testing/httptesting/response.go new file mode 100644 index 0000000000..77b7f21fc8 --- /dev/null +++ b/pkg/testing/httptesting/response.go @@ -0,0 +1,54 @@ +package httptesting + +import ( + "bytes" + "encoding/json" + "io" + "net/http" +) + +func BuildResponse(code int, payload []byte) *http.Response { + return &http.Response{ + StatusCode: code, + Body: io.NopCloser(bytes.NewBuffer(payload)), + ContentLength: int64(len(payload)), + } +} + +func BuildResponseString(code int, payload string) *http.Response { + b := []byte(payload) + return &http.Response{ + StatusCode: code, + Body: io.NopCloser( + bytes.NewBuffer(b), + ), + ContentLength: int64(len(b)), + } +} + +func BuildResponseJson(code int, payload interface{}) *http.Response { + data, err := json.Marshal(payload) + if err != nil { + return BuildResponseString(http.StatusInternalServerError, `{error: "httptesting.MockTransport error calling json.Marshal()"}`) + } + + resp := BuildResponse(code, data) + resp.Header = http.Header{} + resp.Header.Set("Content-Type", "application/json") + return resp +} + +func SetHeader(resp *http.Response, name string, value string) *http.Response { + if resp.Header == nil { + resp.Header = http.Header{} + } + resp.Header.Set(name, value) + return resp +} + +func DeleteHeader(resp *http.Response, name string) *http.Response { + if resp.Header != nil { + resp.Header.Del(name) + } + return resp +} diff --git a/pkg/testing/httptesting/transport.go b/pkg/testing/httptesting/transport.go new file mode 100644 index 0000000000..f4ed47d9d3 --- /dev/null +++ b/pkg/testing/httptesting/transport.go @@ -0,0 +1,98 @@ +package httptesting + +import ( + "net/http" + "strings" + + "github.com/pkg/errors" +) + +type RoundTripFunc func(req *http.Request) (*http.Response, error) + +type MockTransport struct { + getHandlers map[string]RoundTripFunc + postHandlers map[string]RoundTripFunc + deleteHandlers map[string]RoundTripFunc + putHandlers map[string]RoundTripFunc +} + +func (transport *MockTransport) GET(path string, f RoundTripFunc) { + if transport.getHandlers == nil { + transport.getHandlers = make(map[string]RoundTripFunc) + } + + transport.getHandlers[path] = f +} + +func (transport *MockTransport) POST(path string, f RoundTripFunc) { + if transport.postHandlers == nil { + transport.postHandlers = make(map[string]RoundTripFunc) + } + + transport.postHandlers[path] = f +} + +func (transport *MockTransport) DELETE(path string, f RoundTripFunc) { + if transport.deleteHandlers == nil { + transport.deleteHandlers = make(map[string]RoundTripFunc) + } + + transport.deleteHandlers[path] = f +} + +func (transport *MockTransport) PUT(path string, f RoundTripFunc) { + if transport.putHandlers == nil { + transport.putHandlers = make(map[string]RoundTripFunc) + } + + transport.putHandlers[path] = f +} + +// Used for migration to MAX v3 api, where order cancel uses DELETE (MAX v2 api uses POST). +func (transport *MockTransport) PostOrDelete(isDelete bool, path string, f RoundTripFunc) { + if isDelete { + transport.DELETE(path, f) + } else { + transport.POST(path, f) + } +} + +func (transport *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + var handlers map[string]RoundTripFunc + + switch strings.ToUpper(req.Method) { + + case "GET": + handlers = transport.getHandlers + case "POST": + handlers = transport.postHandlers + case "DELETE": + handlers = transport.deleteHandlers + case "PUT": + handlers = transport.putHandlers + + default: + return nil, errors.Errorf("unsupported mock transport request method: %s", req.Method) + + } + + f, ok := handlers[req.URL.Path] + if !ok { + return nil, errors.Errorf("roundtrip mock to %s %s is not defined", req.Method, req.URL.Path) + } + + return f(req) +} + +func MockWithJsonReply(url string, rawData interface{}) *http.Client { + tripFunc := func(_ *http.Request) (*http.Response, error) { + return BuildResponseJson(http.StatusOK, rawData), nil + } + + transport := &MockTransport{} + transport.DELETE(url, tripFunc) + transport.GET(url, tripFunc) + transport.POST(url, tripFunc) + transport.PUT(url, tripFunc) + return &http.Client{Transport: transport} +} From f5feb72355c6deec2d5931456e9fd12a3501c857 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Jul 2023 13:35:08 +0800 Subject: [PATCH 1254/1392] max: add fee_discounted to Trade struct for RESTful api --- pkg/exchange/max/convert.go | 5 +---- pkg/exchange/max/maxapi/userdata.go | 4 ++-- .../maxapi/v3/get_wallet_trades_request_requestgen.go | 1 - pkg/exchange/max/maxapi/v3/trade.go | 10 +++++++++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 48b7687f77..3368ffc7bc 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -285,9 +285,6 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) { // skip trade ID that is the same. however this should not happen var side = toGlobalSideType(t.Side) - // trade time - mts := time.Unix(0, t.Timestamp*int64(time.Millisecond)) - return &types.Trade{ ID: t.ID, OrderID: t.OrderID, @@ -301,7 +298,7 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) { Fee: t.Fee, FeeCurrency: toGlobalCurrency(t.FeeCurrency), QuoteQuantity: t.Price.Mul(t.Volume), - Time: types.Time(mts), + Time: types.Time(t.Timestamp.Time()), }, nil } diff --git a/pkg/exchange/max/maxapi/userdata.go b/pkg/exchange/max/maxapi/userdata.go index 26ff6cf4be..03a06f8cc9 100644 --- a/pkg/exchange/max/maxapi/userdata.go +++ b/pkg/exchange/max/maxapi/userdata.go @@ -104,8 +104,8 @@ type TradeUpdate struct { FeeCurrency string `json:"fc"` FeeDiscounted bool `json:"fd"` - Timestamp int64 `json:"T"` - UpdateTime int64 `json:"TU"` + Timestamp types.MillisecondTimestamp `json:"T"` + UpdateTime int64 `json:"TU"` OrderID uint64 `json:"oi"` diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go index 647916103c..2baeeb611b 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/c9s/bbgo/pkg/exchange/max/maxapi" "net/url" "reflect" "regexp" diff --git a/pkg/exchange/max/maxapi/v3/trade.go b/pkg/exchange/max/maxapi/v3/trade.go index c9e975bb0b..1774e4ba57 100644 --- a/pkg/exchange/max/maxapi/v3/trade.go +++ b/pkg/exchange/max/maxapi/v3/trade.go @@ -5,6 +5,13 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +type Liquidity string + +const ( + LiquidityMaker = "maker" + LiquidityTaker = "taker" +) + type Trade struct { ID uint64 `json:"id" db:"exchange_id"` WalletType WalletType `json:"wallet_type,omitempty"` @@ -18,7 +25,8 @@ type Trade struct { OrderID uint64 `json:"order_id"` Fee fixedpoint.Value `json:"fee"` // float number as string FeeCurrency string `json:"fee_currency"` - Liquidity string `json:"liquidity"` + FeeDiscounted bool `json:"fee_discounted"` + Liquidity Liquidity `json:"liquidity"` SelfTradeBidFee fixedpoint.Value `json:"self_trade_bid_fee"` SelfTradeBidFeeCurrency string `json:"self_trade_bid_fee_currency"` SelfTradeBidOrderID uint64 `json:"self_trade_bid_order_id"` From 4de82ccdff302332d420e6c07c8b6d1e304aa3d3 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Jul 2023 13:37:31 +0800 Subject: [PATCH 1255/1392] max: use types.MillisecondTimestamp for UpdateTime field --- pkg/exchange/max/maxapi/userdata.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/exchange/max/maxapi/userdata.go b/pkg/exchange/max/maxapi/userdata.go index 03a06f8cc9..551d3d6852 100644 --- a/pkg/exchange/max/maxapi/userdata.go +++ b/pkg/exchange/max/maxapi/userdata.go @@ -105,7 +105,7 @@ type TradeUpdate struct { FeeDiscounted bool `json:"fd"` Timestamp types.MillisecondTimestamp `json:"T"` - UpdateTime int64 `json:"TU"` + UpdateTime types.MillisecondTimestamp `json:"TU"` OrderID uint64 `json:"oi"` From fcca3f6432b08b6d2230ffad626b80650c9152a2 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Jul 2023 13:40:10 +0800 Subject: [PATCH 1256/1392] types: add fee discounted field to the global trade struct --- pkg/exchange/max/convert.go | 1 + pkg/exchange/max/maxapi/userdata_test.go | 2 +- .../max/maxapi/v3/get_wallet_trades_request_requestgen.go | 2 +- pkg/types/trade.go | 6 ++++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 3368ffc7bc..4c578bde6b 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -297,6 +297,7 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) { IsMaker: t.Maker, Fee: t.Fee, FeeCurrency: toGlobalCurrency(t.FeeCurrency), + FeeDiscounted: t.FeeDiscounted, QuoteQuantity: t.Price.Mul(t.Volume), Time: types.Time(t.Timestamp.Time()), }, nil diff --git a/pkg/exchange/max/maxapi/userdata_test.go b/pkg/exchange/max/maxapi/userdata_test.go index 2967e16dfe..5c4880ce5e 100644 --- a/pkg/exchange/max/maxapi/userdata_test.go +++ b/pkg/exchange/max/maxapi/userdata_test.go @@ -39,7 +39,7 @@ func Test_parseTradeSnapshotEvent(t *testing.T) { assert.Equal(t, 1, len(evt.Trades)) assert.Equal(t, "bid", evt.Trades[0].Side) assert.Equal(t, "ethtwd", evt.Trades[0].Market) - assert.Equal(t, int64(1521726960357), evt.Trades[0].Timestamp) + assert.Equal(t, int64(1521726960357), evt.Trades[0].Timestamp.Time().UnixMilli()) assert.Equal(t, "3.2", evt.Trades[0].Fee.String()) assert.Equal(t, "twd", evt.Trades[0].FeeCurrency) } diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go index 2baeeb611b..ec7614c10e 100644 --- a/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go @@ -38,7 +38,7 @@ func (g *GetWalletTradesRequest) Limit(limit uint64) *GetWalletTradesRequest { return g } -func (g *GetWalletTradesRequest) WalletType(walletType max.WalletType) *GetWalletTradesRequest { +func (g *GetWalletTradesRequest) WalletType(walletType WalletType) *GetWalletTradesRequest { g.walletType = walletType return g } diff --git a/pkg/types/trade.go b/pkg/types/trade.go index 0db78b6efe..f80b864a27 100644 --- a/pkg/types/trade.go +++ b/pkg/types/trade.go @@ -67,6 +67,12 @@ type Trade struct { Fee fixedpoint.Value `json:"fee" db:"fee"` FeeCurrency string `json:"feeCurrency" db:"fee_currency"` + // FeeDiscounted is an optional field which indicates whether the trade is using the platform fee token for discount. + // When FeeDiscounted = true, means the fee is deducted outside the trade + // By default, it's set to false. + // This is only used by the MAX exchange + FeeDiscounted bool `json:"feeDiscounted" db:"-"` + IsMargin bool `json:"isMargin" db:"is_margin"` IsFutures bool `json:"isFutures" db:"is_futures"` IsIsolated bool `json:"isIsolated" db:"is_isolated"` From ef8d1c70461ffc1bec64cd914b5110d1e93ce870 Mon Sep 17 00:00:00 2001 From: Edwin Date: Mon, 24 Jul 2023 23:27:43 +0800 Subject: [PATCH 1257/1392] pkg/exchange: support QueryTickers API on bybit --- pkg/exchange/bybit/bybitapi/client.go | 11 +- pkg/exchange/bybit/bybitapi/client_test.go | 12 ++ .../bybit/bybitapi/get_tickers_request.go | 72 ++++++++ .../get_tickers_request_requestgen.go | 172 ++++++++++++++++++ pkg/exchange/bybit/convert.go | 36 ++-- pkg/exchange/bybit/convert_test.go | 70 +++++++ pkg/exchange/bybit/exchange.go | 69 +++++++ 7 files changed, 415 insertions(+), 27 deletions(-) create mode 100644 pkg/exchange/bybit/bybitapi/get_tickers_request.go create mode 100644 pkg/exchange/bybit/bybitapi/get_tickers_request_requestgen.go diff --git a/pkg/exchange/bybit/bybitapi/client.go b/pkg/exchange/bybit/bybitapi/client.go index aed96e4196..cfd8ac2bbf 100644 --- a/pkg/exchange/bybit/bybitapi/client.go +++ b/pkg/exchange/bybit/bybitapi/client.go @@ -157,9 +157,10 @@ sample: */ type APIResponse struct { - RetCode uint `json:"retCode"` - RetMsg string `json:"retMsg"` - Result json.RawMessage `json:"result"` - RetExtInfo json.RawMessage `json:"retExtInfo"` - Time types.MillisecondTimestamp `json:"time"` + RetCode uint `json:"retCode"` + RetMsg string `json:"retMsg"` + Result json.RawMessage `json:"result"` + RetExtInfo json.RawMessage `json:"retExtInfo"` + // Time is current timestamp (ms) + Time types.MillisecondTimestamp `json:"time"` } diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index bc89d60fc5..e18e78941b 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -45,4 +45,16 @@ func TestClient(t *testing.T) { assert.NoError(t, err) t.Logf("instrumentsInfo: %+v", instrumentsInfo) }) + + t.Run("GetTicker", func(t *testing.T) { + req := client.NewGetTickersRequest() + apiResp, err := req.Symbol("BTCUSDT").Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %+v", apiResp) + + req = client.NewGetTickersRequest() + tickers, err := req.Symbol("BTCUSDT").DoWithResponseTime(ctx) + assert.NoError(t, err) + t.Logf("tickers: %+v", tickers) + }) } diff --git a/pkg/exchange/bybit/bybitapi/get_tickers_request.go b/pkg/exchange/bybit/bybitapi/get_tickers_request.go new file mode 100644 index 0000000000..2fa79a38f0 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_tickers_request.go @@ -0,0 +1,72 @@ +package bybitapi + +import ( + "context" + "encoding/json" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +type Tickers struct { + Category Category `json:"category"` + List []Ticker `json:"list"` + + // ClosedTime is current timestamp (ms). This value is obtained from outside APIResponse. + ClosedTime types.MillisecondTimestamp +} + +type Ticker struct { + Symbol string `json:"symbol"` + Bid1Price fixedpoint.Value `json:"bid1Price"` + Bid1Size fixedpoint.Value `json:"bid1Size"` + Ask1Price fixedpoint.Value `json:"ask1Price"` + Ask1Size fixedpoint.Value `json:"ask1Size"` + LastPrice fixedpoint.Value `json:"lastPrice"` + PrevPrice24H fixedpoint.Value `json:"prevPrice24h"` + Price24HPcnt fixedpoint.Value `json:"price24hPcnt"` + HighPrice24H fixedpoint.Value `json:"highPrice24h"` + LowPrice24H fixedpoint.Value `json:"lowPrice24h"` + Turnover24H fixedpoint.Value `json:"turnover24h"` + Volume24H fixedpoint.Value `json:"volume24h"` + UsdIndexPrice fixedpoint.Value `json:"usdIndexPrice"` +} + +// GetTickersRequest without **-responseDataType .InstrumentsInfo** in generation command, because the caller +// needs the APIResponse.Time. We implemented the DoWithResponseTime to handle this. +// +//go:generate GetRequest -url "/v5/market/tickers" -type GetTickersRequest +type GetTickersRequest struct { + client requestgen.APIClient + + category Category `param:"category,query" validValues:"spot"` + symbol *string `param:"symbol,query"` +} + +func (c *RestClient) NewGetTickersRequest() *GetTickersRequest { + return &GetTickersRequest{ + client: c, + category: CategorySpot, + } +} + +func (g *GetTickersRequest) DoWithResponseTime(ctx context.Context) (*Tickers, error) { + resp, err := g.Do(ctx) + if err != nil { + return nil, err + } + + var data Tickers + if err := json.Unmarshal(resp.Result, &data); err != nil { + return nil, err + } + + // Our types.Ticker requires the closed time, but this API does not provide it. This API returns the Tickers of the + // past 24 hours, so in terms of closed time, it is the current time, so fill it in Tickers.ClosedTime. + data.ClosedTime = resp.Time + return &data, nil +} diff --git a/pkg/exchange/bybit/bybitapi/get_tickers_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_tickers_request_requestgen.go new file mode 100644 index 0000000000..0f85973d8d --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_tickers_request_requestgen.go @@ -0,0 +1,172 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/market/tickers -type GetTickersRequest"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetTickersRequest) Category(category Category) *GetTickersRequest { + g.category = category + return g +} + +func (g *GetTickersRequest) Symbol(symbol string) *GetTickersRequest { + g.symbol = &symbol + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetTickersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check category field -> json key category + category := g.category + + // TEMPLATE check-valid-values + switch category { + case "spot": + params["category"] = category + + default: + return nil, fmt.Errorf("category value %v is invalid", category) + + } + // END TEMPLATE check-valid-values + + // assign parameter of category + params["category"] = category + // check symbol field -> json key symbol + if g.symbol != nil { + symbol := *g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + } else { + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetTickersRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetTickersRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetTickersRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetTickersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetTickersRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetTickersRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetTickersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetTickersRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetTickersRequest) Do(ctx context.Context) (*APIResponse, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v5/market/tickers" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index 0b37d7adc3..e83f31d31a 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -2,34 +2,13 @@ package bybit import ( "math" + "time" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" "github.com/c9s/bbgo/pkg/types" ) func toGlobalMarket(m bybitapi.Instrument) types.Market { - // sample: - //Symbol: BTCUSDT - //BaseCoin: BTC - //QuoteCoin: USDT - //Innovation: 0 - //Status: Trading - //MarginTrading: both - // - //LotSizeFilter: - //{ - // BasePrecision: 0.000001 - // QuotePrecision: 0.00000001 - // MinOrderQty: 0.000048 - // MaxOrderQty: 71.73956243 - // MinOrderAmt: 1 - // MaxOrderAmt: 2000000 - //} - // - //PriceFilter: - //{ - // TickSize: 0.01 - //} return types.Market{ Symbol: m.Symbol, LocalSymbol: m.Symbol, @@ -51,3 +30,16 @@ func toGlobalMarket(m bybitapi.Instrument) types.Market { TickSize: m.PriceFilter.TickSize, } } + +func toGlobalTicker(stats bybitapi.Ticker, time time.Time) types.Ticker { + return types.Ticker{ + Volume: stats.Volume24H, + Last: stats.LastPrice, + Open: stats.PrevPrice24H, // Market price 24 hours ago + High: stats.HighPrice24H, + Low: stats.LowPrice24H, + Buy: stats.Bid1Price, + Sell: stats.Ask1Price, + Time: time, + } +} diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index 22325b9536..6ec60cfe0a 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -3,6 +3,7 @@ package bybit import ( "math" "testing" + "time" "github.com/stretchr/testify/assert" @@ -12,6 +13,26 @@ import ( ) func TestToGlobalMarket(t *testing.T) { + // sample: + //{ + // "Symbol": "BTCUSDT", + // "BaseCoin": "BTC", + // "QuoteCoin": "USDT", + // "Innovation": 0, + // "Status": "Trading", + // "MarginTrading": "both", + // "LotSizeFilter": { + // "BasePrecision": 0.000001, + // "QuotePrecision": 0.00000001, + // "MinOrderQty": 0.000048, + // "MaxOrderQty": 71.73956243, + // "MinOrderAmt": 1, + // "MaxOrderAmt": 2000000 + // }, + // "PriceFilter": { + // "TickSize": 0.01 + // } + //} inst := bybitapi.Instrument{ Symbol: "BTCUSDT", BaseCoin: "BTC", @@ -60,3 +81,52 @@ func TestToGlobalMarket(t *testing.T) { assert.Equal(t, toGlobalMarket(inst), exp) } + +func TestToGlobalTicker(t *testing.T) { + // sample + //{ + // "symbol": "BTCUSDT", + // "bid1Price": "28995.98", + // "bid1Size": "4.741552", + // "ask1Price": "28995.99", + // "ask1Size": "0.16075", + // "lastPrice": "28994", + // "prevPrice24h": "29900", + // "price24hPcnt": "-0.0303", + // "highPrice24h": "30344.78", + // "lowPrice24h": "28948.87", + // "turnover24h": "184705500.13172874", + // "volume24h": "6240.807096", + // "usdIndexPrice": "28977.82001643" + //} + ticker := bybitapi.Ticker{ + Symbol: "BTCUSDT", + Bid1Price: fixedpoint.NewFromFloat(28995.98), + Bid1Size: fixedpoint.NewFromFloat(4.741552), + Ask1Price: fixedpoint.NewFromFloat(28995.99), + Ask1Size: fixedpoint.NewFromFloat(0.16075), + LastPrice: fixedpoint.NewFromFloat(28994), + PrevPrice24H: fixedpoint.NewFromFloat(29900), + Price24HPcnt: fixedpoint.NewFromFloat(-0.0303), + HighPrice24H: fixedpoint.NewFromFloat(30344.78), + LowPrice24H: fixedpoint.NewFromFloat(28948.87), + Turnover24H: fixedpoint.NewFromFloat(184705500.13172874), + Volume24H: fixedpoint.NewFromFloat(6240.807096), + UsdIndexPrice: fixedpoint.NewFromFloat(28977.82001643), + } + + timeNow := time.Now() + + exp := types.Ticker{ + Time: timeNow, + Volume: ticker.Volume24H, + Last: ticker.LastPrice, + Open: ticker.PrevPrice24H, + High: ticker.HighPrice24H, + Low: ticker.LowPrice24H, + Buy: ticker.Bid1Price, + Sell: ticker.Ask1Price, + } + + assert.Equal(t, toGlobalTicker(ticker, timeNow), exp) +} diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 5abd5c6453..9d8a01059d 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -2,13 +2,23 @@ package bybit import ( "context" + "fmt" + "time" "github.com/sirupsen/logrus" + "golang.org/x/time/rate" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" "github.com/c9s/bbgo/pkg/types" ) +// https://bybit-exchange.github.io/docs/zh-TW/v5/rate-limit +// sharedRateLimiter indicates that the API belongs to the public API. +// +// The default order limiter apply 2 requests per second and a 2 initial bucket +// this includes QueryMarkets, QueryTicker +var sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2) + var log = logrus.WithFields(logrus.Fields{ "exchange": "bybit", }) @@ -47,8 +57,14 @@ func (e *Exchange) PlatformFeeCurrency() string { } func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { + if err := sharedRateLimiter.Wait(ctx); err != nil { + log.WithError(err).Errorf("markets rate limiter wait error") + return nil, err + } + instruments, err := e.client.NewGetInstrumentsInfoRequest().Do(ctx) if err != nil { + log.Warnf("failed to query instruments, err: %v", err) return nil, err } @@ -59,3 +75,56 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { return marketMap, nil } + +func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { + if err := sharedRateLimiter.Wait(ctx); err != nil { + log.WithError(err).Errorf("ticker rate limiter wait error") + return nil, err + } + + s, err := e.client.NewGetTickersRequest().Symbol(symbol).DoWithResponseTime(ctx) + if err != nil { + log.Warnf("failed to get tickers, symbol: %s, err: %v", symbol, err) + return nil, err + } + + if len(s.List) != 1 { + log.Warnf("unexpected ticker length, exp: 1, got: %d", len(s.List)) + return nil, fmt.Errorf("unexpected ticker lenght, exp:1, got:%d", len(s.List)) + } + + ticker := toGlobalTicker(s.List[0], s.ClosedTime.Time()) + return &ticker, nil +} + +func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[string]types.Ticker, error) { + tickers := map[string]types.Ticker{} + if len(symbols) > 0 { + for _, s := range symbols { + t, err := e.QueryTicker(ctx, s) + if err != nil { + return nil, err + } + + tickers[s] = *t + } + + return tickers, nil + } + + if err := sharedRateLimiter.Wait(ctx); err != nil { + log.WithError(err).Errorf("ticker rate limiter wait error") + return nil, err + } + allTickers, err := e.client.NewGetTickersRequest().DoWithResponseTime(ctx) + if err != nil { + log.Warnf("failed to get tickers, err: %v", err) + return nil, err + } + + for _, s := range allTickers.List { + tickers[s.Symbol] = toGlobalTicker(s, allTickers.ClosedTime.Time()) + } + + return tickers, nil +} From b71030c5db033df2e394514d2b01b7e8ec4faa90 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 25 Jul 2023 15:09:57 +0800 Subject: [PATCH 1258/1392] pkg: return err if rate limit err --- pkg/exchange/binance/cancel_replace.go | 1 + pkg/exchange/binance/exchange.go | 9 ++++++--- pkg/interact/telegram.go | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/binance/cancel_replace.go b/pkg/exchange/binance/cancel_replace.go index c993c66d03..e98090a489 100644 --- a/pkg/exchange/binance/cancel_replace.go +++ b/pkg/exchange/binance/cancel_replace.go @@ -12,6 +12,7 @@ import ( func (e *Exchange) CancelReplace(ctx context.Context, cancelReplaceMode types.CancelReplaceModeType, o types.Order) (*types.Order, error) { if err := orderLimiter.Wait(ctx); err != nil { log.WithError(err).Errorf("order rate limiter wait error") + return nil, err } if e.IsFutures || e.IsMargin { diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 43d7554afe..9f8534fc8b 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -764,8 +764,9 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, } */ - if err := orderLimiter.Wait(ctx); err != nil { + if err = orderLimiter.Wait(ctx); err != nil { log.WithError(err).Errorf("order rate limiter wait error") + return nil, err } log.Infof("querying closed orders %s from %s <=> %s ...", symbol, since, until) @@ -822,8 +823,9 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, } func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err error) { - if err := orderLimiter.Wait(ctx); err != nil { + if err = orderLimiter.Wait(ctx); err != nil { log.WithError(err).Errorf("order rate limiter wait error") + return err } if e.IsFutures { @@ -1086,8 +1088,9 @@ func (e *Exchange) submitSpotOrder(ctx context.Context, order types.SubmitOrder) } func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) { - if err := orderLimiter.Wait(ctx); err != nil { + if err = orderLimiter.Wait(ctx); err != nil { log.WithError(err).Errorf("order rate limiter wait error") + return nil, err } if e.IsMargin { diff --git a/pkg/interact/telegram.go b/pkg/interact/telegram.go index c2a45a0025..4635cbe39a 100644 --- a/pkg/interact/telegram.go +++ b/pkg/interact/telegram.go @@ -73,6 +73,7 @@ func (r *TelegramReply) Send(message string) { for _, split := range splits { if err := sendLimiter.Wait(ctx); err != nil { log.WithError(err).Errorf("telegram send limit exceeded") + return } checkSendErr(r.bot.Send(r.session.Chat, split)) } @@ -175,6 +176,7 @@ func (tm *Telegram) Start(ctx context.Context) { for i, split := range splits { if err := sendLimiter.Wait(ctx); err != nil { log.WithError(err).Errorf("telegram send limit exceeded") + return } if i == len(splits)-1 { // only set menu on the last message From 1d24af13a8ddc4df0a132a80737ce2176cf4a1aa Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 25 Jul 2023 17:50:48 +0800 Subject: [PATCH 1259/1392] core: document order store options --- pkg/core/orderstore.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pkg/core/orderstore.go b/pkg/core/orderstore.go index 3af1ab81e7..b639e8ce10 100644 --- a/pkg/core/orderstore.go +++ b/pkg/core/orderstore.go @@ -11,11 +11,19 @@ type OrderStore struct { mu sync.Mutex orders map[uint64]types.Order - Symbol string + Symbol string + + // RemoveCancelled removes the canceled order when receiving a cancel order update event + // It also removes the order even if it's partially filled + // by default, only 0 filled canceled order will be removed. RemoveCancelled bool - RemoveFilled bool - AddOrderUpdate bool - C chan types.Order + + // RemoveFilled removes the fully filled order when receiving a filled order update event + RemoveFilled bool + + // AddOrderUpdate adds the order into the store when receiving an order update when the order does not exist in the current store. + AddOrderUpdate bool + C chan types.Order } func NewOrderStore(symbol string) *OrderStore { From 6d4deb54cc93812f6e38e612f75925cd5106468a Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 25 Jul 2023 21:30:49 +0800 Subject: [PATCH 1260/1392] pkg/exchange: add QueryOpenOrders API for bybit --- pkg/exchange/bybit/bybitapi/client.go | 6 +- pkg/exchange/bybit/bybitapi/client_test.go | 20 ++ .../bybit/bybitapi/get_open_order_request.go | 83 +++++ .../get_open_orders_request_requestgen.go | 291 ++++++++++++++++++ pkg/exchange/bybit/bybitapi/types.go | 75 +++++ pkg/exchange/bybit/convert.go | 127 ++++++++ pkg/exchange/bybit/convert_test.go | 226 ++++++++++++++ pkg/exchange/bybit/exchange.go | 49 ++- 8 files changed, 870 insertions(+), 7 deletions(-) create mode 100644 pkg/exchange/bybit/bybitapi/get_open_order_request.go create mode 100644 pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go diff --git a/pkg/exchange/bybit/bybitapi/client.go b/pkg/exchange/bybit/bybitapi/client.go index cfd8ac2bbf..9184f9f5bb 100644 --- a/pkg/exchange/bybit/bybitapi/client.go +++ b/pkg/exchange/bybit/bybitapi/client.go @@ -5,7 +5,7 @@ import ( "context" "crypto/hmac" "crypto/sha256" - "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "net/http" @@ -23,7 +23,7 @@ const defaultHTTPTimeout = time.Second * 15 const RestBaseURL = "https://api.bybit.com" // defaultRequestWindowMilliseconds specify how long an HTTP request is valid. It is also used to prevent replay attacks. -var defaultRequestWindowMilliseconds = fmt.Sprintf("%d", time.Millisecond*5000) +var defaultRequestWindowMilliseconds = fmt.Sprintf("%d", 5*time.Second.Milliseconds()) type RestClient struct { requestgen.BaseAPIClient @@ -124,7 +124,7 @@ func sign(payload string, secret string) string { return "" } - return base64.StdEncoding.EncodeToString(sig.Sum(nil)) + return hex.EncodeToString(sig.Sum(nil)) } func castPayload(payload interface{}) ([]byte, error) { diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index e18e78941b..02433fdd9e 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -57,4 +57,24 @@ func TestClient(t *testing.T) { assert.NoError(t, err) t.Logf("tickers: %+v", tickers) }) + + t.Run("GetOpenOrderRequest", func(t *testing.T) { + cursor := "" + for { + req := client.NewGetOpenOrderRequest().Limit(1) + if len(cursor) != 0 { + req = req.Cursor(cursor) + } + openOrders, err := req.Do(ctx) + assert.NoError(t, err) + + for _, o := range openOrders.List { + t.Logf("openOrders: %+v", o) + } + if len(openOrders.NextPageCursor) == 0 { + break + } + cursor = openOrders.NextPageCursor + } + }) } diff --git a/pkg/exchange/bybit/bybitapi/get_open_order_request.go b/pkg/exchange/bybit/bybitapi/get_open_order_request.go new file mode 100644 index 0000000000..e56ea01c10 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_open_order_request.go @@ -0,0 +1,83 @@ +package bybitapi + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +type OpenOrdersResponse struct { + List []OpenOrder `json:"list"` + NextPageCursor string `json:"nextPageCursor"` + Category string `json:"category"` +} + +type OpenOrder struct { + OrderId string `json:"orderId"` + OrderLinkId string `json:"orderLinkId"` + BlockTradeId string `json:"blockTradeId"` + Symbol string `json:"symbol"` + Price fixedpoint.Value `json:"price"` + Qty fixedpoint.Value `json:"qty"` + Side Side `json:"side"` + IsLeverage string `json:"isLeverage"` + PositionIdx int `json:"positionIdx"` + OrderStatus OrderStatus `json:"orderStatus"` + CancelType string `json:"cancelType"` + RejectReason string `json:"rejectReason"` + AvgPrice fixedpoint.Value `json:"avgPrice"` + LeavesQty fixedpoint.Value `json:"leavesQty"` + LeavesValue fixedpoint.Value `json:"leavesValue"` + CumExecQty fixedpoint.Value `json:"cumExecQty"` + CumExecValue fixedpoint.Value `json:"cumExecValue"` + CumExecFee fixedpoint.Value `json:"cumExecFee"` + TimeInForce TimeInForce `json:"timeInForce"` + OrderType OrderType `json:"orderType"` + StopOrderType string `json:"stopOrderType"` + OrderIv string `json:"orderIv"` + TriggerPrice fixedpoint.Value `json:"triggerPrice"` + TakeProfit fixedpoint.Value `json:"takeProfit"` + StopLoss fixedpoint.Value `json:"stopLoss"` + TpTriggerBy string `json:"tpTriggerBy"` + SlTriggerBy string `json:"slTriggerBy"` + TriggerDirection int `json:"triggerDirection"` + TriggerBy string `json:"triggerBy"` + LastPriceOnCreated string `json:"lastPriceOnCreated"` + ReduceOnly bool `json:"reduceOnly"` + CloseOnTrigger bool `json:"closeOnTrigger"` + SmpType string `json:"smpType"` + SmpGroup int `json:"smpGroup"` + SmpOrderId string `json:"smpOrderId"` + TpslMode string `json:"tpslMode"` + TpLimitPrice string `json:"tpLimitPrice"` + SlLimitPrice string `json:"slLimitPrice"` + PlaceType string `json:"placeType"` + CreatedTime types.MillisecondTimestamp `json:"createdTime"` + UpdatedTime types.MillisecondTimestamp `json:"updatedTime"` +} + +//go:generate GetRequest -url "/v5/order/realtime" -type GetOpenOrdersRequest -responseDataType .OpenOrdersResponse +type GetOpenOrdersRequest struct { + client requestgen.AuthenticatedAPIClient + + category Category `param:"category,query" validValues:"spot"` + symbol *string `param:"symbol,query"` + baseCoin *string `param:"baseCoin,query"` + settleCoin *string `param:"settleCoin,query"` + orderId *string `param:"orderId,query"` + orderLinkId *string `param:"orderLinkId,query"` + openOnly *OpenOnly `param:"openOnly,query" validValues:"0"` + orderFilter *string `param:"orderFilter,query"` + limit *uint64 `param:"limit,query"` + cursor *string `param:"cursor,query"` +} + +func (c *RestClient) NewGetOpenOrderRequest() *GetOpenOrdersRequest { + return &GetOpenOrdersRequest{ + client: c, + category: CategorySpot, + } +} diff --git a/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go new file mode 100644 index 0000000000..d68c31f4d5 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go @@ -0,0 +1,291 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/order/realtime -type GetOpenOrdersRequest -responseDataType .OpenOrdersResponse"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetOpenOrdersRequest) Category(category Category) *GetOpenOrdersRequest { + g.category = category + return g +} + +func (g *GetOpenOrdersRequest) Symbol(symbol string) *GetOpenOrdersRequest { + g.symbol = &symbol + return g +} + +func (g *GetOpenOrdersRequest) BaseCoin(baseCoin string) *GetOpenOrdersRequest { + g.baseCoin = &baseCoin + return g +} + +func (g *GetOpenOrdersRequest) SettleCoin(settleCoin string) *GetOpenOrdersRequest { + g.settleCoin = &settleCoin + return g +} + +func (g *GetOpenOrdersRequest) OrderId(orderId string) *GetOpenOrdersRequest { + g.orderId = &orderId + return g +} + +func (g *GetOpenOrdersRequest) OrderLinkId(orderLinkId string) *GetOpenOrdersRequest { + g.orderLinkId = &orderLinkId + return g +} + +func (g *GetOpenOrdersRequest) OpenOnly(openOnly OpenOnly) *GetOpenOrdersRequest { + g.openOnly = &openOnly + return g +} + +func (g *GetOpenOrdersRequest) OrderFilter(orderFilter string) *GetOpenOrdersRequest { + g.orderFilter = &orderFilter + return g +} + +func (g *GetOpenOrdersRequest) Limit(limit uint64) *GetOpenOrdersRequest { + g.limit = &limit + return g +} + +func (g *GetOpenOrdersRequest) Cursor(cursor string) *GetOpenOrdersRequest { + g.cursor = &cursor + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetOpenOrdersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check category field -> json key category + category := g.category + + // TEMPLATE check-valid-values + switch category { + case "spot": + params["category"] = category + + default: + return nil, fmt.Errorf("category value %v is invalid", category) + + } + // END TEMPLATE check-valid-values + + // assign parameter of category + params["category"] = category + // check symbol field -> json key symbol + if g.symbol != nil { + symbol := *g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + } else { + } + // check baseCoin field -> json key baseCoin + if g.baseCoin != nil { + baseCoin := *g.baseCoin + + // assign parameter of baseCoin + params["baseCoin"] = baseCoin + } else { + } + // check settleCoin field -> json key settleCoin + if g.settleCoin != nil { + settleCoin := *g.settleCoin + + // assign parameter of settleCoin + params["settleCoin"] = settleCoin + } else { + } + // check orderId field -> json key orderId + if g.orderId != nil { + orderId := *g.orderId + + // assign parameter of orderId + params["orderId"] = orderId + } else { + } + // check orderLinkId field -> json key orderLinkId + if g.orderLinkId != nil { + orderLinkId := *g.orderLinkId + + // assign parameter of orderLinkId + params["orderLinkId"] = orderLinkId + } else { + } + // check openOnly field -> json key openOnly + if g.openOnly != nil { + openOnly := *g.openOnly + + // TEMPLATE check-valid-values + switch openOnly { + case OpenOnlyOrder: + params["openOnly"] = openOnly + + default: + return nil, fmt.Errorf("openOnly value %v is invalid", openOnly) + + } + // END TEMPLATE check-valid-values + + // assign parameter of openOnly + params["openOnly"] = openOnly + } else { + } + // check orderFilter field -> json key orderFilter + if g.orderFilter != nil { + orderFilter := *g.orderFilter + + // assign parameter of orderFilter + params["orderFilter"] = orderFilter + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check cursor field -> json key cursor + if g.cursor != nil { + cursor := *g.cursor + + // assign parameter of cursor + params["cursor"] = cursor + } else { + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetOpenOrdersRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetOpenOrdersRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetOpenOrdersRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetOpenOrdersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetOpenOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetOpenOrdersRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetOpenOrdersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetOpenOrdersRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OpenOrdersResponse, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v5/order/realtime" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data OpenOrdersResponse + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bybit/bybitapi/types.go b/pkg/exchange/bybit/bybitapi/types.go index d58b31c333..7685d54253 100644 --- a/pkg/exchange/bybit/bybitapi/types.go +++ b/pkg/exchange/bybit/bybitapi/types.go @@ -12,3 +12,78 @@ const ( // StatusTrading is only include the "Trading" status for `spot` category. StatusTrading Status = "Trading" ) + +type OpenOnly int + +const ( + OpenOnlyOrder OpenOnly = 0 +) + +type Side string + +const ( + SideBuy Side = "Buy" + SideSell Side = "Sell" +) + +type OrderStatus string + +const ( + // OrderStatusCreated order has been accepted by the system but not yet put through the matching engine + OrderStatusCreated OrderStatus = "Created" + // OrderStatusNew is order has been placed successfully. + OrderStatusNew OrderStatus = "New" + OrderStatusRejected OrderStatus = "Rejected" + OrderStatusPartiallyFilled OrderStatus = "PartiallyFilled" + OrderStatusPartiallyFilledCanceled OrderStatus = "PartiallyFilledCanceled" + OrderStatusFilled OrderStatus = "Filled" + OrderStatusCancelled OrderStatus = "Cancelled" + + // Following statuses is conditional orders. Once you place conditional orders, it will be in untriggered status. + // Untriggered -> Triggered -> New + // Once the trigger price reached, order status will be moved to triggered + // Singe BBGO not support Untriggered/Triggered, so comment it. + // + // OrderStatusUntriggered means that the order not triggered + //OrderStatusUntriggered OrderStatus = "Untriggered" + //// OrderStatusTriggered means that the order has been triggered + //OrderStatusTriggered OrderStatus = "Triggered" + + // Following statuses is stop orders + // OrderStatusDeactivated is an order status for stopOrders. + //e.g. when you place a conditional order, then you cancel it, this order status is "Deactivated" + OrderStatusDeactivated OrderStatus = "Deactivated" + + // OrderStatusActive order has been triggered and the new active order has been successfully placed. Is the final + // state of a successful conditional order + OrderStatusActive OrderStatus = "Active" +) + +var ( + AllOrderStatuses = []OrderStatus{ + OrderStatusCreated, + OrderStatusNew, + OrderStatusRejected, + OrderStatusPartiallyFilled, + OrderStatusPartiallyFilledCanceled, + OrderStatusFilled, + OrderStatusCancelled, + OrderStatusDeactivated, + OrderStatusActive, + } +) + +type OrderType string + +const ( + OrderTypeMarket OrderType = "Market" + OrderTypeLimit OrderType = "Limit" +) + +type TimeInForce string + +const ( + TimeInForceGTC TimeInForce = "GTC" + TimeInForceIOC TimeInForce = "IOC" + TimeInForceFOK TimeInForce = "FOK" +) diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index e83f31d31a..5a2963fa26 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -1,6 +1,8 @@ package bybit import ( + "fmt" + "hash/fnv" "math" "time" @@ -43,3 +45,128 @@ func toGlobalTicker(stats bybitapi.Ticker, time time.Time) types.Ticker { Time: time, } } + +func toGlobalOrder(order bybitapi.OpenOrder) (*types.Order, error) { + side, err := toGlobalSideType(order.Side) + if err != nil { + return nil, err + } + orderType, err := toGlobalOrderType(order.OrderType) + if err != nil { + return nil, err + } + timeInForce, err := toGlobalTimeInForce(order.TimeInForce) + if err != nil { + return nil, err + } + status, err := toGlobalOrderStatus(order.OrderStatus) + if err != nil { + return nil, err + } + working, err := isWorking(order.OrderStatus) + if err != nil { + return nil, err + } + + return &types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: order.OrderLinkId, + Symbol: order.Symbol, + Side: side, + Type: orderType, + Quantity: order.Qty, + Price: order.Price, + TimeInForce: timeInForce, + }, + Exchange: types.ExchangeBybit, + OrderID: hashStringID(order.OrderId), + UUID: order.OrderId, + Status: status, + ExecutedQuantity: order.CumExecQty, + IsWorking: working, + CreationTime: types.Time(order.CreatedTime.Time()), + UpdateTime: types.Time(order.UpdatedTime.Time()), + }, nil +} + +func toGlobalSideType(side bybitapi.Side) (types.SideType, error) { + switch side { + case bybitapi.SideBuy: + return types.SideTypeBuy, nil + + case bybitapi.SideSell: + return types.SideTypeSell, nil + + default: + return types.SideType(side), fmt.Errorf("unexpected side: %s", side) + } +} + +func toGlobalOrderType(s bybitapi.OrderType) (types.OrderType, error) { + switch s { + case bybitapi.OrderTypeMarket: + return types.OrderTypeMarket, nil + + case bybitapi.OrderTypeLimit: + return types.OrderTypeLimit, nil + + default: + return types.OrderType(s), fmt.Errorf("unexpected order type: %s", s) + } +} + +func toGlobalTimeInForce(force bybitapi.TimeInForce) (types.TimeInForce, error) { + switch force { + case bybitapi.TimeInForceGTC: + return types.TimeInForceGTC, nil + + case bybitapi.TimeInForceIOC: + return types.TimeInForceIOC, nil + + case bybitapi.TimeInForceFOK: + return types.TimeInForceFOK, nil + + default: + return types.TimeInForce(force), fmt.Errorf("unexpected timeInForce type: %s", force) + } +} + +func toGlobalOrderStatus(status bybitapi.OrderStatus) (types.OrderStatus, error) { + switch status { + case bybitapi.OrderStatusCreated, + bybitapi.OrderStatusNew, + bybitapi.OrderStatusActive: + return types.OrderStatusNew, nil + + case bybitapi.OrderStatusFilled: + return types.OrderStatusFilled, nil + + case bybitapi.OrderStatusPartiallyFilled: + return types.OrderStatusPartiallyFilled, nil + + case bybitapi.OrderStatusCancelled, + bybitapi.OrderStatusPartiallyFilledCanceled, + bybitapi.OrderStatusDeactivated: + return types.OrderStatusCanceled, nil + + case bybitapi.OrderStatusRejected: + return types.OrderStatusRejected, nil + + default: + // following not supported + // bybitapi.OrderStatusUntriggered + // bybitapi.OrderStatusTriggered + return types.OrderStatus(status), fmt.Errorf("unexpected order status: %s", status) + } +} + +func hashStringID(s string) uint64 { + h := fnv.New64a() + h.Write([]byte(s)) + return h.Sum64() +} + +func isWorking(status bybitapi.OrderStatus) (bool, error) { + s, err := toGlobalOrderStatus(status) + return s == types.OrderStatusNew || s == types.OrderStatusPartiallyFilled, err +} diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index 6ec60cfe0a..55e05d692e 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -130,3 +130,229 @@ func TestToGlobalTicker(t *testing.T) { assert.Equal(t, toGlobalTicker(ticker, timeNow), exp) } + +func TestToGlobalOrder(t *testing.T) { + // sample: partialFilled + //{ + // "OrderId": 1472539279335923200, + // "OrderLinkId": 1690276361150, + // "BlockTradeId": null, + // "Symbol": "DOTUSDT", + // "Price": 7.278, + // "Qty": 0.8, + // "Side": "Sell", + // "IsLeverage": 0, + // "PositionIdx": 0, + // "OrderStatus": "PartiallyFilled", + // "CancelType": "UNKNOWN", + // "RejectReason": null, + // "AvgPrice": 7.278, + // "LeavesQty": 0, + // "LeavesValue": 0, + // "CumExecQty": 0.5, + // "CumExecValue": 0, + // "CumExecFee": 0, + // "TimeInForce": "GTC", + // "OrderType": "Limit", + // "StopOrderType": null, + // "OrderIv": null, + // "TriggerPrice": 0, + // "TakeProfit": 0, + // "StopLoss": 0, + // "TpTriggerBy": null, + // "SlTriggerBy": null, + // "TriggerDirection": 0, + // "TriggerBy": null, + // "LastPriceOnCreated": null, + // "ReduceOnly": false, + // "CloseOnTrigger": false, + // "SmpType": "None", + // "SmpGroup": 0, + // "SmpOrderId": null, + // "TpslMode": null, + // "TpLimitPrice": null, + // "SlLimitPrice": null, + // "PlaceType": null, + // "CreatedTime": "2023-07-25 17:12:41.325 +0800 CST", + // "UpdatedTime": "2023-07-25 17:12:57.868 +0800 CST" + //} + timeNow := time.Now() + openOrder := bybitapi.OpenOrder{ + OrderId: "1472539279335923200", + OrderLinkId: "1690276361150", + BlockTradeId: "", + Symbol: "DOTUSDT", + Price: fixedpoint.NewFromFloat(7.278), + Qty: fixedpoint.NewFromFloat(0.8), + Side: bybitapi.SideSell, + IsLeverage: "0", + PositionIdx: 0, + OrderStatus: bybitapi.OrderStatusPartiallyFilled, + CancelType: "UNKNOWN", + RejectReason: "", + AvgPrice: fixedpoint.NewFromFloat(7.728), + LeavesQty: fixedpoint.NewFromFloat(0), + LeavesValue: fixedpoint.NewFromFloat(0), + CumExecQty: fixedpoint.NewFromFloat(0.5), + CumExecValue: fixedpoint.NewFromFloat(0), + CumExecFee: fixedpoint.NewFromFloat(0), + TimeInForce: "GTC", + OrderType: bybitapi.OrderTypeLimit, + StopOrderType: "", + OrderIv: "", + TriggerPrice: fixedpoint.NewFromFloat(0), + TakeProfit: fixedpoint.NewFromFloat(0), + StopLoss: fixedpoint.NewFromFloat(0), + TpTriggerBy: "", + SlTriggerBy: "", + TriggerDirection: 0, + TriggerBy: "", + LastPriceOnCreated: "", + ReduceOnly: false, + CloseOnTrigger: false, + SmpType: "None", + SmpGroup: 0, + SmpOrderId: "", + TpslMode: "", + TpLimitPrice: "", + SlLimitPrice: "", + PlaceType: "", + CreatedTime: types.MillisecondTimestamp(timeNow), + UpdatedTime: types.MillisecondTimestamp(timeNow), + } + side, err := toGlobalSideType(openOrder.Side) + assert.NoError(t, err) + orderType, err := toGlobalOrderType(openOrder.OrderType) + assert.NoError(t, err) + tif, err := toGlobalTimeInForce(openOrder.TimeInForce) + assert.NoError(t, err) + status, err := toGlobalOrderStatus(openOrder.OrderStatus) + assert.NoError(t, err) + working, err := isWorking(openOrder.OrderStatus) + assert.NoError(t, err) + + exp := types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: openOrder.OrderLinkId, + Symbol: openOrder.Symbol, + Side: side, + Type: orderType, + Quantity: openOrder.Qty, + Price: openOrder.Price, + TimeInForce: tif, + }, + Exchange: types.ExchangeBybit, + OrderID: hashStringID(openOrder.OrderId), + UUID: openOrder.OrderId, + Status: status, + ExecutedQuantity: openOrder.CumExecQty, + IsWorking: working, + CreationTime: types.Time(timeNow), + UpdateTime: types.Time(timeNow), + IsFutures: false, + IsMargin: false, + IsIsolated: false, + } + res, err := toGlobalOrder(openOrder) + assert.NoError(t, err) + assert.Equal(t, res, &exp) +} + +func TestToGlobalSideType(t *testing.T) { + res, err := toGlobalSideType(bybitapi.SideBuy) + assert.NoError(t, err) + assert.Equal(t, types.SideTypeBuy, res) + + res, err = toGlobalSideType(bybitapi.SideSell) + assert.NoError(t, err) + assert.Equal(t, types.SideTypeSell, res) + + res, err = toGlobalSideType("GG") + assert.Error(t, err) +} + +func TestToGlobalOrderType(t *testing.T) { + res, err := toGlobalOrderType(bybitapi.OrderTypeMarket) + assert.NoError(t, err) + assert.Equal(t, types.OrderTypeMarket, res) + + res, err = toGlobalOrderType(bybitapi.OrderTypeLimit) + assert.NoError(t, err) + assert.Equal(t, types.OrderTypeLimit, res) + + res, err = toGlobalOrderType("GG") + assert.Error(t, err) +} + +func TestToGlobalTimeInForce(t *testing.T) { + res, err := toGlobalTimeInForce(bybitapi.TimeInForceGTC) + assert.NoError(t, err) + assert.Equal(t, types.TimeInForceGTC, res) + + res, err = toGlobalTimeInForce(bybitapi.TimeInForceIOC) + assert.NoError(t, err) + assert.Equal(t, types.TimeInForceIOC, res) + + res, err = toGlobalTimeInForce(bybitapi.TimeInForceFOK) + assert.NoError(t, err) + assert.Equal(t, types.TimeInForceFOK, res) + + res, err = toGlobalTimeInForce("GG") + assert.Error(t, err) +} + +func TestToGlobalOrderStatus(t *testing.T) { + t.Run("New", func(t *testing.T) { + res, err := toGlobalOrderStatus(bybitapi.OrderStatusNew) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusNew, res) + + res, err = toGlobalOrderStatus(bybitapi.OrderStatusActive) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusNew, res) + }) + + t.Run("Filled", func(t *testing.T) { + res, err := toGlobalOrderStatus(bybitapi.OrderStatusFilled) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusFilled, res) + }) + + t.Run("PartiallyFilled", func(t *testing.T) { + res, err := toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilled) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusPartiallyFilled, res) + }) + + t.Run("OrderStatusCanceled", func(t *testing.T) { + res, err := toGlobalOrderStatus(bybitapi.OrderStatusCancelled) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusCanceled, res) + + res, err = toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilledCanceled) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusCanceled, res) + + res, err = toGlobalOrderStatus(bybitapi.OrderStatusDeactivated) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusCanceled, res) + }) + + t.Run("OrderStatusRejected", func(t *testing.T) { + res, err := toGlobalOrderStatus(bybitapi.OrderStatusRejected) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusRejected, res) + }) +} + +func TestIsWorking(t *testing.T) { + for _, s := range bybitapi.AllOrderStatuses { + res, err := isWorking(s) + assert.NoError(t, err) + if res { + gos, err := toGlobalOrderStatus(s) + assert.NoError(t, err) + assert.True(t, gos == types.OrderStatusNew || gos == types.OrderStatusPartiallyFilled) + } + } +} diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 9d8a01059d..8624434e97 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -17,11 +17,14 @@ import ( // // The default order limiter apply 2 requests per second and a 2 initial bucket // this includes QueryMarkets, QueryTicker -var sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2) +var ( + sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2) + tradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5) -var log = logrus.WithFields(logrus.Fields{ - "exchange": "bybit", -}) + log = logrus.WithFields(logrus.Fields{ + "exchange": "bybit", + }) +) type Exchange struct { key, secret string @@ -128,3 +131,41 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[str return tickers, nil } + +func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { + cursor := "" + for { + req := e.client.NewGetOpenOrderRequest().Symbol(symbol) + if len(cursor) != 0 { + // the default limit is 20. + req = req.Cursor(cursor) + } + + if err = tradeRateLimiter.Wait(ctx); err != nil { + log.WithError(err).Errorf("trade rate limiter wait error") + return nil, err + } + res, err := req.Do(ctx) + if err != nil { + log.Warnf("failed to get open order, cursor: %s, err: %v", cursor, err) + return nil, err + } + + for _, order := range res.List { + order, err := toGlobalOrder(order) + if err != nil { + log.Warnf("failed to convert order, err: %v", err) + return nil, err + } + + orders = append(orders, *order) + } + + if len(res.NextPageCursor) == 0 { + break + } + cursor = res.NextPageCursor + } + + return orders, nil +} From cddb7874ce6b2f5df0d6403a2bb6f14ba5f5a13d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 26 Jul 2023 14:34:50 +0800 Subject: [PATCH 1261/1392] maxapi: set user agent --- pkg/exchange/max/maxapi/restapi.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index 9f3eb7b28d..1dcdc4039e 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -271,6 +271,8 @@ func (c *RestClient) newAuthenticatedRequest(ctx context.Context, m string, refU req.Header.Set("USER-AGENT", UserAgent) } + req.Header.Set("USER-AGENT", "Go-http-client/1.1,bbgo/"+version.Version) + if false { out, _ := httputil.DumpRequestOut(req, true) fmt.Println(string(out)) From 151e8d2acf363bffcea8c3d65b3076daeea0c2ca Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 26 Jul 2023 21:40:39 +0800 Subject: [PATCH 1262/1392] pkg/exchange: support place order for bybit --- pkg/exchange/bybit/bybitapi/client_test.go | 20 + .../bybitapi/post_place_order_request.go | 57 ++ .../post_place_order_request_requestgen.go | 528 ++++++++++++++++++ pkg/exchange/bybit/convert.go | 26 + pkg/exchange/bybit/convert_test.go | 29 + pkg/exchange/bybit/exchange.go | 78 +++ 6 files changed, 738 insertions(+) create mode 100644 pkg/exchange/bybit/bybitapi/post_place_order_request.go create mode 100644 pkg/exchange/bybit/bybitapi/post_place_order_request_requestgen.go diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index 02433fdd9e..a3735e555c 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -6,6 +6,7 @@ import ( "strconv" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/testutil" @@ -77,4 +78,23 @@ func TestClient(t *testing.T) { cursor = openOrders.NextPageCursor } }) + + t.Run("PostPlaceOrderRequest", func(t *testing.T) { + req := client.NewPlaceOrderRequest(). + Symbol("DOTUSDT"). + Side(SideBuy). + OrderType(OrderTypeLimit). + Qty("1"). + Price("4.6"). + OrderLinkId(uuid.NewString()). + TimeInForce(TimeInForceGTC) + apiResp, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %+v", apiResp) + + ordersResp, err := client.NewGetOpenOrderRequest().OrderLinkId(apiResp.OrderLinkId).Do(ctx) + assert.NoError(t, err) + assert.Equal(t, len(ordersResp.List), 1) + t.Logf("apiResp: %+v", ordersResp.List[0]) + }) } diff --git a/pkg/exchange/bybit/bybitapi/post_place_order_request.go b/pkg/exchange/bybit/bybitapi/post_place_order_request.go new file mode 100644 index 0000000000..4360d2cdf0 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/post_place_order_request.go @@ -0,0 +1,57 @@ +package bybitapi + +import ( + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +type PlaceOrderResponse struct { + OrderId string `json:"orderId"` + OrderLinkId string `json:"orderLinkId"` +} + +//go:generate PostRequest -url "/v5/order/create" -type PostPlaceOrderRequest -responseDataType .PlaceOrderResponse +type PostPlaceOrderRequest struct { + client requestgen.AuthenticatedAPIClient + + category Category `param:"category" validValues:"spot"` + symbol string `param:"symbol"` + side Side `param:"side" validValues:"Buy,Sell"` + orderType OrderType `param:"orderType" validValues:"Market,Limit"` + qty string `param:"qty"` + orderLinkId string `param:"orderLinkId"` + timeInForce TimeInForce `param:"timeInForce"` + + isLeverage *bool `param:"isLeverage"` + price *string `param:"price"` + triggerDirection *int `param:"triggerDirection"` + // orderFilter default spot + orderFilter *string `param:"orderFilter"` + // triggerPrice when submitting an order, if triggerPrice is set, the order will be automatically converted into a conditional order. + triggerPrice *string `param:"triggerPrice"` + triggerBy *string `param:"triggerBy"` + orderIv *string `param:"orderIv"` + positionIdx *string `param:"positionIdx"` + takeProfit *string `param:"takeProfit"` + stopLoss *string `param:"stopLoss"` + tpTriggerBy *string `param:"tpTriggerBy"` + slTriggerBy *string `param:"slTriggerBy"` + reduceOnly *bool `param:"reduceOnly"` + closeOnTrigger *bool `param:"closeOnTrigger"` + smpType *string `param:"smpType"` + mmp *bool `param:"mmp"` // option only + tpslMode *string `param:"tpslMode"` + tpLimitPrice *string `param:"tpLimitPrice"` + slLimitPrice *string `param:"slLimitPrice"` + tpOrderType *string `param:"tpOrderType"` + slOrderType *string `param:"slOrderType"` +} + +func (c *RestClient) NewPlaceOrderRequest() *PostPlaceOrderRequest { + return &PostPlaceOrderRequest{ + client: c, + category: CategorySpot, + } +} diff --git a/pkg/exchange/bybit/bybitapi/post_place_order_request_requestgen.go b/pkg/exchange/bybit/bybitapi/post_place_order_request_requestgen.go new file mode 100644 index 0000000000..630fe71d9e --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/post_place_order_request_requestgen.go @@ -0,0 +1,528 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Result -url /v5/order/create -type PostPlaceOrderRequest -responseDataType .PlaceOrderResponse"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (p *PostPlaceOrderRequest) Category(category Category) *PostPlaceOrderRequest { + p.category = category + return p +} + +func (p *PostPlaceOrderRequest) Symbol(symbol string) *PostPlaceOrderRequest { + p.symbol = symbol + return p +} + +func (p *PostPlaceOrderRequest) Side(side Side) *PostPlaceOrderRequest { + p.side = side + return p +} + +func (p *PostPlaceOrderRequest) OrderType(orderType OrderType) *PostPlaceOrderRequest { + p.orderType = orderType + return p +} + +func (p *PostPlaceOrderRequest) Qty(qty string) *PostPlaceOrderRequest { + p.qty = qty + return p +} + +func (p *PostPlaceOrderRequest) OrderLinkId(orderLinkId string) *PostPlaceOrderRequest { + p.orderLinkId = orderLinkId + return p +} + +func (p *PostPlaceOrderRequest) TimeInForce(timeInForce TimeInForce) *PostPlaceOrderRequest { + p.timeInForce = timeInForce + return p +} + +func (p *PostPlaceOrderRequest) IsLeverage(isLeverage bool) *PostPlaceOrderRequest { + p.isLeverage = &isLeverage + return p +} + +func (p *PostPlaceOrderRequest) Price(price string) *PostPlaceOrderRequest { + p.price = &price + return p +} + +func (p *PostPlaceOrderRequest) TriggerDirection(triggerDirection int) *PostPlaceOrderRequest { + p.triggerDirection = &triggerDirection + return p +} + +func (p *PostPlaceOrderRequest) OrderFilter(orderFilter string) *PostPlaceOrderRequest { + p.orderFilter = &orderFilter + return p +} + +func (p *PostPlaceOrderRequest) TriggerPrice(triggerPrice string) *PostPlaceOrderRequest { + p.triggerPrice = &triggerPrice + return p +} + +func (p *PostPlaceOrderRequest) TriggerBy(triggerBy string) *PostPlaceOrderRequest { + p.triggerBy = &triggerBy + return p +} + +func (p *PostPlaceOrderRequest) OrderIv(orderIv string) *PostPlaceOrderRequest { + p.orderIv = &orderIv + return p +} + +func (p *PostPlaceOrderRequest) PositionIdx(positionIdx string) *PostPlaceOrderRequest { + p.positionIdx = &positionIdx + return p +} + +func (p *PostPlaceOrderRequest) TakeProfit(takeProfit string) *PostPlaceOrderRequest { + p.takeProfit = &takeProfit + return p +} + +func (p *PostPlaceOrderRequest) StopLoss(stopLoss string) *PostPlaceOrderRequest { + p.stopLoss = &stopLoss + return p +} + +func (p *PostPlaceOrderRequest) TpTriggerBy(tpTriggerBy string) *PostPlaceOrderRequest { + p.tpTriggerBy = &tpTriggerBy + return p +} + +func (p *PostPlaceOrderRequest) SlTriggerBy(slTriggerBy string) *PostPlaceOrderRequest { + p.slTriggerBy = &slTriggerBy + return p +} + +func (p *PostPlaceOrderRequest) ReduceOnly(reduceOnly bool) *PostPlaceOrderRequest { + p.reduceOnly = &reduceOnly + return p +} + +func (p *PostPlaceOrderRequest) CloseOnTrigger(closeOnTrigger bool) *PostPlaceOrderRequest { + p.closeOnTrigger = &closeOnTrigger + return p +} + +func (p *PostPlaceOrderRequest) SmpType(smpType string) *PostPlaceOrderRequest { + p.smpType = &smpType + return p +} + +func (p *PostPlaceOrderRequest) Mmp(mmp bool) *PostPlaceOrderRequest { + p.mmp = &mmp + return p +} + +func (p *PostPlaceOrderRequest) TpslMode(tpslMode string) *PostPlaceOrderRequest { + p.tpslMode = &tpslMode + return p +} + +func (p *PostPlaceOrderRequest) TpLimitPrice(tpLimitPrice string) *PostPlaceOrderRequest { + p.tpLimitPrice = &tpLimitPrice + return p +} + +func (p *PostPlaceOrderRequest) SlLimitPrice(slLimitPrice string) *PostPlaceOrderRequest { + p.slLimitPrice = &slLimitPrice + return p +} + +func (p *PostPlaceOrderRequest) TpOrderType(tpOrderType string) *PostPlaceOrderRequest { + p.tpOrderType = &tpOrderType + return p +} + +func (p *PostPlaceOrderRequest) SlOrderType(slOrderType string) *PostPlaceOrderRequest { + p.slOrderType = &slOrderType + return p +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (p *PostPlaceOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (p *PostPlaceOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check category field -> json key category + category := p.category + + // TEMPLATE check-valid-values + switch category { + case "spot": + params["category"] = category + + default: + return nil, fmt.Errorf("category value %v is invalid", category) + + } + // END TEMPLATE check-valid-values + + // assign parameter of category + params["category"] = category + // check symbol field -> json key symbol + symbol := p.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check side field -> json key side + side := p.side + + // TEMPLATE check-valid-values + switch side { + case "Buy", "Sell": + params["side"] = side + + default: + return nil, fmt.Errorf("side value %v is invalid", side) + + } + // END TEMPLATE check-valid-values + + // assign parameter of side + params["side"] = side + // check orderType field -> json key orderType + orderType := p.orderType + + // TEMPLATE check-valid-values + switch orderType { + case "Market", "Limit": + params["orderType"] = orderType + + default: + return nil, fmt.Errorf("orderType value %v is invalid", orderType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of orderType + params["orderType"] = orderType + // check qty field -> json key qty + qty := p.qty + + // assign parameter of qty + params["qty"] = qty + // check orderLinkId field -> json key orderLinkId + orderLinkId := p.orderLinkId + + // assign parameter of orderLinkId + params["orderLinkId"] = orderLinkId + // check timeInForce field -> json key timeInForce + timeInForce := p.timeInForce + + // TEMPLATE check-valid-values + switch timeInForce { + case TimeInForceGTC, TimeInForceIOC, TimeInForceFOK: + params["timeInForce"] = timeInForce + + default: + return nil, fmt.Errorf("timeInForce value %v is invalid", timeInForce) + + } + // END TEMPLATE check-valid-values + + // assign parameter of timeInForce + params["timeInForce"] = timeInForce + // check isLeverage field -> json key isLeverage + if p.isLeverage != nil { + isLeverage := *p.isLeverage + + // assign parameter of isLeverage + params["isLeverage"] = isLeverage + } else { + } + // check price field -> json key price + if p.price != nil { + price := *p.price + + // assign parameter of price + params["price"] = price + } else { + } + // check triggerDirection field -> json key triggerDirection + if p.triggerDirection != nil { + triggerDirection := *p.triggerDirection + + // assign parameter of triggerDirection + params["triggerDirection"] = triggerDirection + } else { + } + // check orderFilter field -> json key orderFilter + if p.orderFilter != nil { + orderFilter := *p.orderFilter + + // assign parameter of orderFilter + params["orderFilter"] = orderFilter + } else { + } + // check triggerPrice field -> json key triggerPrice + if p.triggerPrice != nil { + triggerPrice := *p.triggerPrice + + // assign parameter of triggerPrice + params["triggerPrice"] = triggerPrice + } else { + } + // check triggerBy field -> json key triggerBy + if p.triggerBy != nil { + triggerBy := *p.triggerBy + + // assign parameter of triggerBy + params["triggerBy"] = triggerBy + } else { + } + // check orderIv field -> json key orderIv + if p.orderIv != nil { + orderIv := *p.orderIv + + // assign parameter of orderIv + params["orderIv"] = orderIv + } else { + } + // check positionIdx field -> json key positionIdx + if p.positionIdx != nil { + positionIdx := *p.positionIdx + + // assign parameter of positionIdx + params["positionIdx"] = positionIdx + } else { + } + // check takeProfit field -> json key takeProfit + if p.takeProfit != nil { + takeProfit := *p.takeProfit + + // assign parameter of takeProfit + params["takeProfit"] = takeProfit + } else { + } + // check stopLoss field -> json key stopLoss + if p.stopLoss != nil { + stopLoss := *p.stopLoss + + // assign parameter of stopLoss + params["stopLoss"] = stopLoss + } else { + } + // check tpTriggerBy field -> json key tpTriggerBy + if p.tpTriggerBy != nil { + tpTriggerBy := *p.tpTriggerBy + + // assign parameter of tpTriggerBy + params["tpTriggerBy"] = tpTriggerBy + } else { + } + // check slTriggerBy field -> json key slTriggerBy + if p.slTriggerBy != nil { + slTriggerBy := *p.slTriggerBy + + // assign parameter of slTriggerBy + params["slTriggerBy"] = slTriggerBy + } else { + } + // check reduceOnly field -> json key reduceOnly + if p.reduceOnly != nil { + reduceOnly := *p.reduceOnly + + // assign parameter of reduceOnly + params["reduceOnly"] = reduceOnly + } else { + } + // check closeOnTrigger field -> json key closeOnTrigger + if p.closeOnTrigger != nil { + closeOnTrigger := *p.closeOnTrigger + + // assign parameter of closeOnTrigger + params["closeOnTrigger"] = closeOnTrigger + } else { + } + // check smpType field -> json key smpType + if p.smpType != nil { + smpType := *p.smpType + + // assign parameter of smpType + params["smpType"] = smpType + } else { + } + // check mmp field -> json key mmp + if p.mmp != nil { + mmp := *p.mmp + + // assign parameter of mmp + params["mmp"] = mmp + } else { + } + // check tpslMode field -> json key tpslMode + if p.tpslMode != nil { + tpslMode := *p.tpslMode + + // assign parameter of tpslMode + params["tpslMode"] = tpslMode + } else { + } + // check tpLimitPrice field -> json key tpLimitPrice + if p.tpLimitPrice != nil { + tpLimitPrice := *p.tpLimitPrice + + // assign parameter of tpLimitPrice + params["tpLimitPrice"] = tpLimitPrice + } else { + } + // check slLimitPrice field -> json key slLimitPrice + if p.slLimitPrice != nil { + slLimitPrice := *p.slLimitPrice + + // assign parameter of slLimitPrice + params["slLimitPrice"] = slLimitPrice + } else { + } + // check tpOrderType field -> json key tpOrderType + if p.tpOrderType != nil { + tpOrderType := *p.tpOrderType + + // assign parameter of tpOrderType + params["tpOrderType"] = tpOrderType + } else { + } + // check slOrderType field -> json key slOrderType + if p.slOrderType != nil { + slOrderType := *p.slOrderType + + // assign parameter of slOrderType + params["slOrderType"] = slOrderType + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (p *PostPlaceOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := p.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if p.isVarSlice(_v) { + p.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (p *PostPlaceOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := p.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (p *PostPlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (p *PostPlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (p *PostPlaceOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (p *PostPlaceOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (p *PostPlaceOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := p.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (p *PostPlaceOrderRequest) Do(ctx context.Context) (*PlaceOrderResponse, error) { + + params, err := p.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/v5/order/create" + + req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := p.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data PlaceOrderResponse + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index 5a2963fa26..85ce0c013f 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -170,3 +170,29 @@ func isWorking(status bybitapi.OrderStatus) (bool, error) { s, err := toGlobalOrderStatus(status) return s == types.OrderStatusNew || s == types.OrderStatusPartiallyFilled, err } + +func toLocalOrderType(orderType types.OrderType) (bybitapi.OrderType, error) { + switch orderType { + case types.OrderTypeLimit: + return bybitapi.OrderTypeLimit, nil + + case types.OrderTypeMarket: + return bybitapi.OrderTypeMarket, nil + + default: + return "", fmt.Errorf("order type %s not supported", orderType) + } +} + +func toLocalSide(side types.SideType) (bybitapi.Side, error) { + switch side { + case types.SideTypeSell: + return bybitapi.SideSell, nil + + case types.SideTypeBuy: + return bybitapi.SideBuy, nil + + default: + return "", fmt.Errorf("side type %s not supported", side) + } +} diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index 55e05d692e..c5c8b497c5 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -1,6 +1,7 @@ package bybit import ( + "fmt" "math" "testing" "time" @@ -356,3 +357,31 @@ func TestIsWorking(t *testing.T) { } } } + +func Test_toLocalOrderType(t *testing.T) { + orderType, err := toLocalOrderType(types.OrderTypeLimit) + assert.NoError(t, err) + assert.Equal(t, bybitapi.OrderTypeLimit, orderType) + + orderType, err = toLocalOrderType(types.OrderTypeMarket) + assert.NoError(t, err) + assert.Equal(t, bybitapi.OrderTypeMarket, orderType) + + orderType, err = toLocalOrderType("wrong type") + assert.Error(t, fmt.Errorf("order type %s not supported", "wrong side"), err) + assert.Equal(t, bybitapi.OrderType(""), orderType) +} + +func Test_toLocalSide(t *testing.T) { + side, err := toLocalSide(types.SideTypeSell) + assert.NoError(t, err) + assert.Equal(t, bybitapi.SideSell, side) + + side, err = toLocalSide(types.SideTypeBuy) + assert.NoError(t, err) + assert.Equal(t, bybitapi.SideBuy, side) + + side, err = toLocalSide("wrong side") + assert.Error(t, fmt.Errorf("side type %s not supported", "wrong side"), err) + assert.Equal(t, bybitapi.Side(""), side) +} diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 8624434e97..fbc01cf5e2 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -12,6 +12,10 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +const ( + maxOrderIdLen = 36 +) + // https://bybit-exchange.github.io/docs/zh-TW/v5/rate-limit // sharedRateLimiter indicates that the API belongs to the public API. // @@ -20,6 +24,7 @@ import ( var ( sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2) tradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5) + orderRateLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) log = logrus.WithFields(logrus.Fields{ "exchange": "bybit", @@ -169,3 +174,76 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ return orders, nil } + +func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { + if len(order.Market.Symbol) == 0 { + return nil, fmt.Errorf("order.Market.Symbol is required: %+v", order) + } + + req := e.client.NewPlaceOrderRequest() + req.Symbol(order.Symbol) + + // set order type + orderType, err := toLocalOrderType(order.Type) + if err != nil { + return nil, err + } + req.OrderType(orderType) + + // set side + side, err := toLocalSide(order.Side) + if err != nil { + return nil, err + } + req.Side(side) + + // set quantity + req.Qty(order.Market.FormatQuantity(order.Quantity)) + + // set price + switch order.Type { + case types.OrderTypeLimit: + req.Price(order.Market.FormatPrice(order.Price)) + } + + // set timeInForce + switch order.TimeInForce { + case types.TimeInForceFOK: + req.TimeInForce(bybitapi.TimeInForceFOK) + case types.TimeInForceIOC: + req.TimeInForce(bybitapi.TimeInForceIOC) + default: + req.TimeInForce(bybitapi.TimeInForceGTC) + } + + // set client order id + if len(order.ClientOrderID) > maxOrderIdLen { + return nil, fmt.Errorf("unexpected length of order id, got: %d", len(order.ClientOrderID)) + } + req.OrderLinkId(order.ClientOrderID) + + if err := orderRateLimiter.Wait(ctx); err != nil { + log.WithError(err).Errorf("place order rate limiter wait error") + return nil, err + } + res, err := req.Do(ctx) + if err != nil { + log.Warnf("failed to place order, order: %#v, err: %v", order, err) + return nil, err + } + + if len(res.OrderId) == 0 || res.OrderLinkId != order.ClientOrderID { + return nil, fmt.Errorf("unexpected order id, resp: %#v, order: %#v", res, order) + } + + ordersResp, err := e.client.NewGetOpenOrderRequest().OrderLinkId(res.OrderLinkId).Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to query order by client order id: %s", res.OrderLinkId) + } + + if len(ordersResp.List) != 1 { + return nil, fmt.Errorf("unexpected order length, client order id: %s", res.OrderLinkId) + } + + return toGlobalOrder(ordersResp.List[0]) +} From 5105046053138c95e249641f997ac7eca56df357 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 26 Jul 2023 22:24:20 +0800 Subject: [PATCH 1263/1392] pkg/exchange: support cancel order --- pkg/exchange/bybit/bybitapi/client_test.go | 32 +++ .../bybitapi/post_cancel_order_request.go | 35 +++ .../post_cancel_order_request_requestgen.go | 219 ++++++++++++++++++ pkg/exchange/bybit/exchange.go | 72 ++++-- 4 files changed, 337 insertions(+), 21 deletions(-) create mode 100644 pkg/exchange/bybit/bybitapi/post_cancel_order_request.go create mode 100644 pkg/exchange/bybit/bybitapi/post_cancel_order_request_requestgen.go diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index a3735e555c..61ce5226a0 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -97,4 +97,36 @@ func TestClient(t *testing.T) { assert.Equal(t, len(ordersResp.List), 1) t.Logf("apiResp: %+v", ordersResp.List[0]) }) + + t.Run("PostCancelOrderRequest", func(t *testing.T) { + req := client.NewPlaceOrderRequest(). + Symbol("DOTUSDT"). + Side(SideBuy). + OrderType(OrderTypeLimit). + Qty("1"). + Price("4.6"). + OrderLinkId(uuid.NewString()). + TimeInForce(TimeInForceGTC) + apiResp, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %+v", apiResp) + + ordersResp, err := client.NewGetOpenOrderRequest().OrderLinkId(apiResp.OrderLinkId).Do(ctx) + assert.NoError(t, err) + assert.Equal(t, len(ordersResp.List), 1) + t.Logf("apiResp: %+v", ordersResp.List[0]) + + cancelReq := client.NewCancelOrderRequest(). + Symbol("DOTUSDT"). + OrderLinkId(apiResp.OrderLinkId) + cancelResp, err := cancelReq.Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %+v", cancelResp) + + ordersResp, err = client.NewGetOpenOrderRequest().OrderLinkId(apiResp.OrderLinkId).Do(ctx) + assert.NoError(t, err) + assert.Equal(t, len(ordersResp.List), 1) + assert.Equal(t, ordersResp.List[0].OrderStatus, OrderStatusCancelled) + t.Logf("apiResp: %+v", ordersResp.List[0]) + }) } diff --git a/pkg/exchange/bybit/bybitapi/post_cancel_order_request.go b/pkg/exchange/bybit/bybitapi/post_cancel_order_request.go new file mode 100644 index 0000000000..0ab642d1dd --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/post_cancel_order_request.go @@ -0,0 +1,35 @@ +package bybitapi + +import ( + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +type CancelOrderResponse struct { + OrderId string `json:"orderId"` + OrderLinkId string `json:"orderLinkId"` +} + +//go:generate PostRequest -url "/v5/order/cancel" -type PostCancelOrderRequest -responseDataType .CancelOrderResponse +type PostCancelOrderRequest struct { + client requestgen.AuthenticatedAPIClient + + category Category `param:"category" validValues:"spot"` + symbol string `param:"symbol"` + // User customised order ID. Either orderId or orderLinkId is required + orderLinkId string `param:"orderLinkId"` + + orderId *string `param:"orderLinkId"` + // orderFilter default type is Order + // tpsl order type are not currently supported + orderFilter *string `param:"timeInForce" validValues:"Order"` +} + +func (c *RestClient) NewCancelOrderRequest() *PostCancelOrderRequest { + return &PostCancelOrderRequest{ + client: c, + category: CategorySpot, + } +} diff --git a/pkg/exchange/bybit/bybitapi/post_cancel_order_request_requestgen.go b/pkg/exchange/bybit/bybitapi/post_cancel_order_request_requestgen.go new file mode 100644 index 0000000000..3e16b258cb --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/post_cancel_order_request_requestgen.go @@ -0,0 +1,219 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Result -url /v5/order/cancel -type PostCancelOrderRequest -responseDataType .CancelOrderResponse"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (p *PostCancelOrderRequest) Category(category Category) *PostCancelOrderRequest { + p.category = category + return p +} + +func (p *PostCancelOrderRequest) Symbol(symbol string) *PostCancelOrderRequest { + p.symbol = symbol + return p +} + +func (p *PostCancelOrderRequest) OrderLinkId(orderLinkId string) *PostCancelOrderRequest { + p.orderLinkId = orderLinkId + return p +} + +func (p *PostCancelOrderRequest) OrderId(orderId string) *PostCancelOrderRequest { + p.orderId = &orderId + return p +} + +func (p *PostCancelOrderRequest) OrderFilter(orderFilter string) *PostCancelOrderRequest { + p.orderFilter = &orderFilter + return p +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (p *PostCancelOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (p *PostCancelOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check category field -> json key category + category := p.category + + // TEMPLATE check-valid-values + switch category { + case "spot": + params["category"] = category + + default: + return nil, fmt.Errorf("category value %v is invalid", category) + + } + // END TEMPLATE check-valid-values + + // assign parameter of category + params["category"] = category + // check symbol field -> json key symbol + symbol := p.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check orderLinkId field -> json key orderLinkId + orderLinkId := p.orderLinkId + + // assign parameter of orderLinkId + params["orderLinkId"] = orderLinkId + // check orderId field -> json key orderLinkId + if p.orderId != nil { + orderId := *p.orderId + + // assign parameter of orderId + params["orderLinkId"] = orderId + } else { + } + // check orderFilter field -> json key timeInForce + if p.orderFilter != nil { + orderFilter := *p.orderFilter + + // TEMPLATE check-valid-values + switch orderFilter { + case "Order": + params["timeInForce"] = orderFilter + + default: + return nil, fmt.Errorf("timeInForce value %v is invalid", orderFilter) + + } + // END TEMPLATE check-valid-values + + // assign parameter of orderFilter + params["timeInForce"] = orderFilter + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (p *PostCancelOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := p.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if p.isVarSlice(_v) { + p.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (p *PostCancelOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := p.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (p *PostCancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (p *PostCancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (p *PostCancelOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (p *PostCancelOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (p *PostCancelOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := p.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (p *PostCancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) { + + params, err := p.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/v5/order/cancel" + + req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := p.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data CancelOrderResponse + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index fbc01cf5e2..a8a75b85b8 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -6,6 +6,7 @@ import ( "time" "github.com/sirupsen/logrus" + "go.uber.org/multierr" "golang.org/x/time/rate" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" @@ -66,14 +67,12 @@ func (e *Exchange) PlatformFeeCurrency() string { func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { if err := sharedRateLimiter.Wait(ctx); err != nil { - log.WithError(err).Errorf("markets rate limiter wait error") - return nil, err + return nil, fmt.Errorf("markets rate limiter wait error: %v", err) } instruments, err := e.client.NewGetInstrumentsInfoRequest().Do(ctx) if err != nil { - log.Warnf("failed to query instruments, err: %v", err) - return nil, err + return nil, fmt.Errorf("failed to get instruments, err: %v", err) } marketMap := types.MarketMap{} @@ -86,18 +85,15 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { if err := sharedRateLimiter.Wait(ctx); err != nil { - log.WithError(err).Errorf("ticker rate limiter wait error") - return nil, err + return nil, fmt.Errorf("ticker order rate limiter wait error: %v", err) } s, err := e.client.NewGetTickersRequest().Symbol(symbol).DoWithResponseTime(ctx) if err != nil { - log.Warnf("failed to get tickers, symbol: %s, err: %v", symbol, err) return nil, err } if len(s.List) != 1 { - log.Warnf("unexpected ticker length, exp: 1, got: %d", len(s.List)) return nil, fmt.Errorf("unexpected ticker lenght, exp:1, got:%d", len(s.List)) } @@ -121,12 +117,10 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[str } if err := sharedRateLimiter.Wait(ctx); err != nil { - log.WithError(err).Errorf("ticker rate limiter wait error") - return nil, err + return nil, fmt.Errorf("tickers rate limiter wait error: %v", err) } allTickers, err := e.client.NewGetTickersRequest().DoWithResponseTime(ctx) if err != nil { - log.Warnf("failed to get tickers, err: %v", err) return nil, err } @@ -147,20 +141,17 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ } if err = tradeRateLimiter.Wait(ctx); err != nil { - log.WithError(err).Errorf("trade rate limiter wait error") - return nil, err + return nil, fmt.Errorf("place order rate limiter wait error: %v", err) } res, err := req.Do(ctx) if err != nil { - log.Warnf("failed to get open order, cursor: %s, err: %v", cursor, err) - return nil, err + return nil, fmt.Errorf("failed to query open orders, err: %v", err) } for _, order := range res.List { order, err := toGlobalOrder(order) if err != nil { - log.Warnf("failed to convert order, err: %v", err) - return nil, err + return nil, fmt.Errorf("failed to convert order, err: %v", err) } orders = append(orders, *order) @@ -223,13 +214,11 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t req.OrderLinkId(order.ClientOrderID) if err := orderRateLimiter.Wait(ctx); err != nil { - log.WithError(err).Errorf("place order rate limiter wait error") - return nil, err + return nil, fmt.Errorf("place order rate limiter wait error: %v", err) } res, err := req.Do(ctx) if err != nil { - log.Warnf("failed to place order, order: %#v, err: %v", order, err) - return nil, err + return nil, fmt.Errorf("failed to place order, order: %#v, err: %v", order, err) } if len(res.OrderId) == 0 || res.OrderLinkId != order.ClientOrderID { @@ -247,3 +236,44 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t return toGlobalOrder(ordersResp.List[0]) } + +func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (errs error) { + if len(orders) == 0 { + return nil + } + + for _, order := range orders { + req := e.client.NewCancelOrderRequest() + + switch { + case len(order.ClientOrderID) != 0: + req.OrderLinkId(order.ClientOrderID) + case len(order.UUID) != 0 && order.OrderID != 0: + req.OrderId(order.UUID) + default: + errs = multierr.Append( + errs, + fmt.Errorf("the order uuid and client order id are empty, order: %#v", order), + ) + continue + } + + req.Symbol(order.Market.Symbol) + + if err := orderRateLimiter.Wait(ctx); err != nil { + errs = multierr.Append(errs, fmt.Errorf("cancel order rate limiter wait, order id: %s, error: %v", order.ClientOrderID, err)) + continue + } + res, err := req.Do(ctx) + if err != nil { + errs = multierr.Append(errs, fmt.Errorf("failed to cancel order id: %s, err: %v", order.ClientOrderID, err)) + continue + } + if res.OrderId != order.UUID || res.OrderLinkId != order.ClientOrderID { + errs = multierr.Append(errs, fmt.Errorf("unexpected order id, resp: %#v, order: %#v", res, order)) + continue + } + } + + return errs +} From 574d7c0c74f464a7233d4d12e460a0fec510bd29 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 27 Jul 2023 10:31:24 +0800 Subject: [PATCH 1264/1392] pkg/exchange: rm redundant prefix --- ...der_request.go => cancel_order_request.go} | 8 +- ....go => cancel_order_request_requestgen.go} | 32 ++++---- pkg/exchange/bybit/bybitapi/client_test.go | 4 +- ...rder_request.go => place_order_request.go} | 8 +- ...n.go => place_order_request_requestgen.go} | 78 +++++++++---------- 5 files changed, 65 insertions(+), 65 deletions(-) rename pkg/exchange/bybit/bybitapi/{post_cancel_order_request.go => cancel_order_request.go} (77%) rename pkg/exchange/bybit/bybitapi/{post_cancel_order_request_requestgen.go => cancel_order_request_requestgen.go} (74%) rename pkg/exchange/bybit/bybitapi/{post_place_order_request.go => place_order_request.go} (88%) rename pkg/exchange/bybit/bybitapi/{post_place_order_request_requestgen.go => place_order_request_requestgen.go} (75%) diff --git a/pkg/exchange/bybit/bybitapi/post_cancel_order_request.go b/pkg/exchange/bybit/bybitapi/cancel_order_request.go similarity index 77% rename from pkg/exchange/bybit/bybitapi/post_cancel_order_request.go rename to pkg/exchange/bybit/bybitapi/cancel_order_request.go index 0ab642d1dd..144a086239 100644 --- a/pkg/exchange/bybit/bybitapi/post_cancel_order_request.go +++ b/pkg/exchange/bybit/bybitapi/cancel_order_request.go @@ -12,8 +12,8 @@ type CancelOrderResponse struct { OrderLinkId string `json:"orderLinkId"` } -//go:generate PostRequest -url "/v5/order/cancel" -type PostCancelOrderRequest -responseDataType .CancelOrderResponse -type PostCancelOrderRequest struct { +//go:generate PostRequest -url "/v5/order/cancel" -type CancelOrderRequest -responseDataType .CancelOrderResponse +type CancelOrderRequest struct { client requestgen.AuthenticatedAPIClient category Category `param:"category" validValues:"spot"` @@ -27,8 +27,8 @@ type PostCancelOrderRequest struct { orderFilter *string `param:"timeInForce" validValues:"Order"` } -func (c *RestClient) NewCancelOrderRequest() *PostCancelOrderRequest { - return &PostCancelOrderRequest{ +func (c *RestClient) NewCancelOrderRequest() *CancelOrderRequest { + return &CancelOrderRequest{ client: c, category: CategorySpot, } diff --git a/pkg/exchange/bybit/bybitapi/post_cancel_order_request_requestgen.go b/pkg/exchange/bybit/bybitapi/cancel_order_request_requestgen.go similarity index 74% rename from pkg/exchange/bybit/bybitapi/post_cancel_order_request_requestgen.go rename to pkg/exchange/bybit/bybitapi/cancel_order_request_requestgen.go index 3e16b258cb..6bda981ded 100644 --- a/pkg/exchange/bybit/bybitapi/post_cancel_order_request_requestgen.go +++ b/pkg/exchange/bybit/bybitapi/cancel_order_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Result -url /v5/order/cancel -type PostCancelOrderRequest -responseDataType .CancelOrderResponse"; DO NOT EDIT. +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Result -url /v5/order/cancel -type CancelOrderRequest -responseDataType .CancelOrderResponse"; DO NOT EDIT. package bybitapi @@ -11,33 +11,33 @@ import ( "regexp" ) -func (p *PostCancelOrderRequest) Category(category Category) *PostCancelOrderRequest { +func (p *CancelOrderRequest) Category(category Category) *CancelOrderRequest { p.category = category return p } -func (p *PostCancelOrderRequest) Symbol(symbol string) *PostCancelOrderRequest { +func (p *CancelOrderRequest) Symbol(symbol string) *CancelOrderRequest { p.symbol = symbol return p } -func (p *PostCancelOrderRequest) OrderLinkId(orderLinkId string) *PostCancelOrderRequest { +func (p *CancelOrderRequest) OrderLinkId(orderLinkId string) *CancelOrderRequest { p.orderLinkId = orderLinkId return p } -func (p *PostCancelOrderRequest) OrderId(orderId string) *PostCancelOrderRequest { +func (p *CancelOrderRequest) OrderId(orderId string) *CancelOrderRequest { p.orderId = &orderId return p } -func (p *PostCancelOrderRequest) OrderFilter(orderFilter string) *PostCancelOrderRequest { +func (p *CancelOrderRequest) OrderFilter(orderFilter string) *CancelOrderRequest { p.orderFilter = &orderFilter return p } // GetQueryParameters builds and checks the query parameters and returns url.Values -func (p *PostCancelOrderRequest) GetQueryParameters() (url.Values, error) { +func (p *CancelOrderRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} @@ -49,7 +49,7 @@ func (p *PostCancelOrderRequest) GetQueryParameters() (url.Values, error) { } // GetParameters builds and checks the parameters and return the result in a map object -func (p *PostCancelOrderRequest) GetParameters() (map[string]interface{}, error) { +func (p *CancelOrderRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} // check category field -> json key category category := p.category @@ -109,7 +109,7 @@ func (p *PostCancelOrderRequest) GetParameters() (map[string]interface{}, error) } // GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (p *PostCancelOrderRequest) GetParametersQuery() (url.Values, error) { +func (p *CancelOrderRequest) GetParametersQuery() (url.Values, error) { query := url.Values{} params, err := p.GetParameters() @@ -131,7 +131,7 @@ func (p *PostCancelOrderRequest) GetParametersQuery() (url.Values, error) { } // GetParametersJSON converts the parameters from GetParameters into the JSON format -func (p *PostCancelOrderRequest) GetParametersJSON() ([]byte, error) { +func (p *CancelOrderRequest) GetParametersJSON() ([]byte, error) { params, err := p.GetParameters() if err != nil { return nil, err @@ -141,13 +141,13 @@ func (p *PostCancelOrderRequest) GetParametersJSON() ([]byte, error) { } // GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (p *PostCancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) { +func (p *CancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} return params, nil } -func (p *PostCancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { +func (p *CancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { for _k, _v := range slugs { needleRE := regexp.MustCompile(":" + _k + "\\b") url = needleRE.ReplaceAllString(url, _v) @@ -156,7 +156,7 @@ func (p *PostCancelOrderRequest) applySlugsToUrl(url string, slugs map[string]st return url } -func (p *PostCancelOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { +func (p *CancelOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) for _i := 0; _i < sliceValue.Len(); _i++ { it := sliceValue.Index(_i).Interface() @@ -164,7 +164,7 @@ func (p *PostCancelOrderRequest) iterateSlice(slice interface{}, _f func(it inte } } -func (p *PostCancelOrderRequest) isVarSlice(_v interface{}) bool { +func (p *CancelOrderRequest) isVarSlice(_v interface{}) bool { rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: @@ -173,7 +173,7 @@ func (p *PostCancelOrderRequest) isVarSlice(_v interface{}) bool { return false } -func (p *PostCancelOrderRequest) GetSlugsMap() (map[string]string, error) { +func (p *CancelOrderRequest) GetSlugsMap() (map[string]string, error) { slugs := map[string]string{} params, err := p.GetSlugParameters() if err != nil { @@ -187,7 +187,7 @@ func (p *PostCancelOrderRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } -func (p *PostCancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) { +func (p *CancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) { params, err := p.GetParameters() if err != nil { diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index 61ce5226a0..f74eb22959 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -79,7 +79,7 @@ func TestClient(t *testing.T) { } }) - t.Run("PostPlaceOrderRequest", func(t *testing.T) { + t.Run("PlaceOrderRequest", func(t *testing.T) { req := client.NewPlaceOrderRequest(). Symbol("DOTUSDT"). Side(SideBuy). @@ -98,7 +98,7 @@ func TestClient(t *testing.T) { t.Logf("apiResp: %+v", ordersResp.List[0]) }) - t.Run("PostCancelOrderRequest", func(t *testing.T) { + t.Run("CancelOrderRequest", func(t *testing.T) { req := client.NewPlaceOrderRequest(). Symbol("DOTUSDT"). Side(SideBuy). diff --git a/pkg/exchange/bybit/bybitapi/post_place_order_request.go b/pkg/exchange/bybit/bybitapi/place_order_request.go similarity index 88% rename from pkg/exchange/bybit/bybitapi/post_place_order_request.go rename to pkg/exchange/bybit/bybitapi/place_order_request.go index 4360d2cdf0..9afbebf58f 100644 --- a/pkg/exchange/bybit/bybitapi/post_place_order_request.go +++ b/pkg/exchange/bybit/bybitapi/place_order_request.go @@ -12,8 +12,8 @@ type PlaceOrderResponse struct { OrderLinkId string `json:"orderLinkId"` } -//go:generate PostRequest -url "/v5/order/create" -type PostPlaceOrderRequest -responseDataType .PlaceOrderResponse -type PostPlaceOrderRequest struct { +//go:generate PostRequest -url "/v5/order/create" -type PlaceOrderRequest -responseDataType .PlaceOrderResponse +type PlaceOrderRequest struct { client requestgen.AuthenticatedAPIClient category Category `param:"category" validValues:"spot"` @@ -49,8 +49,8 @@ type PostPlaceOrderRequest struct { slOrderType *string `param:"slOrderType"` } -func (c *RestClient) NewPlaceOrderRequest() *PostPlaceOrderRequest { - return &PostPlaceOrderRequest{ +func (c *RestClient) NewPlaceOrderRequest() *PlaceOrderRequest { + return &PlaceOrderRequest{ client: c, category: CategorySpot, } diff --git a/pkg/exchange/bybit/bybitapi/post_place_order_request_requestgen.go b/pkg/exchange/bybit/bybitapi/place_order_request_requestgen.go similarity index 75% rename from pkg/exchange/bybit/bybitapi/post_place_order_request_requestgen.go rename to pkg/exchange/bybit/bybitapi/place_order_request_requestgen.go index 630fe71d9e..bb85937cec 100644 --- a/pkg/exchange/bybit/bybitapi/post_place_order_request_requestgen.go +++ b/pkg/exchange/bybit/bybitapi/place_order_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Result -url /v5/order/create -type PostPlaceOrderRequest -responseDataType .PlaceOrderResponse"; DO NOT EDIT. +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Result -url /v5/order/create -type PlaceOrderRequest -responseDataType .PlaceOrderResponse"; DO NOT EDIT. package bybitapi @@ -11,148 +11,148 @@ import ( "regexp" ) -func (p *PostPlaceOrderRequest) Category(category Category) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) Category(category Category) *PlaceOrderRequest { p.category = category return p } -func (p *PostPlaceOrderRequest) Symbol(symbol string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) Symbol(symbol string) *PlaceOrderRequest { p.symbol = symbol return p } -func (p *PostPlaceOrderRequest) Side(side Side) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) Side(side Side) *PlaceOrderRequest { p.side = side return p } -func (p *PostPlaceOrderRequest) OrderType(orderType OrderType) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest { p.orderType = orderType return p } -func (p *PostPlaceOrderRequest) Qty(qty string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) Qty(qty string) *PlaceOrderRequest { p.qty = qty return p } -func (p *PostPlaceOrderRequest) OrderLinkId(orderLinkId string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) OrderLinkId(orderLinkId string) *PlaceOrderRequest { p.orderLinkId = orderLinkId return p } -func (p *PostPlaceOrderRequest) TimeInForce(timeInForce TimeInForce) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) TimeInForce(timeInForce TimeInForce) *PlaceOrderRequest { p.timeInForce = timeInForce return p } -func (p *PostPlaceOrderRequest) IsLeverage(isLeverage bool) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) IsLeverage(isLeverage bool) *PlaceOrderRequest { p.isLeverage = &isLeverage return p } -func (p *PostPlaceOrderRequest) Price(price string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) Price(price string) *PlaceOrderRequest { p.price = &price return p } -func (p *PostPlaceOrderRequest) TriggerDirection(triggerDirection int) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) TriggerDirection(triggerDirection int) *PlaceOrderRequest { p.triggerDirection = &triggerDirection return p } -func (p *PostPlaceOrderRequest) OrderFilter(orderFilter string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) OrderFilter(orderFilter string) *PlaceOrderRequest { p.orderFilter = &orderFilter return p } -func (p *PostPlaceOrderRequest) TriggerPrice(triggerPrice string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) TriggerPrice(triggerPrice string) *PlaceOrderRequest { p.triggerPrice = &triggerPrice return p } -func (p *PostPlaceOrderRequest) TriggerBy(triggerBy string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) TriggerBy(triggerBy string) *PlaceOrderRequest { p.triggerBy = &triggerBy return p } -func (p *PostPlaceOrderRequest) OrderIv(orderIv string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) OrderIv(orderIv string) *PlaceOrderRequest { p.orderIv = &orderIv return p } -func (p *PostPlaceOrderRequest) PositionIdx(positionIdx string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) PositionIdx(positionIdx string) *PlaceOrderRequest { p.positionIdx = &positionIdx return p } -func (p *PostPlaceOrderRequest) TakeProfit(takeProfit string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) TakeProfit(takeProfit string) *PlaceOrderRequest { p.takeProfit = &takeProfit return p } -func (p *PostPlaceOrderRequest) StopLoss(stopLoss string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) StopLoss(stopLoss string) *PlaceOrderRequest { p.stopLoss = &stopLoss return p } -func (p *PostPlaceOrderRequest) TpTriggerBy(tpTriggerBy string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) TpTriggerBy(tpTriggerBy string) *PlaceOrderRequest { p.tpTriggerBy = &tpTriggerBy return p } -func (p *PostPlaceOrderRequest) SlTriggerBy(slTriggerBy string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) SlTriggerBy(slTriggerBy string) *PlaceOrderRequest { p.slTriggerBy = &slTriggerBy return p } -func (p *PostPlaceOrderRequest) ReduceOnly(reduceOnly bool) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) ReduceOnly(reduceOnly bool) *PlaceOrderRequest { p.reduceOnly = &reduceOnly return p } -func (p *PostPlaceOrderRequest) CloseOnTrigger(closeOnTrigger bool) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) CloseOnTrigger(closeOnTrigger bool) *PlaceOrderRequest { p.closeOnTrigger = &closeOnTrigger return p } -func (p *PostPlaceOrderRequest) SmpType(smpType string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) SmpType(smpType string) *PlaceOrderRequest { p.smpType = &smpType return p } -func (p *PostPlaceOrderRequest) Mmp(mmp bool) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) Mmp(mmp bool) *PlaceOrderRequest { p.mmp = &mmp return p } -func (p *PostPlaceOrderRequest) TpslMode(tpslMode string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) TpslMode(tpslMode string) *PlaceOrderRequest { p.tpslMode = &tpslMode return p } -func (p *PostPlaceOrderRequest) TpLimitPrice(tpLimitPrice string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) TpLimitPrice(tpLimitPrice string) *PlaceOrderRequest { p.tpLimitPrice = &tpLimitPrice return p } -func (p *PostPlaceOrderRequest) SlLimitPrice(slLimitPrice string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) SlLimitPrice(slLimitPrice string) *PlaceOrderRequest { p.slLimitPrice = &slLimitPrice return p } -func (p *PostPlaceOrderRequest) TpOrderType(tpOrderType string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) TpOrderType(tpOrderType string) *PlaceOrderRequest { p.tpOrderType = &tpOrderType return p } -func (p *PostPlaceOrderRequest) SlOrderType(slOrderType string) *PostPlaceOrderRequest { +func (p *PlaceOrderRequest) SlOrderType(slOrderType string) *PlaceOrderRequest { p.slOrderType = &slOrderType return p } // GetQueryParameters builds and checks the query parameters and returns url.Values -func (p *PostPlaceOrderRequest) GetQueryParameters() (url.Values, error) { +func (p *PlaceOrderRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} @@ -164,7 +164,7 @@ func (p *PostPlaceOrderRequest) GetQueryParameters() (url.Values, error) { } // GetParameters builds and checks the parameters and return the result in a map object -func (p *PostPlaceOrderRequest) GetParameters() (map[string]interface{}, error) { +func (p *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} // check category field -> json key category category := p.category @@ -418,7 +418,7 @@ func (p *PostPlaceOrderRequest) GetParameters() (map[string]interface{}, error) } // GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (p *PostPlaceOrderRequest) GetParametersQuery() (url.Values, error) { +func (p *PlaceOrderRequest) GetParametersQuery() (url.Values, error) { query := url.Values{} params, err := p.GetParameters() @@ -440,7 +440,7 @@ func (p *PostPlaceOrderRequest) GetParametersQuery() (url.Values, error) { } // GetParametersJSON converts the parameters from GetParameters into the JSON format -func (p *PostPlaceOrderRequest) GetParametersJSON() ([]byte, error) { +func (p *PlaceOrderRequest) GetParametersJSON() ([]byte, error) { params, err := p.GetParameters() if err != nil { return nil, err @@ -450,13 +450,13 @@ func (p *PostPlaceOrderRequest) GetParametersJSON() ([]byte, error) { } // GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (p *PostPlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) { +func (p *PlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} return params, nil } -func (p *PostPlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { +func (p *PlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { for _k, _v := range slugs { needleRE := regexp.MustCompile(":" + _k + "\\b") url = needleRE.ReplaceAllString(url, _v) @@ -465,7 +465,7 @@ func (p *PostPlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]str return url } -func (p *PostPlaceOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { +func (p *PlaceOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) for _i := 0; _i < sliceValue.Len(); _i++ { it := sliceValue.Index(_i).Interface() @@ -473,7 +473,7 @@ func (p *PostPlaceOrderRequest) iterateSlice(slice interface{}, _f func(it inter } } -func (p *PostPlaceOrderRequest) isVarSlice(_v interface{}) bool { +func (p *PlaceOrderRequest) isVarSlice(_v interface{}) bool { rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: @@ -482,7 +482,7 @@ func (p *PostPlaceOrderRequest) isVarSlice(_v interface{}) bool { return false } -func (p *PostPlaceOrderRequest) GetSlugsMap() (map[string]string, error) { +func (p *PlaceOrderRequest) GetSlugsMap() (map[string]string, error) { slugs := map[string]string{} params, err := p.GetSlugParameters() if err != nil { @@ -496,7 +496,7 @@ func (p *PostPlaceOrderRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } -func (p *PostPlaceOrderRequest) Do(ctx context.Context) (*PlaceOrderResponse, error) { +func (p *PlaceOrderRequest) Do(ctx context.Context) (*PlaceOrderResponse, error) { params, err := p.GetParameters() if err != nil { From b02ac837ea3e7d04eae9c843e983a8b673f1e939 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 27 Jul 2023 16:28:54 +0800 Subject: [PATCH 1265/1392] max: handle SelfTradeBidFeeDiscounted --- pkg/exchange/max/convert.go | 2 ++ pkg/exchange/max/maxapi/v3/trade.go | 35 +++++++++++++++-------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 4c578bde6b..b6ea59ba2e 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -211,6 +211,7 @@ func toGlobalTradeV3(t v3.Trade) ([]types.Trade, error) { IsMaker: t.IsMaker(), Fee: t.Fee, FeeCurrency: toGlobalCurrency(t.FeeCurrency), + FeeDiscounted: t.FeeDiscounted, QuoteQuantity: t.Funds, Time: types.Time(t.CreatedAt), IsMargin: isMargin, @@ -227,6 +228,7 @@ func toGlobalTradeV3(t v3.Trade) ([]types.Trade, error) { bidTrade.OrderID = t.SelfTradeBidOrderID bidTrade.Fee = t.SelfTradeBidFee bidTrade.FeeCurrency = toGlobalCurrency(t.SelfTradeBidFeeCurrency) + bidTrade.FeeDiscounted = t.SelfTradeBidFeeDiscounted bidTrade.IsBuyer = !trade.IsBuyer bidTrade.IsMaker = !trade.IsMaker trades = append(trades, bidTrade) diff --git a/pkg/exchange/max/maxapi/v3/trade.go b/pkg/exchange/max/maxapi/v3/trade.go index 1774e4ba57..d5569deb6c 100644 --- a/pkg/exchange/max/maxapi/v3/trade.go +++ b/pkg/exchange/max/maxapi/v3/trade.go @@ -13,23 +13,24 @@ const ( ) type Trade struct { - ID uint64 `json:"id" db:"exchange_id"` - WalletType WalletType `json:"wallet_type,omitempty"` - Price fixedpoint.Value `json:"price"` - Volume fixedpoint.Value `json:"volume"` - Funds fixedpoint.Value `json:"funds"` - Market string `json:"market"` - MarketName string `json:"market_name"` - CreatedAt types.MillisecondTimestamp `json:"created_at"` - Side string `json:"side"` - OrderID uint64 `json:"order_id"` - Fee fixedpoint.Value `json:"fee"` // float number as string - FeeCurrency string `json:"fee_currency"` - FeeDiscounted bool `json:"fee_discounted"` - Liquidity Liquidity `json:"liquidity"` - SelfTradeBidFee fixedpoint.Value `json:"self_trade_bid_fee"` - SelfTradeBidFeeCurrency string `json:"self_trade_bid_fee_currency"` - SelfTradeBidOrderID uint64 `json:"self_trade_bid_order_id"` + ID uint64 `json:"id" db:"exchange_id"` + WalletType WalletType `json:"wallet_type,omitempty"` + Price fixedpoint.Value `json:"price"` + Volume fixedpoint.Value `json:"volume"` + Funds fixedpoint.Value `json:"funds"` + Market string `json:"market"` + MarketName string `json:"market_name"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + Side string `json:"side"` + OrderID uint64 `json:"order_id"` + Fee fixedpoint.Value `json:"fee"` // float number as string + FeeCurrency string `json:"fee_currency"` + FeeDiscounted bool `json:"fee_discounted"` + Liquidity Liquidity `json:"liquidity"` + SelfTradeBidFee fixedpoint.Value `json:"self_trade_bid_fee"` + SelfTradeBidFeeCurrency string `json:"self_trade_bid_fee_currency"` + SelfTradeBidFeeDiscounted bool `json:"self_trade_bid_fee_discounted"` + SelfTradeBidOrderID uint64 `json:"self_trade_bid_order_id"` } func (t Trade) IsBuyer() bool { From d8b8e7f2ace2cb5424977306a461a3a8fe08378d Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 27 Jul 2023 17:35:33 +0800 Subject: [PATCH 1266/1392] pkg/exchange: rename OpenOrders to Orders --- .../bybit/bybitapi/get_open_order_request.go | 14 ++++++++------ .../bybitapi/get_open_orders_request_requestgen.go | 6 +++--- pkg/exchange/bybit/convert.go | 2 +- pkg/exchange/bybit/convert_test.go | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pkg/exchange/bybit/bybitapi/get_open_order_request.go b/pkg/exchange/bybit/bybitapi/get_open_order_request.go index e56ea01c10..3634d005f6 100644 --- a/pkg/exchange/bybit/bybitapi/get_open_order_request.go +++ b/pkg/exchange/bybit/bybitapi/get_open_order_request.go @@ -9,13 +9,13 @@ import ( //go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result //go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -type OpenOrdersResponse struct { - List []OpenOrder `json:"list"` - NextPageCursor string `json:"nextPageCursor"` - Category string `json:"category"` +type OrdersResponse struct { + List []Order `json:"list"` + NextPageCursor string `json:"nextPageCursor"` + Category string `json:"category"` } -type OpenOrder struct { +type Order struct { OrderId string `json:"orderId"` OrderLinkId string `json:"orderLinkId"` BlockTradeId string `json:"blockTradeId"` @@ -59,7 +59,7 @@ type OpenOrder struct { UpdatedTime types.MillisecondTimestamp `json:"updatedTime"` } -//go:generate GetRequest -url "/v5/order/realtime" -type GetOpenOrdersRequest -responseDataType .OpenOrdersResponse +//go:generate GetRequest -url "/v5/order/realtime" -type GetOpenOrdersRequest -responseDataType .OrdersResponse type GetOpenOrdersRequest struct { client requestgen.AuthenticatedAPIClient @@ -75,6 +75,8 @@ type GetOpenOrdersRequest struct { cursor *string `param:"cursor,query"` } +// NewGetOpenOrderRequest queries unfilled or partially filled orders in real-time. To query older order records, +// please use the order history interface. func (c *RestClient) NewGetOpenOrderRequest() *GetOpenOrdersRequest { return &GetOpenOrdersRequest{ client: c, diff --git a/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go index d68c31f4d5..aa9d13cc63 100644 --- a/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go +++ b/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/order/realtime -type GetOpenOrdersRequest -responseDataType .OpenOrdersResponse"; DO NOT EDIT. +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/order/realtime -type GetOpenOrdersRequest -responseDataType .OrdersResponse"; DO NOT EDIT. package bybitapi @@ -258,7 +258,7 @@ func (g *GetOpenOrdersRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } -func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OpenOrdersResponse, error) { +func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OrdersResponse, error) { // no body params var params interface{} @@ -283,7 +283,7 @@ func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OpenOrdersResponse, err if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } - var data OpenOrdersResponse + var data OrdersResponse if err := json.Unmarshal(apiResponse.Result, &data); err != nil { return nil, err } diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index 85ce0c013f..add5def315 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -46,7 +46,7 @@ func toGlobalTicker(stats bybitapi.Ticker, time time.Time) types.Ticker { } } -func toGlobalOrder(order bybitapi.OpenOrder) (*types.Order, error) { +func toGlobalOrder(order bybitapi.Order) (*types.Order, error) { side, err := toGlobalSideType(order.Side) if err != nil { return nil, err diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index c5c8b497c5..eddbce7ccd 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -178,7 +178,7 @@ func TestToGlobalOrder(t *testing.T) { // "UpdatedTime": "2023-07-25 17:12:57.868 +0800 CST" //} timeNow := time.Now() - openOrder := bybitapi.OpenOrder{ + openOrder := bybitapi.Order{ OrderId: "1472539279335923200", OrderLinkId: "1690276361150", BlockTradeId: "", From 1760a5b8d68d69f8a80e266597a4ff9491332323 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 27 Jul 2023 17:39:50 +0800 Subject: [PATCH 1267/1392] pkg/exchange: try to parse order id to integer --- pkg/exchange/bybit/bybitapi/client_test.go | 3 +++ pkg/exchange/bybit/convert.go | 10 +++++++++- pkg/exchange/bybit/convert_test.go | 5 ++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index f74eb22959..1fbc136e39 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -92,6 +92,9 @@ func TestClient(t *testing.T) { assert.NoError(t, err) t.Logf("apiResp: %+v", apiResp) + _, err = strconv.ParseUint(apiResp.OrderId, 10, 64) + assert.NoError(t, err) + ordersResp, err := client.NewGetOpenOrderRequest().OrderLinkId(apiResp.OrderLinkId).Do(ctx) assert.NoError(t, err) assert.Equal(t, len(ordersResp.List), 1) diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index add5def315..b70a052012 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -4,6 +4,7 @@ import ( "fmt" "hash/fnv" "math" + "strconv" "time" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" @@ -67,6 +68,13 @@ func toGlobalOrder(order bybitapi.Order) (*types.Order, error) { if err != nil { return nil, err } + // linear and inverse : 42f4f364-82e1-49d3-ad1d-cd8cf9aa308d (UUID format) + // spot : 1468264727470772736 (only numbers) + // Now we only use spot trading. + orderIdNum, err := strconv.ParseUint(order.OrderId, 10, 64) + if err != nil { + return nil, fmt.Errorf("unexpected order id: %s, err: %v", order.OrderId, err) + } return &types.Order{ SubmitOrder: types.SubmitOrder{ @@ -79,7 +87,7 @@ func toGlobalOrder(order bybitapi.Order) (*types.Order, error) { TimeInForce: timeInForce, }, Exchange: types.ExchangeBybit, - OrderID: hashStringID(order.OrderId), + OrderID: orderIdNum, UUID: order.OrderId, Status: status, ExecutedQuantity: order.CumExecQty, diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index eddbce7ccd..f67bc90034 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -3,6 +3,7 @@ package bybit import ( "fmt" "math" + "strconv" "testing" "time" @@ -231,6 +232,8 @@ func TestToGlobalOrder(t *testing.T) { assert.NoError(t, err) working, err := isWorking(openOrder.OrderStatus) assert.NoError(t, err) + orderIdNum, err := strconv.ParseUint(openOrder.OrderId, 10, 64) + assert.NoError(t, err) exp := types.Order{ SubmitOrder: types.SubmitOrder{ @@ -243,7 +246,7 @@ func TestToGlobalOrder(t *testing.T) { TimeInForce: tif, }, Exchange: types.ExchangeBybit, - OrderID: hashStringID(openOrder.OrderId), + OrderID: orderIdNum, UUID: openOrder.OrderId, Status: status, ExecutedQuantity: openOrder.CumExecQty, From f25ab567eb6f72e8a9955eaddb830dc0327f757a Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 27 Jul 2023 18:35:58 +0800 Subject: [PATCH 1268/1392] pkg/exhcange: return err on max queryClosedOrdersByLastOrderID --- pkg/exchange/max/exchange.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 70d6ddcafc..33671bf7ea 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -305,8 +305,10 @@ func (e *Exchange) queryClosedOrdersByLastOrderID(ctx context.Context, symbol st orders = append(orders, *order) } - orders = types.SortOrdersAscending(orders) - return orders, nil + if err != nil { + return nil, err + } + return types.SortOrdersAscending(orders), nil } func (e *Exchange) CancelAllOrders(ctx context.Context) ([]types.Order, error) { From 23dddab8fb5e4fe181230b564fcfb49ca49490d7 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 28 Jul 2023 09:45:01 +0800 Subject: [PATCH 1269/1392] update readme --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 534af12b30..91c73b334b 100644 --- a/README.md +++ b/README.md @@ -98,27 +98,27 @@ the implementation. | grid2 | the second generation grid strategy, it can convert your quote asset into a grid, supports base+quote mode | maker | | | bollgrid | strategy implements a basic grid strategy with the built-in bollinger indicator | maker | | | xmaker | cross exchange market making strategy, it hedges your inventory risk on the other side | maker | no | -| xnav | this strategy helps you record the current net asset value | | | -| xalign | this strategy aligns your balance position automatically | | | -| xfunding | a funding rate fee strategy | | | +| xnav | this strategy helps you record the current net asset value | tool | | +| xalign | this strategy aligns your balance position automatically | tool | | +| xfunding | a funding rate fee strategy | funding | | | autoborrow | this strategy uses margin to borrow assets, to help you keep the minimal balance | tool | no | | pivotshort | this strategy finds the pivot low and entry the trade when the price breaks the previous low | long/short | | | schedule | this strategy buy/sell with a fixed quantity periodically, you can use this as a single DCA, or to refill the fee asset like BNB. | tool | | irr | this strategy opens the position based on the predicated return rate | long/short | | | bollmaker | this strategy holds a long-term long/short position, places maker orders on both side, uses bollinger band to control the position size | maker | | -| wall | this strategy creates wall (large amount order) on the order book | | | -| scmaker | this market making strategy is desgiend for stable coin markets, like USDC/USDT | | | +| wall | this strategy creates wall (large amount order) on the order book | maker | | +| scmaker | this market making strategy is desgiend for stable coin markets, like USDC/USDT | maker | | | drift | | long/short | | | rsicross | this strategy opens a long position when the fast rsi cross over the slow rsi, this is a demo strategy for using the v2 indicator | long/short | | | marketcap | this strategy implements a strategy that rebalances the portfolio based on the market capitalization | rebalance | no | | supertrend | this strategy uses DEMA and Supertrend indicator to open the long/short position | long/short | | -| trendtrader | this strategy opens long/short position based on the trendline breakout | | | -| elliottwave | | | | -| ewoDgtrd | | | | -| fixedmaker | | | | -| factoryzoo | | | | -| fmaker | | | | -| linregmaker | a linear regression based market maker | | | +| trendtrader | this strategy opens long/short position based on the trendline breakout | long/short | | +| elliottwave | | long/short | | +| ewoDgtrd | | long/short | | +| fixedmaker | | maker | | +| factoryzoo | | long/short | | +| fmaker | | maker | | +| linregmaker | a linear regression based market maker | maker | | From d2ad504579b3e608a990d675d4028826114f7d31 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 27 Jul 2023 17:44:47 +0800 Subject: [PATCH 1270/1392] pkg/exchange: add QueryClosedOrders --- pkg/exchange/bybit/bybitapi/client_test.go | 23 ++ .../get_order_histories_request_requestgen.go | 282 ++++++++++++++++++ .../bybitapi/get_order_history_request.go | 51 ++++ pkg/exchange/bybit/convert_test.go | 15 + pkg/exchange/bybit/exchange.go | 64 +++- pkg/types/order.go | 6 + 6 files changed, 428 insertions(+), 13 deletions(-) create mode 100644 pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go create mode 100644 pkg/exchange/bybit/bybitapi/get_order_history_request.go diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index 1fbc136e39..0daf9eb2be 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -132,4 +132,27 @@ func TestClient(t *testing.T) { assert.Equal(t, ordersResp.List[0].OrderStatus, OrderStatusCancelled) t.Logf("apiResp: %+v", ordersResp.List[0]) }) + + t.Run("GetOrderHistoriesRequest", func(t *testing.T) { + req := client.NewPlaceOrderRequest(). + Symbol("DOTUSDT"). + Side(SideBuy). + OrderType(OrderTypeLimit). + Qty("1"). + Price("4.6"). + OrderLinkId(uuid.NewString()). + TimeInForce(TimeInForceGTC) + apiResp, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %+v", apiResp) + + ordersResp, err := client.NewGetOpenOrderRequest().OrderLinkId(apiResp.OrderLinkId).Do(ctx) + assert.NoError(t, err) + assert.Equal(t, len(ordersResp.List), 1) + t.Logf("apiResp: %+v", ordersResp.List[0]) + + orderResp, err := client.NewGetOrderHistoriesRequest().Symbol("DOTUSDT").Cursor("0").Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %#v", orderResp) + }) } diff --git a/pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go new file mode 100644 index 0000000000..4fafe108b6 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go @@ -0,0 +1,282 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/order/history -type GetOrderHistoriesRequest -responseDataType .OrdersResponse"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetOrderHistoriesRequest) Category(category Category) *GetOrderHistoriesRequest { + g.category = category + return g +} + +func (g *GetOrderHistoriesRequest) Symbol(symbol string) *GetOrderHistoriesRequest { + g.symbol = &symbol + return g +} + +func (g *GetOrderHistoriesRequest) OrderId(orderId string) *GetOrderHistoriesRequest { + g.orderId = &orderId + return g +} + +func (g *GetOrderHistoriesRequest) OrderFilter(orderFilter string) *GetOrderHistoriesRequest { + g.orderFilter = &orderFilter + return g +} + +func (g *GetOrderHistoriesRequest) OrderStatus(orderStatus OrderStatus) *GetOrderHistoriesRequest { + g.orderStatus = &orderStatus + return g +} + +func (g *GetOrderHistoriesRequest) StartTime(startTime time.Time) *GetOrderHistoriesRequest { + g.startTime = &startTime + return g +} + +func (g *GetOrderHistoriesRequest) EndTime(endTime time.Time) *GetOrderHistoriesRequest { + g.endTime = &endTime + return g +} + +func (g *GetOrderHistoriesRequest) Limit(limit uint64) *GetOrderHistoriesRequest { + g.limit = &limit + return g +} + +func (g *GetOrderHistoriesRequest) Cursor(cursor string) *GetOrderHistoriesRequest { + g.cursor = &cursor + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetOrderHistoriesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check category field -> json key category + category := g.category + + // TEMPLATE check-valid-values + switch category { + case "spot": + params["category"] = category + + default: + return nil, fmt.Errorf("category value %v is invalid", category) + + } + // END TEMPLATE check-valid-values + + // assign parameter of category + params["category"] = category + // check symbol field -> json key symbol + if g.symbol != nil { + symbol := *g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + } else { + } + // check orderId field -> json key orderId + if g.orderId != nil { + orderId := *g.orderId + + // assign parameter of orderId + params["orderId"] = orderId + } else { + } + // check orderFilter field -> json key orderFilter + if g.orderFilter != nil { + orderFilter := *g.orderFilter + + // assign parameter of orderFilter + params["orderFilter"] = orderFilter + } else { + } + // check orderStatus field -> json key orderStatus + if g.orderStatus != nil { + orderStatus := *g.orderStatus + + // TEMPLATE check-valid-values + switch orderStatus { + case OrderStatusCreated, OrderStatusNew, OrderStatusRejected, OrderStatusPartiallyFilled, OrderStatusPartiallyFilledCanceled, OrderStatusFilled, OrderStatusCancelled, OrderStatusDeactivated, OrderStatusActive: + params["orderStatus"] = orderStatus + + default: + return nil, fmt.Errorf("orderStatus value %v is invalid", orderStatus) + + } + // END TEMPLATE check-valid-values + + // assign parameter of orderStatus + params["orderStatus"] = orderStatus + } else { + } + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check cursor field -> json key cursor + if g.cursor != nil { + cursor := *g.cursor + + // assign parameter of cursor + params["cursor"] = cursor + } else { + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetOrderHistoriesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetOrderHistoriesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetOrderHistoriesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetOrderHistoriesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetOrderHistoriesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetOrderHistoriesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetOrderHistoriesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetOrderHistoriesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetOrderHistoriesRequest) Do(ctx context.Context) (*OrdersResponse, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v5/order/history" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data OrdersResponse + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bybit/bybitapi/get_order_history_request.go b/pkg/exchange/bybit/bybitapi/get_order_history_request.go new file mode 100644 index 0000000000..7d46c8eff1 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_order_history_request.go @@ -0,0 +1,51 @@ +package bybitapi + +import ( + "time" + + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +//go:generate GetRequest -url "/v5/order/history" -type GetOrderHistoriesRequest -responseDataType .OpenOrdersResponse +type GetOrderHistoriesRequest struct { + client requestgen.AuthenticatedAPIClient + + category Category `param:"category,query" validValues:"spot"` + + symbol *string `param:"symbol,query"` + orderId *string `param:"orderId,query"` + // orderFilter supports 3 types of Order: + // 1. active order, 2. StopOrder: conditional order, 3. tpslOrder: spot TP/SL order + // Normal spot: return Order active order by default + // Others: all kinds of orders by default + orderFilter *string `param:"orderFilter,query"` + // orderStatus if the account belongs to Normal spot, orderStatus is not supported. + //// For other accounts, return all status orders if not explicitly passed. + orderStatus *OrderStatus `param:"orderStatus,query"` + + // startTime must + // Normal spot is not supported temporarily + // startTime and endTime must be passed together + // If not passed, query the past 7 days data by default + // For each request, startTime and endTime interval should be less then 7 days + startTime *time.Time `param:"startTime,query,milliseconds"` + + // endTime for each request, startTime and endTime interval should be less then 7 days + endTime *time.Time `param:"endTime,query,milliseconds"` + + // limit for data size per page. [1, 50]. Default: 20 + limit *uint64 `param:"limit,query"` + // cursor uses the nextPageCursor token from the response to retrieve the next page of the result set + cursor *string `param:"cursor,query"` +} + +// NewGetOrderHistoriesRequest is descending order by createdTime +func (c *RestClient) NewGetOrderHistoriesRequest() *GetOrderHistoriesRequest { + return &GetOrderHistoriesRequest{ + client: c, + category: CategorySpot, + } +} diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index f67bc90034..e4bffcf302 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -1,7 +1,10 @@ package bybit import ( + "context" "fmt" + "github.com/pkg/errors" + "go.uber.org/multierr" "math" "strconv" "testing" @@ -14,6 +17,18 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +func TestU(t *testing.T) { + e := returnErr() + + t.Log(errors.Is(e, context.DeadlineExceeded)) + +} + +func returnErr() error { + var err error + return multierr.Append(multierr.Append(err, fmt.Errorf("got err: %w", context.DeadlineExceeded)), fmt.Errorf("GG")) +} + func TestToGlobalMarket(t *testing.T) { // sample: //{ diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index a8a75b85b8..33a68f2dca 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -3,6 +3,7 @@ package bybit import ( "context" "fmt" + "strconv" "time" "github.com/sirupsen/logrus" @@ -14,7 +15,8 @@ import ( ) const ( - maxOrderIdLen = 36 + maxOrderIdLen = 36 + defaultQueryClosedLen = 50 ) // https://bybit-exchange.github.io/docs/zh-TW/v5/rate-limit @@ -26,6 +28,7 @@ var ( sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2) tradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5) orderRateLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) + closedRateLimiter = rate.NewLimiter(rate.Every(time.Second), 1) log = logrus.WithFields(logrus.Fields{ "exchange": "bybit", @@ -67,7 +70,7 @@ func (e *Exchange) PlatformFeeCurrency() string { func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { if err := sharedRateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("markets rate limiter wait error: %v", err) + return nil, fmt.Errorf("markets rate limiter wait error: %w", err) } instruments, err := e.client.NewGetInstrumentsInfoRequest().Do(ctx) @@ -85,12 +88,12 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { if err := sharedRateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("ticker order rate limiter wait error: %v", err) + return nil, fmt.Errorf("ticker order rate limiter wait error: %w", err) } s, err := e.client.NewGetTickersRequest().Symbol(symbol).DoWithResponseTime(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to call ticker, symbol: %s, err: %w", symbol, err) } if len(s.List) != 1 { @@ -117,11 +120,11 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[str } if err := sharedRateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("tickers rate limiter wait error: %v", err) + return nil, fmt.Errorf("tickers rate limiter wait error: %w", err) } allTickers, err := e.client.NewGetTickersRequest().DoWithResponseTime(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to call ticker, err: %w", err) } for _, s := range allTickers.List { @@ -141,11 +144,11 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ } if err = tradeRateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("place order rate limiter wait error: %v", err) + return nil, fmt.Errorf("place order rate limiter wait error: %w", err) } res, err := req.Do(ctx) if err != nil { - return nil, fmt.Errorf("failed to query open orders, err: %v", err) + return nil, fmt.Errorf("failed to query open orders, err: %w", err) } for _, order := range res.List { @@ -214,11 +217,11 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t req.OrderLinkId(order.ClientOrderID) if err := orderRateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("place order rate limiter wait error: %v", err) + return nil, fmt.Errorf("place order rate limiter wait error: %w", err) } res, err := req.Do(ctx) if err != nil { - return nil, fmt.Errorf("failed to place order, order: %#v, err: %v", order, err) + return nil, fmt.Errorf("failed to place order, order: %#v, err: %w", order, err) } if len(res.OrderId) == 0 || res.OrderLinkId != order.ClientOrderID { @@ -227,7 +230,7 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t ordersResp, err := e.client.NewGetOpenOrderRequest().OrderLinkId(res.OrderLinkId).Do(ctx) if err != nil { - return nil, fmt.Errorf("failed to query order by client order id: %s", res.OrderLinkId) + return nil, fmt.Errorf("failed to query order by client order id: %s, err: %w", res.OrderLinkId, err) } if len(ordersResp.List) != 1 { @@ -261,12 +264,12 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err req.Symbol(order.Market.Symbol) if err := orderRateLimiter.Wait(ctx); err != nil { - errs = multierr.Append(errs, fmt.Errorf("cancel order rate limiter wait, order id: %s, error: %v", order.ClientOrderID, err)) + errs = multierr.Append(errs, fmt.Errorf("cancel order rate limiter wait, order id: %s, error: %w", order.ClientOrderID, err)) continue } res, err := req.Do(ctx) if err != nil { - errs = multierr.Append(errs, fmt.Errorf("failed to cancel order id: %s, err: %v", order.ClientOrderID, err)) + errs = multierr.Append(errs, fmt.Errorf("failed to cancel order id: %s, err: %w", order.ClientOrderID, err)) continue } if res.OrderId != order.UUID || res.OrderLinkId != order.ClientOrderID { @@ -277,3 +280,38 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err return errs } + +func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, util time.Time, lastOrderID uint64) (orders []types.Order, err error) { + if !since.IsZero() || !util.IsZero() { + log.Warn("!!!BYBIT EXCHANGE API NOTICE!!! the since/until conditions will not be effected on SPOT account, bybit exchange does not support time-range-based query currently") + } + + if err := closedRateLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("query closed order rate limiter wait error: %w", err) + } + res, err := e.client.NewGetOrderHistoriesRequest(). + Symbol(symbol). + Cursor(strconv.FormatUint(lastOrderID, 10)). + Limit(defaultQueryClosedLen). + Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to call get order histories error: %w", err) + } + + for _, order := range res.List { + o, err2 := toGlobalOrder(order) + if err2 != nil { + err = multierr.Append(err, err2) + continue + } + + if o.Status.Closed() { + orders = append(orders, *o) + } + } + if err != nil { + return nil, err + } + + return types.SortOrdersAscending(orders), nil +} diff --git a/pkg/types/order.go b/pkg/types/order.go index 369b8889b1..5e29fdd36a 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -116,6 +116,12 @@ const ( OrderStatusRejected OrderStatus = "REJECTED" ) +func (o OrderStatus) Closed() bool { + return o == OrderStatusFilled || + o == OrderStatusCanceled || + o == OrderStatusRejected +} + type SubmitOrder struct { ClientOrderID string `json:"clientOrderID,omitempty" db:"client_order_id"` From 4eefe72cb68eae1510b801acd96fd3d5fc01b8be Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 28 Jul 2023 14:41:36 +0800 Subject: [PATCH 1271/1392] service: fix db reflection --- pkg/service/reflect.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/service/reflect.go b/pkg/service/reflect.go index b60c0c3371..1f8ce68a6e 100644 --- a/pkg/service/reflect.go +++ b/pkg/service/reflect.go @@ -40,7 +40,7 @@ func placeholdersOf(record interface{}) []string { for i := 0; i < rt.NumField(); i++ { fieldType := rt.Field(i) if tag, ok := fieldType.Tag.Lookup("db"); ok { - if tag == "gid" { + if tag == "gid" || tag == "-" || tag == "" { continue } @@ -65,7 +65,7 @@ func fieldsNamesOf(record interface{}) []string { for i := 0; i < rt.NumField(); i++ { fieldType := rt.Field(i) if tag, ok := fieldType.Tag.Lookup("db"); ok { - if tag == "gid" { + if tag == "gid" || tag == "-" || tag == "" { continue } From 86c643b51383fb248e4816f745808c0f6ccc56bd Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 28 Jul 2023 17:47:35 +0800 Subject: [PATCH 1272/1392] pkx/exchange: fix batch query trade missing time range --- pkg/exchange/batch/trade.go | 2 + pkg/exchange/batch/trade_test.go | 140 +++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 pkg/exchange/batch/trade_test.go diff --git a/pkg/exchange/batch/trade.go b/pkg/exchange/batch/trade.go index 43603a407c..1c91da7773 100644 --- a/pkg/exchange/batch/trade.go +++ b/pkg/exchange/batch/trade.go @@ -28,6 +28,8 @@ func (e TradeBatchQuery) Query(ctx context.Context, symbol string, options *type query := &AsyncTimeRangedBatchQuery{ Type: types.Trade{}, Q: func(startTime, endTime time.Time) (interface{}, error) { + options.StartTime = &startTime + options.EndTime = &endTime return e.ExchangeTradeHistoryService.QueryTrades(ctx, symbol, options) }, T: func(obj interface{}) time.Time { diff --git a/pkg/exchange/batch/trade_test.go b/pkg/exchange/batch/trade_test.go new file mode 100644 index 0000000000..1b704136c2 --- /dev/null +++ b/pkg/exchange/batch/trade_test.go @@ -0,0 +1,140 @@ +package batch + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/mocks" +) + +func Test_TradeBatchQuery(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var ( + ctx = context.Background() + timeNow = time.Now() + startTime = timeNow.Add(-24 * time.Hour) + endTime = timeNow + expSymbol = "BTCUSDT" + queryTrades1 = []types.Trade{ + { + ID: 1, + Time: types.Time(startTime.Add(time.Hour)), + }, + } + queryTrades2 = []types.Trade{ + { + ID: 2, + Time: types.Time(startTime.Add(time.Hour)), + }, + { + ID: 3, + Time: types.Time(startTime.Add(2 * time.Hour)), + }, + } + emptyTrades = []types.Trade{} + allRes = append(queryTrades1, queryTrades2...) + ) + + t.Run("succeeds", func(t *testing.T) { + var ( + expOptions = &types.TradeQueryOptions{ + StartTime: &startTime, + EndTime: &endTime, + LastTradeID: 0, + Limit: 50, + } + mockExchange = mocks.NewMockExchangeTradeHistoryService(ctrl) + ) + + mockExchange.EXPECT().QueryTrades(ctx, expSymbol, expOptions).DoAndReturn( + func(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { + assert.Equal(t, startTime, *options.StartTime) + assert.Equal(t, endTime, *options.EndTime) + assert.Equal(t, uint64(0), options.LastTradeID) + assert.Equal(t, expOptions.Limit, options.Limit) + return queryTrades1, nil + }).Times(1) + mockExchange.EXPECT().QueryTrades(ctx, expSymbol, expOptions).DoAndReturn( + func(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { + assert.Equal(t, queryTrades1[0].Time.Time(), *options.StartTime) + assert.Equal(t, endTime, *options.EndTime) + assert.Equal(t, queryTrades1[0].ID, options.LastTradeID) + assert.Equal(t, expOptions.Limit, options.Limit) + return queryTrades2, nil + }).Times(1) + mockExchange.EXPECT().QueryTrades(ctx, expSymbol, expOptions).DoAndReturn( + func(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { + assert.Equal(t, queryTrades2[1].Time.Time(), *options.StartTime) + assert.Equal(t, endTime, *options.EndTime) + assert.Equal(t, queryTrades2[1].ID, options.LastTradeID) + assert.Equal(t, expOptions.Limit, options.Limit) + return emptyTrades, nil + }).Times(1) + + tradeBatchQuery := &TradeBatchQuery{ExchangeTradeHistoryService: mockExchange} + + resCh, errC := tradeBatchQuery.Query(ctx, expSymbol, expOptions) + wg := sync.WaitGroup{} + wg.Add(1) + rcvCount := 0 + go func() { + defer wg.Done() + for ch := range resCh { + assert.Equal(t, allRes[rcvCount], ch) + rcvCount++ + } + assert.NoError(t, <-errC) + }() + wg.Wait() + assert.Equal(t, rcvCount, len(allRes)) + }) + + t.Run("failed to call query trades", func(t *testing.T) { + var ( + expOptions = &types.TradeQueryOptions{ + StartTime: &startTime, + EndTime: &endTime, + LastTradeID: 0, + Limit: 50, + } + mockExchange = mocks.NewMockExchangeTradeHistoryService(ctrl) + unknownErr = errors.New("unknown err") + ) + + mockExchange.EXPECT().QueryTrades(ctx, expSymbol, expOptions).DoAndReturn( + func(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { + assert.Equal(t, startTime, *options.StartTime) + assert.Equal(t, endTime, *options.EndTime) + assert.Equal(t, uint64(0), options.LastTradeID) + assert.Equal(t, expOptions.Limit, options.Limit) + return nil, unknownErr + }).Times(1) + + tradeBatchQuery := &TradeBatchQuery{ExchangeTradeHistoryService: mockExchange} + + resCh, errC := tradeBatchQuery.Query(ctx, expSymbol, expOptions) + wg := sync.WaitGroup{} + wg.Add(1) + rcvCount := 0 + go func() { + defer wg.Done() + for ch := range resCh { + assert.Equal(t, allRes[rcvCount], ch) + rcvCount++ + } + assert.Equal(t, 0, rcvCount) + assert.Equal(t, unknownErr, <-errC) + }() + wg.Wait() + assert.Equal(t, rcvCount, 0) + }) +} From b0ccc7e51bdd9c0f3f42cd54c53630fec7255514 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Mon, 31 Jul 2023 11:00:38 +0900 Subject: [PATCH 1273/1392] use &PublicDataService{} to create it as a pointer object and rename ser to srv --- pkg/exchange/okex/okexapi/client_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index f2c9d3e68b..a5abe42109 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -31,8 +31,8 @@ func TestClient_GetInstrumentsRequest(t *testing.T) { client := NewClient() ctx := context.Background() - ser := PublicDataService{client: client} - req := ser.NewGetInstrumentsRequest() + srv := &PublicDataService{client: client} + req := srv.NewGetInstrumentsRequest() instruments, err := req. InstrumentType(InstrumentTypeSpot). @@ -45,8 +45,8 @@ func TestClient_GetInstrumentsRequest(t *testing.T) { func TestClient_GetFundingRateRequest(t *testing.T) { client := NewClient() ctx := context.Background() - ser := PublicDataService{client: client} - req := ser.NewGetFundingRate() + srv := &PublicDataService{client: client} + req := srv.NewGetFundingRate() instrument, err := req. InstrumentID("BTC-USDT-SWAP"). @@ -59,8 +59,8 @@ func TestClient_GetFundingRateRequest(t *testing.T) { func TestClient_PlaceOrderRequest(t *testing.T) { client := getTestClientOrSkip(t) ctx := context.Background() - ser := TradeService{client: client} - req := ser.NewPlaceOrderRequest() + srv := &TradeService{client: client} + req := srv.NewPlaceOrderRequest() order, err := req. InstrumentID("XTZ-BTC"). From f2109afa0e72c1cd5e3cf92ce403055874d5626d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 31 Jul 2023 17:54:34 +0800 Subject: [PATCH 1274/1392] add last 30 days to loose date support --- pkg/types/time.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/types/time.go b/pkg/types/time.go index 5bc811c85b..c6e912cc55 100644 --- a/pkg/types/time.go +++ b/pkg/types/time.go @@ -256,6 +256,10 @@ func ParseLooseFormatTime(s string) (LooseFormatTime, error) { t = time.Now().AddDate(0, -1, 0) return LooseFormatTime(t), nil + case "last 30 days": + t = time.Now().AddDate(0, 0, -30) + return LooseFormatTime(t), nil + case "last year": t = time.Now().AddDate(-1, 0, 0) return LooseFormatTime(t), nil @@ -357,4 +361,3 @@ func BeginningOfTheDay(t time.Time) time.Time { func Over24Hours(since time.Time) bool { return time.Since(since) >= 24*time.Hour } - From 43b8e7870da287acfcff7b9833bcc4a57d2770cb Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 31 Jul 2023 18:06:20 +0800 Subject: [PATCH 1275/1392] grid2: ignore discounted trades --- pkg/strategy/grid2/trade.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/grid2/trade.go b/pkg/strategy/grid2/trade.go index 381744ccd6..c2a4d8eb67 100644 --- a/pkg/strategy/grid2/trade.go +++ b/pkg/strategy/grid2/trade.go @@ -9,6 +9,10 @@ import ( func collectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { fees := make(map[string]fixedpoint.Value) for _, t := range trades { + if t.FeeDiscounted { + continue + } + if fee, ok := fees[t.FeeCurrency]; ok { fees[t.FeeCurrency] = fee.Add(t.Fee) } else { From 4560b475560801fe815cc21d10e2733a8435474c Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 31 Jul 2023 18:12:28 +0800 Subject: [PATCH 1276/1392] grid2: only for positive non-zero fee --- pkg/strategy/grid2/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6a38c5f1f7..e8247099bd 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -484,7 +484,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { // use the profit to buy more inventory in the grid if s.Compound || s.EarnBase { // if it's not using the platform fee currency, reduce the quote quantity for the buy order - if feeCurrency == s.Market.QuoteCurrency { + if feeCurrency == s.Market.QuoteCurrency && fee.Sign() > 0 { orderExecutedQuoteAmount = orderExecutedQuoteAmount.Sub(fee) } @@ -517,7 +517,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { } } - if feeCurrency == s.Market.BaseCurrency { + if feeCurrency == s.Market.BaseCurrency && fee.Sign() > 0 { newQuantity = newQuantity.Sub(fee) } From 4363f0ae7bd70b62e739abbdc0232813b5addef9 Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 28 Jul 2023 15:37:05 +0800 Subject: [PATCH 1277/1392] pkg/exchange: add query trade api --- pkg/exchange/bybit/bybitapi/v3/client.go | 13 + pkg/exchange/bybit/bybitapi/v3/client_test.go | 44 ++++ .../bybit/bybitapi/v3/get_trades_request.go | 55 ++++ .../v3/get_trades_request_requestgen.go | 238 ++++++++++++++++++ pkg/exchange/bybit/bybitapi/v3/types.go | 15 ++ pkg/exchange/bybit/convert.go | 63 ++++- pkg/exchange/bybit/convert_test.go | 76 +++++- pkg/exchange/bybit/exchange.go | 88 ++++++- 8 files changed, 581 insertions(+), 11 deletions(-) create mode 100644 pkg/exchange/bybit/bybitapi/v3/client.go create mode 100644 pkg/exchange/bybit/bybitapi/v3/client_test.go create mode 100644 pkg/exchange/bybit/bybitapi/v3/get_trades_request.go create mode 100644 pkg/exchange/bybit/bybitapi/v3/get_trades_request_requestgen.go create mode 100644 pkg/exchange/bybit/bybitapi/v3/types.go diff --git a/pkg/exchange/bybit/bybitapi/v3/client.go b/pkg/exchange/bybit/bybitapi/v3/client.go new file mode 100644 index 0000000000..9fdb889fb3 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/v3/client.go @@ -0,0 +1,13 @@ +package v3 + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" +) + +type APIResponse = bybitapi.APIResponse + +type Client struct { + Client requestgen.AuthenticatedAPIClient +} diff --git a/pkg/exchange/bybit/bybitapi/v3/client_test.go b/pkg/exchange/bybit/bybitapi/v3/client_test.go new file mode 100644 index 0000000000..5bc075c62e --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/v3/client_test.go @@ -0,0 +1,44 @@ +package v3 + +import ( + "context" + "os" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + "github.com/c9s/bbgo/pkg/testutil" +) + +func getTestClientOrSkip(t *testing.T) *bybitapi.RestClient { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + + key, secret, ok := testutil.IntegrationTestConfigured(t, "BYBIT") + if !ok { + t.Skip("BYBIT_* env vars are not configured") + return nil + } + + client, err := bybitapi.NewClient() + assert.NoError(t, err) + client.Auth(key, secret) + return client +} + +func TestClient(t *testing.T) { + client := getTestClientOrSkip(t) + v3Client := Client{Client: client} + ctx := context.Background() + + t.Run("GetTradeRequest", func(t *testing.T) { + startTime := time.Date(2023, 7, 27, 16, 13, 9, 0, time.UTC) + apiResp, err := v3Client.NewGetTradesRequest().Symbol("BTCUSDT").StartTime(startTime).Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %+v", apiResp) + }) +} diff --git a/pkg/exchange/bybit/bybitapi/v3/get_trades_request.go b/pkg/exchange/bybit/bybitapi/v3/get_trades_request.go new file mode 100644 index 0000000000..7f91487d1d --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/v3/get_trades_request.go @@ -0,0 +1,55 @@ +package v3 + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +type TradesResponse struct { + List []Trade `json:"list"` +} + +type Trade struct { + Symbol string `json:"symbol"` + Id string `json:"id"` + OrderId string `json:"orderId"` + TradeId string `json:"tradeId"` + OrderPrice fixedpoint.Value `json:"orderPrice"` + OrderQty fixedpoint.Value `json:"orderQty"` + ExecFee fixedpoint.Value `json:"execFee"` + FeeTokenId string `json:"feeTokenId"` + CreatTime types.MillisecondTimestamp `json:"creatTime"` + IsBuyer Side `json:"isBuyer"` + IsMaker OrderType `json:"isMaker"` + MatchOrderId string `json:"matchOrderId"` + MakerRebate fixedpoint.Value `json:"makerRebate"` + ExecutionTime types.MillisecondTimestamp `json:"executionTime"` + BlockTradeId string `json:"blockTradeId"` +} + +//go:generate GetRequest -url "/spot/v3/private/my-trades" -type GetTradesRequest -responseDataType .TradesResponse +type GetTradesRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol *string `param:"symbol,query"` + orderId *string `param:"orderId,query"` + // Limit default value is 50, max 50 + limit *uint64 `param:"limit,query"` + startTime *time.Time `param:"startTime,query,milliseconds"` + endTime *time.Time `param:"endTime,query,milliseconds"` + fromTradeId *string `param:"fromTradeId,query"` + toTradeId *string `param:"toTradeId,query"` +} + +func (c *Client) NewGetTradesRequest() *GetTradesRequest { + return &GetTradesRequest{ + client: c.Client, + } +} diff --git a/pkg/exchange/bybit/bybitapi/v3/get_trades_request_requestgen.go b/pkg/exchange/bybit/bybitapi/v3/get_trades_request_requestgen.go new file mode 100644 index 0000000000..00d7dd80a9 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/v3/get_trades_request_requestgen.go @@ -0,0 +1,238 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /spot/v3/private/my-trades -type GetTradesRequest -responseDataType .TradesResponse"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetTradesRequest) Symbol(symbol string) *GetTradesRequest { + g.symbol = &symbol + return g +} + +func (g *GetTradesRequest) OrderId(orderId string) *GetTradesRequest { + g.orderId = &orderId + return g +} + +func (g *GetTradesRequest) Limit(limit uint64) *GetTradesRequest { + g.limit = &limit + return g +} + +func (g *GetTradesRequest) StartTime(startTime time.Time) *GetTradesRequest { + g.startTime = &startTime + return g +} + +func (g *GetTradesRequest) EndTime(endTime time.Time) *GetTradesRequest { + g.endTime = &endTime + return g +} + +func (g *GetTradesRequest) FromTradeId(fromTradeId string) *GetTradesRequest { + g.fromTradeId = &fromTradeId + return g +} + +func (g *GetTradesRequest) ToTradeId(toTradeId string) *GetTradesRequest { + g.toTradeId = &toTradeId + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetTradesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + if g.symbol != nil { + symbol := *g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + } else { + } + // check orderId field -> json key orderId + if g.orderId != nil { + orderId := *g.orderId + + // assign parameter of orderId + params["orderId"] = orderId + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check fromTradeId field -> json key fromTradeId + if g.fromTradeId != nil { + fromTradeId := *g.fromTradeId + + // assign parameter of fromTradeId + params["fromTradeId"] = fromTradeId + } else { + } + // check toTradeId field -> json key toTradeId + if g.toTradeId != nil { + toTradeId := *g.toTradeId + + // assign parameter of toTradeId + params["toTradeId"] = toTradeId + } else { + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetTradesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetTradesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetTradesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetTradesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetTradesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetTradesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetTradesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetTradesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetTradesRequest) Do(ctx context.Context) (*TradesResponse, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/spot/v3/private/my-trades" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse bybitapi.APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data TradesResponse + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bybit/bybitapi/v3/types.go b/pkg/exchange/bybit/bybitapi/v3/types.go new file mode 100644 index 0000000000..c3f7a1b83d --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/v3/types.go @@ -0,0 +1,15 @@ +package v3 + +type Side string + +const ( + SideBuy Side = "0" + SideSell Side = "1" +) + +type OrderType string + +const ( + OrderTypeMaker OrderType = "0" + OrderTypeTaker OrderType = "1" +) diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index b70a052012..c78f6dc1ba 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -8,6 +8,7 @@ import ( "time" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi/v3" "github.com/c9s/bbgo/pkg/types" ) @@ -73,7 +74,7 @@ func toGlobalOrder(order bybitapi.Order) (*types.Order, error) { // Now we only use spot trading. orderIdNum, err := strconv.ParseUint(order.OrderId, 10, 64) if err != nil { - return nil, fmt.Errorf("unexpected order id: %s, err: %v", order.OrderId, err) + return nil, fmt.Errorf("unexpected order id: %s, err: %w", order.OrderId, err) } return &types.Order{ @@ -204,3 +205,63 @@ func toLocalSide(side types.SideType) (bybitapi.Side, error) { return "", fmt.Errorf("side type %s not supported", side) } } + +func toV3Buyer(isBuyer v3.Side) (types.SideType, error) { + switch isBuyer { + case v3.SideBuy: + return types.SideTypeBuy, nil + case v3.SideSell: + return types.SideTypeSell, nil + default: + return "", fmt.Errorf("unexpected side type: %s", isBuyer) + } +} +func toV3Maker(isMaker v3.OrderType) (bool, error) { + switch isMaker { + case v3.OrderTypeMaker: + return true, nil + case v3.OrderTypeTaker: + return false, nil + default: + return false, fmt.Errorf("unexpected order type: %s", isMaker) + } +} + +func v3ToGlobalTrade(trade v3.Trade) (*types.Trade, error) { + side, err := toV3Buyer(trade.IsBuyer) + if err != nil { + return nil, err + } + isMaker, err := toV3Maker(trade.IsMaker) + if err != nil { + return nil, err + } + + orderIdNum, err := strconv.ParseUint(trade.OrderId, 10, 64) + if err != nil { + return nil, fmt.Errorf("unexpected order id: %s, err: %w", trade.OrderId, err) + } + tradeIdNum, err := strconv.ParseUint(trade.TradeId, 10, 64) + if err != nil { + return nil, fmt.Errorf("unexpected trade id: %s, err: %w", trade.TradeId, err) + } + + return &types.Trade{ + ID: tradeIdNum, + OrderID: orderIdNum, + Exchange: types.ExchangeBybit, + Price: trade.OrderPrice, + Quantity: trade.OrderQty, + QuoteQuantity: trade.OrderPrice.Mul(trade.OrderQty), + Symbol: trade.Symbol, + Side: side, + IsBuyer: side == types.SideTypeBuy, + IsMaker: isMaker, + Time: types.Time(trade.ExecutionTime), + Fee: trade.ExecFee, + FeeCurrency: trade.FeeTokenId, + IsMargin: false, + IsFutures: false, + IsIsolated: false, + }, nil +} diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index e4bffcf302..3051a33767 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -3,16 +3,17 @@ package bybit import ( "context" "fmt" - "github.com/pkg/errors" - "go.uber.org/multierr" "math" "strconv" "testing" "time" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "go.uber.org/multierr" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + v3 "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi/v3" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -403,3 +404,74 @@ func Test_toLocalSide(t *testing.T) { assert.Error(t, fmt.Errorf("side type %s not supported", "wrong side"), err) assert.Equal(t, bybitapi.Side(""), side) } + +func Test_toGlobalTrade(t *testing.T) { + /* sample: trade + { + "Symbol":"BTCUSDT", + "Id":"1474200510090276864", + "OrderId":"1474200270671015936", + "TradeId":"2100000000031181772", + "OrderPrice":"27628", + "OrderQty":"0.007959", + "ExecFee":"0.21989125", + "FeeTokenId":"USDT", + "CreatTime":"2023-07-28 00:13:15.457 +0800 CST", + "IsBuyer":"1", + "IsMaker":"0", + "MatchOrderId":"5760912963729109504", + "MakerRebate":"0", + "ExecutionTime":"2023-07-28 00:13:15.463 +0800 CST", + "BlockTradeId": "", + } + */ + timeNow := time.Now() + trade := v3.Trade{ + Symbol: "DOTUSDT", + Id: "1474200510090276864", + OrderId: "1474200270671015936", + TradeId: "2100000000031181772", + OrderPrice: fixedpoint.NewFromFloat(27628), + OrderQty: fixedpoint.NewFromFloat(0.007959), + ExecFee: fixedpoint.NewFromFloat(0.21989125), + FeeTokenId: "USDT", + CreatTime: types.MillisecondTimestamp(timeNow), + IsBuyer: "0", + IsMaker: "0", + MatchOrderId: "5760912963729109504", + MakerRebate: fixedpoint.NewFromFloat(0), + ExecutionTime: types.MillisecondTimestamp(timeNow), + BlockTradeId: "", + } + + s, err := toV3Buyer(trade.IsBuyer) + assert.NoError(t, err) + m, err := toV3Maker(trade.IsMaker) + assert.NoError(t, err) + orderIdNum, err := strconv.ParseUint(trade.OrderId, 10, 64) + assert.NoError(t, err) + tradeId, err := strconv.ParseUint(trade.TradeId, 10, 64) + assert.NoError(t, err) + + exp := types.Trade{ + ID: tradeId, + OrderID: orderIdNum, + Exchange: types.ExchangeBybit, + Price: trade.OrderPrice, + Quantity: trade.OrderQty, + QuoteQuantity: trade.OrderPrice.Mul(trade.OrderQty), + Symbol: trade.Symbol, + Side: s, + IsBuyer: s == types.SideTypeBuy, + IsMaker: m, + Time: types.Time(timeNow), + Fee: trade.ExecFee, + FeeCurrency: trade.FeeTokenId, + IsMargin: false, + IsFutures: false, + IsIsolated: false, + } + res, err := v3ToGlobalTrade(trade) + assert.NoError(t, err) + assert.Equal(t, res, &exp) +} diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 33a68f2dca..d838283f5b 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -11,12 +11,15 @@ import ( "golang.org/x/time/rate" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + v3 "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi/v3" "github.com/c9s/bbgo/pkg/types" ) const ( - maxOrderIdLen = 36 - defaultQueryClosedLen = 50 + maxOrderIdLen = 36 + defaultQueryLimit = 50 + + halfYearDuration = 6 * 30 * 24 * time.Hour ) // https://bybit-exchange.github.io/docs/zh-TW/v5/rate-limit @@ -25,10 +28,10 @@ const ( // The default order limiter apply 2 requests per second and a 2 initial bucket // this includes QueryMarkets, QueryTicker var ( - sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2) - tradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5) - orderRateLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) - closedRateLimiter = rate.NewLimiter(rate.Every(time.Second), 1) + sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2) + tradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5) + orderRateLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) + closedOrderQueryLimiter = rate.NewLimiter(rate.Every(time.Second), 1) log = logrus.WithFields(logrus.Fields{ "exchange": "bybit", @@ -38,6 +41,7 @@ var ( type Exchange struct { key, secret string client *bybitapi.RestClient + v3client *v3.Client } func New(key, secret string) (*Exchange, error) { @@ -286,13 +290,13 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, log.Warn("!!!BYBIT EXCHANGE API NOTICE!!! the since/until conditions will not be effected on SPOT account, bybit exchange does not support time-range-based query currently") } - if err := closedRateLimiter.Wait(ctx); err != nil { + if err := closedOrderQueryLimiter.Wait(ctx); err != nil { return nil, fmt.Errorf("query closed order rate limiter wait error: %w", err) } res, err := e.client.NewGetOrderHistoriesRequest(). Symbol(symbol). Cursor(strconv.FormatUint(lastOrderID, 10)). - Limit(defaultQueryClosedLen). + Limit(defaultQueryLimit). Do(ctx) if err != nil { return nil, fmt.Errorf("failed to call get order histories error: %w", err) @@ -315,3 +319,71 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, return types.SortOrdersAscending(orders), nil } + +/* +QueryTrades queries trades by time range or trade id range. +If options.StartTime is not specified, you can only query for records in the last 7 days. +If you want to query for records older than 7 days, options.StartTime is required. +It supports to query records up to 180 days. + +If the orderId is null, fromTradeId is passed, and toTradeId is null, then the result is sorted by +ticketId in ascend. Otherwise, the result is sorted by ticketId in descend. + +** Here includes MakerRebate. If needed, let's discuss how to modify it to return in trade. ** +** StartTime and EndTime are inclusive. ** +** StartTime and EndTime cannot exceed 180 days. ** +*/ +func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { + if options.StartTime != nil && options.EndTime != nil && options.EndTime.Sub(*options.StartTime) > halfYearDuration { + return nil, fmt.Errorf("StartTime and EndTime cannot exceed 180 days, startTime: %v, endTime: %v, diff: %v", + options.StartTime.String(), + options.EndTime.String(), + options.EndTime.Sub(*options.StartTime)/24) + } + + // using v3 client, since the v5 API does not support feeCurrency. + req := e.v3client.NewGetTradesRequest() + req.Symbol(symbol) + + if options.StartTime != nil || options.EndTime != nil { + if options.StartTime != nil { + req.StartTime(options.StartTime.UTC()) + } + if options.EndTime != nil { + req.EndTime(options.EndTime.UTC()) + } + } else { + req.FromTradeId(strconv.FormatUint(options.LastTradeID, 10)) + } + + limit := uint64(options.Limit) + if limit > defaultQueryLimit || limit <= 0 { + log.Debugf("limtit is exceeded or zero, update to %d, got: %d", defaultQueryLimit, options.Limit) + limit = defaultQueryLimit + } + req.Limit(limit) + + if err := tradeRateLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("trade rate limiter wait error: %w", err) + } + response, err := req.Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to query trades, err: %w", err) + } + + var errs error + for _, trade := range response.List { + res, err := v3ToGlobalTrade(trade) + if err != nil { + errs = multierr.Append(errs, err) + continue + } + trades = append(trades, *res) + } + + if errs != nil { + return nil, errs + } + + return trades, nil +} From f095a1ab71d5202f736f087cd0cea9eb7ffc0bc6 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 1 Aug 2023 20:11:33 +0800 Subject: [PATCH 1278/1392] core: fix trade collector dead lock --- pkg/core/tradecollector.go | 12 +++++++----- pkg/risk/riskcontrol/position.go | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go index 695cd8a8a4..05d7d7ff94 100644 --- a/pkg/core/tradecollector.go +++ b/pkg/core/tradecollector.go @@ -127,7 +127,6 @@ func (c *TradeCollector) RecoverTrade(td types.Trade) bool { // if we have the order in the order store, then the trade will be considered for the position. // profit will also be calculated. func (c *TradeCollector) Process() bool { - logrus.Debugf("TradeCollector.Process()") positionChanged := false var trades []types.Trade @@ -184,20 +183,24 @@ func (c *TradeCollector) Process() bool { // return true when the given trade is added // return false when the given trade is not added func (c *TradeCollector) processTrade(trade types.Trade) bool { - c.mu.Lock() - defer c.mu.Unlock() - key := trade.Key() + c.mu.Lock() + // if it's already done, remove the trade from the trade store if _, done := c.doneTrades[key]; done { + c.mu.Unlock() return false } if !c.orderStore.Exists(trade.OrderID) { + c.mu.Unlock() return false } + c.doneTrades[key] = struct{}{} + c.mu.Unlock() + if c.position != nil { profit, netProfit, madeProfit := c.position.AddTrade(trade) if madeProfit { @@ -213,7 +216,6 @@ func (c *TradeCollector) processTrade(trade types.Trade) bool { c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) } - c.doneTrades[key] = struct{}{} return true } diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go index 2714238582..851c6f5664 100644 --- a/pkg/risk/riskcontrol/position.go +++ b/pkg/risk/riskcontrol/position.go @@ -44,7 +44,7 @@ func NewPositionRiskControl(orderExecutor bbgo.OrderExecutorExtended, hardLimit, Quantity: quantity, } - log.Infof("submitting order: %+v", submitOrder) + log.Infof("RiskControl: position limit exceeded, submitting order to reduce position: %+v", submitOrder) createdOrders, err := orderExecutor.SubmitOrders(context.Background(), submitOrder) if err != nil { log.WithError(err).Errorf("failed to submit orders") From 5bb2a50f21e99c866ff14f1290d41a98425a9ca4 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 1 Aug 2023 20:17:20 +0800 Subject: [PATCH 1279/1392] fix lint issues --- pkg/fixedpoint/convert_test.go | 2 +- pkg/migrations/sqlite3/migration_api_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/fixedpoint/convert_test.go b/pkg/fixedpoint/convert_test.go index a992c0f79e..dc008eabae 100644 --- a/pkg/fixedpoint/convert_test.go +++ b/pkg/fixedpoint/convert_test.go @@ -121,4 +121,4 @@ func Test_FormatString(t *testing.T) { assert.Equal(t, c.expected, s) }) } -} \ No newline at end of file +} diff --git a/pkg/migrations/sqlite3/migration_api_test.go b/pkg/migrations/sqlite3/migration_api_test.go index d1f4fe1ab0..d7f77c875c 100644 --- a/pkg/migrations/sqlite3/migration_api_test.go +++ b/pkg/migrations/sqlite3/migration_api_test.go @@ -14,7 +14,7 @@ func TestGetMigrationsMap(t *testing.T) { func TestMergeMigrationsMap(t *testing.T) { MergeMigrationsMap(map[int64]*rockhopper.Migration{ - 2: &rockhopper.Migration{}, - 3: &rockhopper.Migration{}, + 2: {}, + 3: {}, }) } From 98145d4dd19d9c129ec74e2aef28b580f2c75c31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 12:20:44 +0000 Subject: [PATCH 1280/1392] build(deps): bump grpcio from 1.44.0 to 1.53.0 in /python Bumps [grpcio](https://github.com/grpc/grpc) from 1.44.0 to 1.53.0. - [Release notes](https://github.com/grpc/grpc/releases) - [Changelog](https://github.com/grpc/grpc/blob/master/doc/grpc_release_schedule.md) - [Commits](https://github.com/grpc/grpc/compare/v1.44.0...v1.53.0) --- updated-dependencies: - dependency-name: grpcio dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- python/poetry.lock | 447 ++++++++++++++++++++++----------------------- 1 file changed, 215 insertions(+), 232 deletions(-) diff --git a/python/poetry.lock b/python/poetry.lock index 74c6544889..072f7cc712 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -1,32 +1,43 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + [[package]] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] [[package]] name = "attrs" version = "21.4.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] [[package]] name = "click" version = "8.0.4" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -35,17 +46,23 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] [[package]] name = "flake8" version = "4.0.1" description = "the modular source code checker: pep8 pyflakes and co" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] [package.dependencies] mccabe = ">=0.6.0,<0.7.0" @@ -54,68 +71,169 @@ pyflakes = ">=2.4.0,<2.5.0" [[package]] name = "grpcio" -version = "1.44.0" +version = "1.53.0" description = "HTTP/2-based RPC framework" -category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -six = ">=1.5.2" +python-versions = ">=3.7" +files = [ + {file = "grpcio-1.53.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:752d2949b40e12e6ad3ed8cc552a65b54d226504f6b1fb67cab2ccee502cc06f"}, + {file = "grpcio-1.53.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:8a48fd3a7222be226bb86b7b413ad248f17f3101a524018cdc4562eeae1eb2a3"}, + {file = "grpcio-1.53.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:f3e837d29f0e1b9d6e7b29d569e2e9b0da61889e41879832ea15569c251c303a"}, + {file = "grpcio-1.53.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef7d30242409c3aa5839b501e877e453a2c8d3759ca8230dd5a21cda029f046"}, + {file = "grpcio-1.53.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6f90698b5d1c5dd7b3236cd1fa959d7b80e17923f918d5be020b65f1c78b173"}, + {file = "grpcio-1.53.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a96c3c7f564b263c5d7c0e49a337166c8611e89c4c919f66dba7b9a84abad137"}, + {file = "grpcio-1.53.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ee81349411648d1abc94095c68cd25e3c2812e4e0367f9a9355be1e804a5135c"}, + {file = "grpcio-1.53.0-cp310-cp310-win32.whl", hash = "sha256:fdc6191587de410a184550d4143e2b24a14df495c86ca15e59508710681690ac"}, + {file = "grpcio-1.53.0-cp310-cp310-win_amd64.whl", hash = "sha256:658ffe1e39171be00490db5bd3b966f79634ac4215a1eb9a85c6cd6783bf7f6e"}, + {file = "grpcio-1.53.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:1b172e6d497191940c4b8d75b53de82dc252e15b61de2951d577ec5b43316b29"}, + {file = "grpcio-1.53.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:82434ba3a5935e47908bc861ce1ebc43c2edfc1001d235d6e31e5d3ed55815f7"}, + {file = "grpcio-1.53.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:1c734a2d4843e4e14ececf5600c3c4750990ec319e1299db7e4f0d02c25c1467"}, + {file = "grpcio-1.53.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a2ead3de3b2d53119d473aa2f224030257ef33af1e4ddabd4afee1dea5f04c"}, + {file = "grpcio-1.53.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a34d6e905f071f9b945cabbcc776e2055de1fdb59cd13683d9aa0a8f265b5bf9"}, + {file = "grpcio-1.53.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eaf8e3b97caaf9415227a3c6ca5aa8d800fecadd526538d2bf8f11af783f1550"}, + {file = "grpcio-1.53.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:da95778d37be8e4e9afca771a83424f892296f5dfb2a100eda2571a1d8bbc0dc"}, + {file = "grpcio-1.53.0-cp311-cp311-win32.whl", hash = "sha256:e4f513d63df6336fd84b74b701f17d1bb3b64e9d78a6ed5b5e8a198bbbe8bbfa"}, + {file = "grpcio-1.53.0-cp311-cp311-win_amd64.whl", hash = "sha256:ddb2511fbbb440ed9e5c9a4b9b870f2ed649b7715859fd6f2ebc585ee85c0364"}, + {file = "grpcio-1.53.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:2a912397eb8d23c177d6d64e3c8bc46b8a1c7680b090d9f13a640b104aaec77c"}, + {file = "grpcio-1.53.0-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:55930c56b8f5b347d6c8c609cc341949a97e176c90f5cbb01d148d778f3bbd23"}, + {file = "grpcio-1.53.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:6601d812105583948ab9c6e403a7e2dba6e387cc678c010e74f2d6d589d1d1b3"}, + {file = "grpcio-1.53.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c705e0c21acb0e8478a00e7e773ad0ecdb34bd0e4adc282d3d2f51ba3961aac7"}, + {file = "grpcio-1.53.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba074af9ca268ad7b05d3fc2b920b5fb3c083da94ab63637aaf67f4f71ecb755"}, + {file = "grpcio-1.53.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:14817de09317dd7d3fbc8272864288320739973ef0f4b56bf2c0032349da8cdf"}, + {file = "grpcio-1.53.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c7ad9fbedb93f331c2e9054e202e95cf825b885811f1bcbbdfdc301e451442db"}, + {file = "grpcio-1.53.0-cp37-cp37m-win_amd64.whl", hash = "sha256:dad5b302a4c21c604d88a5d441973f320134e6ff6a84ecef9c1139e5ffd466f6"}, + {file = "grpcio-1.53.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:fa8eaac75d3107e3f5465f2c9e3bbd13db21790c6e45b7de1756eba16b050aca"}, + {file = "grpcio-1.53.0-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:104a2210edd3776c38448b4f76c2f16e527adafbde171fc72a8a32976c20abc7"}, + {file = "grpcio-1.53.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:dbc1ba968639c1d23476f75c356e549e7bbf2d8d6688717dcab5290e88e8482b"}, + {file = "grpcio-1.53.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95952d3fe795b06af29bb8ec7bbf3342cdd867fc17b77cc25e6733d23fa6c519"}, + {file = "grpcio-1.53.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f144a790f14c51b8a8e591eb5af40507ffee45ea6b818c2482f0457fec2e1a2e"}, + {file = "grpcio-1.53.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0698c094688a2dd4c7c2f2c0e3e142cac439a64d1cef6904c97f6cde38ba422f"}, + {file = "grpcio-1.53.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6b6d60b0958be711bab047e9f4df5dbbc40367955f8651232bfdcdd21450b9ab"}, + {file = "grpcio-1.53.0-cp38-cp38-win32.whl", hash = "sha256:1948539ce78805d4e6256ab0e048ec793956d54787dc9d6777df71c1d19c7f81"}, + {file = "grpcio-1.53.0-cp38-cp38-win_amd64.whl", hash = "sha256:df9ba1183b3f649210788cf80c239041dddcb375d6142d8bccafcfdf549522cd"}, + {file = "grpcio-1.53.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:19caa5b7282a89b799e63776ff602bb39604f7ca98db6df27e2de06756ae86c3"}, + {file = "grpcio-1.53.0-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:b5bd026ac928c96cc23149e6ef79183125542062eb6d1ccec34c0a37e02255e7"}, + {file = "grpcio-1.53.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:7dc8584ca6c015ad82e186e82f4c0fe977394588f66b8ecfc4ec873285314619"}, + {file = "grpcio-1.53.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2eddaae8af625e45b5c8500dcca1043264d751a6872cde2eda5022df8a336959"}, + {file = "grpcio-1.53.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5fb6f3d7824696c1c9f2ad36ddb080ba5a86f2d929ef712d511b4d9972d3d27"}, + {file = "grpcio-1.53.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8270d1dc2c98ab57e6dbf36fa187db8df4c036f04a398e5d5e25b4e01a766d70"}, + {file = "grpcio-1.53.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:976a7f24eb213e8429cab78d5e120500dfcdeb01041f1f5a77b17b9101902615"}, + {file = "grpcio-1.53.0-cp39-cp39-win32.whl", hash = "sha256:9c84a481451e7174f3a764a44150f93b041ab51045aa33d7b5b68b6979114e48"}, + {file = "grpcio-1.53.0-cp39-cp39-win_amd64.whl", hash = "sha256:6beb84f83360ff29a3654f43f251ec11b809dcb5524b698d711550243debd289"}, + {file = "grpcio-1.53.0.tar.gz", hash = "sha256:a4952899b4931a6ba12951f9a141ef3e74ff8a6ec9aa2dc602afa40f63595e33"}, +] [package.extras] -protobuf = ["grpcio-tools (>=1.44.0)"] +protobuf = ["grpcio-tools (>=1.53.0)"] [[package]] name = "grpcio-tools" version = "1.44.0" description = "Protobuf code generator for gRPC" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "grpcio-tools-1.44.0.tar.gz", hash = "sha256:be37f458ea510c9a8f1caabbc2b258d12e55d189a567f5edcace90f27dc0efbf"}, + {file = "grpcio_tools-1.44.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:9f58529e24f613019a85c258a274d441d89e0cad8cf7fca21ef3807ba5840c5d"}, + {file = "grpcio_tools-1.44.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:1d120082236f8d2877f8a19366476b82c3562423b877b7c471a142432e31c2c4"}, + {file = "grpcio_tools-1.44.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:65c2fe3cdc5425180f01dd303e28d4f363d38f4c2e3a7e1a87caedd5417e23bb"}, + {file = "grpcio_tools-1.44.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5caef118deb8cdee1978fd3d8e388a9b256cd8d34e4a8895731ac0e86fa5e47c"}, + {file = "grpcio_tools-1.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121c9765cee8636201cf0d4e80bc7b509813194919bccdb66e9671c4ece6dac3"}, + {file = "grpcio_tools-1.44.0-cp310-cp310-win32.whl", hash = "sha256:90d1fac188bac838c4169eb3b67197887fa0572ea8a90519a20cddb080800549"}, + {file = "grpcio_tools-1.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:3e16260dfe6e997330473863e01466b0992369ae2337a0249b390b4651cff424"}, + {file = "grpcio_tools-1.44.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:608414cc1093e1e9e5980c97a6ee78e51dffff359e7a3f123d1fb9d95b8763a5"}, + {file = "grpcio_tools-1.44.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:395609c06f69fbc79518b30a01931127088a3f9ef2cc2a35269c5f187eefd38c"}, + {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f7ce16766b24b88ec0e4355f5dd66c2eee6af210e889fcb7961c9c4634c687de"}, + {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3c9abc4a40c62f46d5e43e49c7afc567dedf12eeef95933ac9ea2986baa2420b"}, + {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:b73fd87a44ba1b91866b0254193c37cdb001737759b77b637cebe0c816d38342"}, + {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b211f12e4cbc0fde8e0f982b0f581cce38874666a02ebfed93c23dcaeb8a4e0"}, + {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b421dc9b27bcaff4c73644cd3801e4893b11ba3eb39729246fd3de98d9f685b"}, + {file = "grpcio_tools-1.44.0-cp36-cp36m-win32.whl", hash = "sha256:33d93027840a873c7b59402fe6db8263b88c56e2f84aa0b6281c05cc8bd314a1"}, + {file = "grpcio_tools-1.44.0-cp36-cp36m-win_amd64.whl", hash = "sha256:71fb6e7e66b918803b1bebd0231560981ab86c2546a3318a45822ce94de5e83d"}, + {file = "grpcio_tools-1.44.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:614c427ff235d92f103e9189f0230197c8f2f817d0dd9fd078f5d2ea4d920d02"}, + {file = "grpcio_tools-1.44.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:c13e0cb486cfa15320ddcd70452a4d736e6ce319c03d6b3c0c2513ec8d2748fb"}, + {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5ade6b13dc4e148f400c8f55a6ef0b14216a3371d7a9e559571d5981b6cec36b"}, + {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6138d2c7eec7ed57585bc58e2dbcb65635a2d574ac632abd29949d3e68936bab"}, + {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:3d6c8548b199591757dbfe89ed14e23782d6079d6d201c6c314c72f4086883aa"}, + {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41c419829f01734d65958ba9b01b759061d8f7e0698f9612ba6b8837269f7a9"}, + {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9f0c5b4567631fec993826e694e83d86a972b3e2e9b05cb0c56839b0316d26c"}, + {file = "grpcio_tools-1.44.0-cp37-cp37m-win32.whl", hash = "sha256:3f0e1d1f3f5a6f0c9f8b5441819dbec831ce7e9ffe04768e4b0d965a95fbbe5e"}, + {file = "grpcio_tools-1.44.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f87fc86d0b4181b6b4da6ec6a29511dca000e6b5694fdd6bbf87d125128bc41"}, + {file = "grpcio_tools-1.44.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:cb8baa1d4cea35ca662c24098377bdd9514c56f227da0e38b43cd9b8223bfcc6"}, + {file = "grpcio_tools-1.44.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:ea36a294f7c70fd2f2bfb5dcf08602006304aa65b055ebd4f7c709e2a89deba7"}, + {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1972caf8f695b91edc6444134445798692fe71276f0cde7604d55e65179adf93"}, + {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:674fb8d9c0e2d75166c4385753962485b757897223fc92a19c9e513ab80b96f7"}, + {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:37045ba850d423cdacede77b266b127025818a5a36d80f1fd7a5a1614a6a0de5"}, + {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cdf72947c6b0b03aa6dac06117a095947d02d43a5c6343051f4ce161fd0abcb"}, + {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bfa6fc1515c202fe428ba9f99e2b2f947b01bafc15d868798235b2e2d36baa"}, + {file = "grpcio_tools-1.44.0-cp38-cp38-win32.whl", hash = "sha256:2c516124356476d9afa126acce10ce568733120afbd9ae17ee01d44b9da20a67"}, + {file = "grpcio_tools-1.44.0-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6441c24176705c5ab056e65a8b330e107107c5a492ba094d1b862a136d15d"}, + {file = "grpcio_tools-1.44.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:398eda759194d355eb09f7beabae6e4fb45b3877cf7efe505b49095fa4889cef"}, + {file = "grpcio_tools-1.44.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:a169bfd7a1fe8cc11472eeeeab3088b3c5d56caac12b2192a920b73adcbc974c"}, + {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:a58aaaec0d846d142edd8e794ebb80aa429abfd581f4493a60a603aac0c50ac8"}, + {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c3253bee8b68fe422754faf0f286aa068861c926a7b11e4daeb44b9af767c7f1"}, + {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:3c0be60721ae1ba09c4f29572a145f412e561b9201e19428758893709827f472"}, + {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e44b9572c2226b85976e0d6054e22d7c59ebd6c9425ee71e5bc8910434aee3e1"}, + {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c04ec47905c4f6d6dad34d29f6ace652cc1ddc986f55aaa5559b72104c3f5cf"}, + {file = "grpcio_tools-1.44.0-cp39-cp39-win32.whl", hash = "sha256:fb8c7b9d24e2c4dc77e7800e83b68081729ac6094b781b2afdabf08af18c3b28"}, + {file = "grpcio_tools-1.44.0-cp39-cp39-win_amd64.whl", hash = "sha256:4eb93619c8cb3773fb899504e3e30a0dc79d3904fd7a84091d15552178e1e920"}, +] [package.dependencies] grpcio = ">=1.44.0" protobuf = ">=3.5.0.post1,<4.0dev" +setuptools = "*" [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] [[package]] name = "loguru" version = "0.6.0" description = "Python logging made (stupidly) simple" -category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, + {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, +] [package.dependencies] colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] -dev = ["colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "black (>=19.10b0)", "isort (>=5.1.1)", "Sphinx (>=4.1.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)"] +dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"] [[package]] name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" -category = "main" optional = false python-versions = "*" +files = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] [[package]] name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] [package.dependencies] pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" @@ -124,9 +242,12 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] [package.extras] dev = ["pre-commit", "tox"] @@ -136,41 +257,80 @@ testing = ["pytest", "pytest-benchmark"] name = "protobuf" version = "3.19.4" description = "Protocol Buffers" -category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, + {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, + {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, + {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, + {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, + {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, + {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, + {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, + {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, + {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, + {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, + {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, + {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, + {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, + {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, + {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, + {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, + {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, +] [[package]] name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] [[package]] name = "pycodestyle" version = "2.8.0" description = "Python style guide checker" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] [[package]] name = "pyflakes" version = "2.4.0" description = "passive checker of Python programs" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] [[package]] name = "pyparsing" version = "3.0.7" description = "Python parsing module" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, +] [package.extras] diagrams = ["jinja2", "railroad-diagrams"] @@ -179,9 +339,12 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pytest" version = "7.0.1" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, + {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, +] [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -197,227 +360,47 @@ tomli = ">=1.0.0" testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.7" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[package]] name = "win32-setctime" version = "1.1.0" description = "A small Python utility to set file creation time on Windows" -category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] [package.extras] -dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] -lock-version = "1.1" +lock-version = "2.0" python-versions = "^3.8" content-hash = "bfda359d4e023f07cd8df05859450215e9f560f50b4a77a8aa8436ac42a74fe3" - -[metadata.files] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] -click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -grpcio = [ - {file = "grpcio-1.44.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:11f811c0fffd84fca747fbc742464575e5eb130fd4fb4d6012ccc34febd001db"}, - {file = "grpcio-1.44.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9a86a91201f8345502ea81dee0a55ae13add5fafadf109b17acd858fe8239651"}, - {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:5f3c54ebb5d9633a557335c01d88d3d4928e9b1b131692283b6184da1edbec0b"}, - {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d47553b8e86ab1e59b0185ba6491a187f94a0239f414c8fc867a22b0405b798"}, - {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1e22d3a510438b7f3365c0071b810672d09febac6e8ca8a47eab657ae5f347b"}, - {file = "grpcio-1.44.0-cp310-cp310-win32.whl", hash = "sha256:41036a574cab3468f24d41d6ed2b52588fb85ed60f8feaa925d7e424a250740b"}, - {file = "grpcio-1.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:4ee51964edfd0a1293a95bb0d72d134ecf889379d90d2612cbf663623ce832b4"}, - {file = "grpcio-1.44.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:e2149077d71e060678130644670389ddf1491200bcea16c5560d4ccdc65e3f2e"}, - {file = "grpcio-1.44.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:0ac72d4b953b76924f8fa21436af060d7e6d8581e279863f30ee14f20751ac27"}, - {file = "grpcio-1.44.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5c30a9a7d3a05920368a60b080cbbeaf06335303be23ac244034c71c03a0fd24"}, - {file = "grpcio-1.44.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:05467acd391e3fffb05991c76cb2ed2fa1309d0e3815ac379764bc5670b4b5d4"}, - {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:b81dc7894062ed2d25b74a2725aaa0a6895ce97ce854f432fe4e87cad5a07316"}, - {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46d4843192e7d36278884282e100b8f305cf37d1b3d8c6b4f736d4454640a069"}, - {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:898c159148f27e23c08a337fb80d31ece6b76bb24f359d83929460d813665b74"}, - {file = "grpcio-1.44.0-cp36-cp36m-win32.whl", hash = "sha256:b8d852329336c584c636caa9c2db990f3a332b19bc86a80f4646b58d27c142db"}, - {file = "grpcio-1.44.0-cp36-cp36m-win_amd64.whl", hash = "sha256:790d7493337558ae168477d1be3178f4c9b8f91d8cd9b8b719d06fd9b2d48836"}, - {file = "grpcio-1.44.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:cd61b52d9cf8fcf8d9628c0b640b9e44fdc5e93d989cc268086a858540ed370c"}, - {file = "grpcio-1.44.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:14eefcf623890f3f7dd7831decd2a2116652b5ce1e0f1d4b464b8f52110743b0"}, - {file = "grpcio-1.44.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:bebe90b8020b4248e5a2076b56154cc6ff45691bbbe980579fc9db26717ac968"}, - {file = "grpcio-1.44.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89b390b1c0de909965280d175c53128ce2f0f4f5c0f011382243dd7f2f894060"}, - {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:c122dac5cb299b8ad7308d61bd9fe0413de13b0347cce465398436b3fdf1f609"}, - {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6641a28cc826a92ef717201cca9a035c34a0185e38b0c93f3ce5f01a01a1570a"}, - {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb0a3e0e64843441793923d9532a3a23907b07b2a1e0a7a31f186dc185bb772"}, - {file = "grpcio-1.44.0-cp37-cp37m-win32.whl", hash = "sha256:be857b7ec2ac43455156e6ba89262f7d7ae60227049427d01a3fecd218a3f88d"}, - {file = "grpcio-1.44.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f6a9cf0e77f72f2ac30c9c6e086bc7446c984c51bebc6c7f50fbcd718037edba"}, - {file = "grpcio-1.44.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:19e54f0c7083c8332b5a75a9081fc5127f1dbb67b6c1a32bd7fe896ef0934918"}, - {file = "grpcio-1.44.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:bfd36b959c3c4e945119387baed1414ea46f7116886aa23de0172302b49d7ff1"}, - {file = "grpcio-1.44.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:ccd388b8f37b19d06e4152189726ce309e36dc03b53f2216a4ea49f09a7438e6"}, - {file = "grpcio-1.44.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:9075c0c003c1ff14ebce8f0ba55cc692158cb55c68da09cf8b0f9fc5b749e343"}, - {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e898194f76212facbaeb6d7545debff29351afa23b53ff8f0834d66611af5139"}, - {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fa6584046a7cf281649975a363673fa5d9c6faf9dc923f261cc0e56713b5892"}, - {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36a7bdd6ef9bca050c7ade8cba5f0e743343ea0756d5d3d520e915098a9dc503"}, - {file = "grpcio-1.44.0-cp38-cp38-win32.whl", hash = "sha256:dc3290d0411ddd2bd49adba5793223de8de8b01588d45e9376f1a9f7d25414f4"}, - {file = "grpcio-1.44.0-cp38-cp38-win_amd64.whl", hash = "sha256:13343e7b840c20f43b44f0e6d3bbdc037c964f0aec9735d7cb685c407731c9ff"}, - {file = "grpcio-1.44.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c5c2f8417d13386e18ccc8c61467cb6a6f9667a1ff7000a2d7d378e5d7df693f"}, - {file = "grpcio-1.44.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:cf220199b7b4992729ad4d55d5d3f652f4ccfe1a35b5eacdbecf189c245e1859"}, - {file = "grpcio-1.44.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4201c597e5057a9bfef9ea5777a6d83f6252cb78044db7d57d941ec2300734a5"}, - {file = "grpcio-1.44.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:e2de61005118ae59d48d5d749283ebfd1ba4ca68cc1000f8a395cd2bdcff7ceb"}, - {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:871078218fa9117e2a378678f327e32fda04e363ed6bc0477275444273255d4d"}, - {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8d610b7b557a7609fecee80b6dd793ecb7a9a3c3497fbdce63ce7d151cdd705"}, - {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fcb53e4eb8c271032c91b8981df5fc1bb974bc73e306ec2c27da41bd95c44b5"}, - {file = "grpcio-1.44.0-cp39-cp39-win32.whl", hash = "sha256:e50ddea6de76c09b656df4b5a55ae222e2a56e625c44250e501ff3c904113ec1"}, - {file = "grpcio-1.44.0-cp39-cp39-win_amd64.whl", hash = "sha256:d2ec124a986093e26420a5fb10fa3f02b2c232f924cdd7b844ddf7e846c020cd"}, - {file = "grpcio-1.44.0.tar.gz", hash = "sha256:4bae1c99896045d3062ab95478411c8d5a52cb84b91a1517312629fa6cfeb50e"}, -] -grpcio-tools = [ - {file = "grpcio-tools-1.44.0.tar.gz", hash = "sha256:be37f458ea510c9a8f1caabbc2b258d12e55d189a567f5edcace90f27dc0efbf"}, - {file = "grpcio_tools-1.44.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:9f58529e24f613019a85c258a274d441d89e0cad8cf7fca21ef3807ba5840c5d"}, - {file = "grpcio_tools-1.44.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:1d120082236f8d2877f8a19366476b82c3562423b877b7c471a142432e31c2c4"}, - {file = "grpcio_tools-1.44.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:65c2fe3cdc5425180f01dd303e28d4f363d38f4c2e3a7e1a87caedd5417e23bb"}, - {file = "grpcio_tools-1.44.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5caef118deb8cdee1978fd3d8e388a9b256cd8d34e4a8895731ac0e86fa5e47c"}, - {file = "grpcio_tools-1.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121c9765cee8636201cf0d4e80bc7b509813194919bccdb66e9671c4ece6dac3"}, - {file = "grpcio_tools-1.44.0-cp310-cp310-win32.whl", hash = "sha256:90d1fac188bac838c4169eb3b67197887fa0572ea8a90519a20cddb080800549"}, - {file = "grpcio_tools-1.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:3e16260dfe6e997330473863e01466b0992369ae2337a0249b390b4651cff424"}, - {file = "grpcio_tools-1.44.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:608414cc1093e1e9e5980c97a6ee78e51dffff359e7a3f123d1fb9d95b8763a5"}, - {file = "grpcio_tools-1.44.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:395609c06f69fbc79518b30a01931127088a3f9ef2cc2a35269c5f187eefd38c"}, - {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f7ce16766b24b88ec0e4355f5dd66c2eee6af210e889fcb7961c9c4634c687de"}, - {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3c9abc4a40c62f46d5e43e49c7afc567dedf12eeef95933ac9ea2986baa2420b"}, - {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:b73fd87a44ba1b91866b0254193c37cdb001737759b77b637cebe0c816d38342"}, - {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b211f12e4cbc0fde8e0f982b0f581cce38874666a02ebfed93c23dcaeb8a4e0"}, - {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b421dc9b27bcaff4c73644cd3801e4893b11ba3eb39729246fd3de98d9f685b"}, - {file = "grpcio_tools-1.44.0-cp36-cp36m-win32.whl", hash = "sha256:33d93027840a873c7b59402fe6db8263b88c56e2f84aa0b6281c05cc8bd314a1"}, - {file = "grpcio_tools-1.44.0-cp36-cp36m-win_amd64.whl", hash = "sha256:71fb6e7e66b918803b1bebd0231560981ab86c2546a3318a45822ce94de5e83d"}, - {file = "grpcio_tools-1.44.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:614c427ff235d92f103e9189f0230197c8f2f817d0dd9fd078f5d2ea4d920d02"}, - {file = "grpcio_tools-1.44.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:c13e0cb486cfa15320ddcd70452a4d736e6ce319c03d6b3c0c2513ec8d2748fb"}, - {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5ade6b13dc4e148f400c8f55a6ef0b14216a3371d7a9e559571d5981b6cec36b"}, - {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6138d2c7eec7ed57585bc58e2dbcb65635a2d574ac632abd29949d3e68936bab"}, - {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:3d6c8548b199591757dbfe89ed14e23782d6079d6d201c6c314c72f4086883aa"}, - {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41c419829f01734d65958ba9b01b759061d8f7e0698f9612ba6b8837269f7a9"}, - {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9f0c5b4567631fec993826e694e83d86a972b3e2e9b05cb0c56839b0316d26c"}, - {file = "grpcio_tools-1.44.0-cp37-cp37m-win32.whl", hash = "sha256:3f0e1d1f3f5a6f0c9f8b5441819dbec831ce7e9ffe04768e4b0d965a95fbbe5e"}, - {file = "grpcio_tools-1.44.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f87fc86d0b4181b6b4da6ec6a29511dca000e6b5694fdd6bbf87d125128bc41"}, - {file = "grpcio_tools-1.44.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:cb8baa1d4cea35ca662c24098377bdd9514c56f227da0e38b43cd9b8223bfcc6"}, - {file = "grpcio_tools-1.44.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:ea36a294f7c70fd2f2bfb5dcf08602006304aa65b055ebd4f7c709e2a89deba7"}, - {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1972caf8f695b91edc6444134445798692fe71276f0cde7604d55e65179adf93"}, - {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:674fb8d9c0e2d75166c4385753962485b757897223fc92a19c9e513ab80b96f7"}, - {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:37045ba850d423cdacede77b266b127025818a5a36d80f1fd7a5a1614a6a0de5"}, - {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cdf72947c6b0b03aa6dac06117a095947d02d43a5c6343051f4ce161fd0abcb"}, - {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bfa6fc1515c202fe428ba9f99e2b2f947b01bafc15d868798235b2e2d36baa"}, - {file = "grpcio_tools-1.44.0-cp38-cp38-win32.whl", hash = "sha256:2c516124356476d9afa126acce10ce568733120afbd9ae17ee01d44b9da20a67"}, - {file = "grpcio_tools-1.44.0-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6441c24176705c5ab056e65a8b330e107107c5a492ba094d1b862a136d15d"}, - {file = "grpcio_tools-1.44.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:398eda759194d355eb09f7beabae6e4fb45b3877cf7efe505b49095fa4889cef"}, - {file = "grpcio_tools-1.44.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:a169bfd7a1fe8cc11472eeeeab3088b3c5d56caac12b2192a920b73adcbc974c"}, - {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:a58aaaec0d846d142edd8e794ebb80aa429abfd581f4493a60a603aac0c50ac8"}, - {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c3253bee8b68fe422754faf0f286aa068861c926a7b11e4daeb44b9af767c7f1"}, - {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:3c0be60721ae1ba09c4f29572a145f412e561b9201e19428758893709827f472"}, - {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e44b9572c2226b85976e0d6054e22d7c59ebd6c9425ee71e5bc8910434aee3e1"}, - {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c04ec47905c4f6d6dad34d29f6ace652cc1ddc986f55aaa5559b72104c3f5cf"}, - {file = "grpcio_tools-1.44.0-cp39-cp39-win32.whl", hash = "sha256:fb8c7b9d24e2c4dc77e7800e83b68081729ac6094b781b2afdabf08af18c3b28"}, - {file = "grpcio_tools-1.44.0-cp39-cp39-win_amd64.whl", hash = "sha256:4eb93619c8cb3773fb899504e3e30a0dc79d3904fd7a84091d15552178e1e920"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -loguru = [ - {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, - {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -protobuf = [ - {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, - {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, - {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, - {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, - {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, - {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, - {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, - {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, - {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, - {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, - {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, - {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, - {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, - {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, - {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, - {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, - {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, - {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, - {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, - {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, - {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, - {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, - {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, - {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, - {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, - {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, -] -pytest = [ - {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, - {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -win32-setctime = [ - {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, - {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, -] From 5d203ac5ba9b4ee91519360e2d902fd26c297837 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 12:22:00 +0000 Subject: [PATCH 1281/1392] build(deps): bump underscore, @nomiclabs/hardhat-waffle and ethereum-waffle Removes [underscore](https://github.com/jashkenas/underscore). It's no longer used after updating ancestor dependencies [underscore](https://github.com/jashkenas/underscore), [@nomiclabs/hardhat-waffle](https://github.com/NomicFoundation/hardhat-waffle) and [ethereum-waffle](https://github.com/EthWorks/Waffle). These dependencies need to be updated together. Removes `underscore` Updates `@nomiclabs/hardhat-waffle` from 2.0.1 to 2.0.6 - [Release notes](https://github.com/NomicFoundation/hardhat-waffle/releases) - [Changelog](https://github.com/NomicFoundation/hardhat-waffle/blob/main/CHANGELOG.md) - [Commits](https://github.com/NomicFoundation/hardhat-waffle/commits) Updates `ethereum-waffle` from 3.4.0 to 4.0.10 - [Release notes](https://github.com/EthWorks/Waffle/releases) - [Commits](https://github.com/EthWorks/Waffle/compare/ethereum-waffle@3.4.0...ethereum-waffle@4.0.10) --- updated-dependencies: - dependency-name: underscore dependency-type: indirect - dependency-name: "@nomiclabs/hardhat-waffle" dependency-type: direct:development - dependency-name: ethereum-waffle dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- contracts/package-lock.json | 30407 +++++++++------------------------- contracts/package.json | 4 +- 2 files changed, 7750 insertions(+), 22661 deletions(-) diff --git a/contracts/package-lock.json b/contracts/package-lock.json index 878822bca7..4915a3cf2e 100644 --- a/contracts/package-lock.json +++ b/contracts/package-lock.json @@ -14,10 +14,10 @@ "devDependencies": { "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-etherscan": "^2.1.1", - "@nomiclabs/hardhat-waffle": "^2.0.1", + "@nomiclabs/hardhat-waffle": "^2.0.6", "@openzeppelin/contracts": "^3.2.0", "chai": "^4.3.4", - "ethereum-waffle": "^3.4.0", + "ethereum-waffle": "^4.0.10", "ethers": "^5.4.7", "hardhat": "^2.6.5", "prettier": "^2.5.1", @@ -364,6 +364,7 @@ "integrity": "sha512-JSvpj1iNMFjK6K+uVl4unqMoa9rf5jopb8cya5UGBWz23Nw8hSNT7efgUx4BTlAPAgpNlEioUfeTyQ6J9ZvTVw==", "deprecated": "Please use @ensdomains/ens-contracts", "dev": true, + "peer": true, "dependencies": { "bluebird": "^3.5.2", "eth-ens-namehash": "^2.0.8", @@ -375,8 +376,9 @@ "node_modules/@ensdomains/ens/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -384,8 +386,9 @@ "node_modules/@ensdomains/ens/node_modules/camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -393,8 +396,9 @@ "node_modules/@ensdomains/ens/node_modules/cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", "dev": true, + "peer": true, "dependencies": { "string-width": "^1.0.1", "strip-ansi": "^3.0.1", @@ -404,8 +408,9 @@ "node_modules/@ensdomains/ens/node_modules/fs-extra": { "version": "0.30.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", - "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "integrity": "sha512-UvSPKyhMn6LEd/WpUaV9C9t3zATuqoqfWc3QdPhPLb58prN9tqYPlPWi8Krxi44loBoUzlobqZ3+8tGpxxSzwA==", "dev": true, + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^2.1.0", @@ -418,13 +423,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@ensdomains/ens/node_modules/is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", "dev": true, + "peer": true, "dependencies": { "number-is-nan": "^1.0.0" }, @@ -435,8 +442,9 @@ "node_modules/@ensdomains/ens/node_modules/jsonfile": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", "dev": true, + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -444,8 +452,9 @@ "node_modules/@ensdomains/ens/node_modules/require-from-string": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", - "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", + "integrity": "sha512-H7AkJWMobeskkttHyhTVtS0fxpFLjxhbfMa6Bk3wimP7sdPRGL3EyCg3sAQenFfAe+xQ+oAc85Nmtvq0ROM83Q==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -453,14 +462,16 @@ "node_modules/@ensdomains/ens/node_modules/require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "dev": true, + "peer": true }, "node_modules/@ensdomains/ens/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "peer": true, "bin": { "semver": "bin/semver" } @@ -470,6 +481,7 @@ "resolved": "https://registry.npmjs.org/solc/-/solc-0.4.26.tgz", "integrity": "sha512-o+c6FpkiHd+HPjmjEVpQgH7fqZ14tJpXhho+/bQXlXbliLIS/xjXb42Vxh+qQY1WCSTMQ0+a5vR9vi0MfhU6mA==", "dev": true, + "peer": true, "dependencies": { "fs-extra": "^0.30.0", "memorystream": "^0.3.1", @@ -484,8 +496,9 @@ "node_modules/@ensdomains/ens/node_modules/string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", "dev": true, + "peer": true, "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -498,8 +511,9 @@ "node_modules/@ensdomains/ens/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -510,14 +524,16 @@ "node_modules/@ensdomains/ens/node_modules/which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", + "dev": true, + "peer": true }, "node_modules/@ensdomains/ens/node_modules/wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", "dev": true, + "peer": true, "dependencies": { "string-width": "^1.0.1", "strip-ansi": "^3.0.1" @@ -530,13 +546,15 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@ensdomains/ens/node_modules/yargs": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", - "integrity": "sha1-wMQpJMpKqmsObaFznfshZDn53cA=", + "integrity": "sha512-LqodLrnIDM3IFT+Hf/5sxBnEGECrfdC1uIbgZeJmESCSo4HoCAaKEus8MylXHAkdacGc0ye+Qa+dpkuom8uVYA==", "dev": true, + "peer": true, "dependencies": { "cliui": "^3.2.0", "decamelize": "^1.1.1", @@ -557,8 +575,9 @@ "node_modules/@ensdomains/ens/node_modules/yargs-parser": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", - "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=", + "integrity": "sha512-9pIKIJhnI5tonzG6OnCFlz/yln8xHYcGl+pn3xR0Vzff0vzN1PbNRaelgfgRUwZ3s4i3jvxT9WhmUGL4whnasA==", "dev": true, + "peer": true, "dependencies": { "camelcase": "^3.0.0", "lodash.assign": "^4.0.6" @@ -569,84 +588,91 @@ "resolved": "https://registry.npmjs.org/@ensdomains/resolver/-/resolver-0.2.4.tgz", "integrity": "sha512-bvaTH34PMCbv6anRa9I/0zjLJgY4EuznbEMgbV77JBCQ9KNC46rzi0avuxpOfu+xDjPEtSFGqVEOr5GlUSGudA==", "deprecated": "Please use @ensdomains/ens-contracts", - "dev": true + "dev": true, + "peer": true }, "node_modules/@ethereum-waffle/chai": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@ethereum-waffle/chai/-/chai-3.4.1.tgz", - "integrity": "sha512-8mjgjWCe8XSCWuyJgVtJY8sm00VTczGBTDxBejgEBWN/J9x7QD8jdmWW8bfxdnqZbxiDCTvRFL58Wmd254BEqQ==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@ethereum-waffle/chai/-/chai-4.0.10.tgz", + "integrity": "sha512-X5RepE7Dn8KQLFO7HHAAe+KeGaX/by14hn90wePGBhzL54tq4Y8JscZFu+/LCwCl6TnkAAy5ebiMoqJ37sFtWw==", "dev": true, "dependencies": { - "@ethereum-waffle/provider": "^3.4.0", - "ethers": "^5.4.7" + "@ethereum-waffle/provider": "4.0.5", + "debug": "^4.3.4", + "json-bigint": "^1.0.0" }, "engines": { "node": ">=10.0" + }, + "peerDependencies": { + "ethers": "*" } }, "node_modules/@ethereum-waffle/compiler": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@ethereum-waffle/compiler/-/compiler-3.4.0.tgz", - "integrity": "sha512-a2wxGOoB9F1QFRE+Om7Cz2wn+pxM/o7a0a6cbwhaS2lECJgFzeN9xEkVrKahRkF4gEfXGcuORg4msP0Asxezlw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@ethereum-waffle/compiler/-/compiler-4.0.3.tgz", + "integrity": "sha512-5x5U52tSvEVJS6dpCeXXKvRKyf8GICDwiTwUvGD3/WD+DpvgvaoHOL82XqpTSUHgV3bBq6ma5/8gKUJUIAnJCw==", "dev": true, "dependencies": { "@resolver-engine/imports": "^0.3.3", "@resolver-engine/imports-fs": "^0.3.3", - "@typechain/ethers-v5": "^2.0.0", + "@typechain/ethers-v5": "^10.0.0", "@types/mkdirp": "^0.5.2", - "@types/node-fetch": "^2.5.5", - "ethers": "^5.0.1", + "@types/node-fetch": "^2.6.1", "mkdirp": "^0.5.1", - "node-fetch": "^2.6.1", - "solc": "^0.6.3", - "ts-generator": "^0.1.1", - "typechain": "^3.0.0" + "node-fetch": "^2.6.7" }, "engines": { "node": ">=10.0" + }, + "peerDependencies": { + "ethers": "*", + "solc": "*", + "typechain": "^8.0.0" } }, "node_modules/@ethereum-waffle/ens": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@ethereum-waffle/ens/-/ens-3.3.1.tgz", - "integrity": "sha512-xSjNWnT2Iwii3J3XGqD+F5yLEOzQzLHNLGfI5KIXdtQ4FHgReW/AMGRgPPLi+n+SP08oEQWJ3sEKrvbFlwJuaA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@ethereum-waffle/ens/-/ens-4.0.3.tgz", + "integrity": "sha512-PVLcdnTbaTfCrfSOrvtlA9Fih73EeDvFS28JQnT5M5P4JMplqmchhcZB1yg/fCtx4cvgHlZXa0+rOCAk2Jk0Jw==", "dev": true, - "dependencies": { - "@ensdomains/ens": "^0.4.4", - "@ensdomains/resolver": "^0.2.4", - "ethers": "^5.5.2" - }, "engines": { "node": ">=10.0" + }, + "peerDependencies": { + "@ensdomains/ens": "^0.4.4", + "@ensdomains/resolver": "^0.2.4", + "ethers": "*" } }, "node_modules/@ethereum-waffle/mock-contract": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@ethereum-waffle/mock-contract/-/mock-contract-3.3.1.tgz", - "integrity": "sha512-h9yChF7IkpJLODg/o9/jlwKwTcXJLSEIq3gewgwUJuBHnhPkJGekcZvsTbximYc+e42QUZrDUATSuTCIryeCEA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@ethereum-waffle/mock-contract/-/mock-contract-4.0.4.tgz", + "integrity": "sha512-LwEj5SIuEe9/gnrXgtqIkWbk2g15imM/qcJcxpLyAkOj981tQxXmtV4XmQMZsdedEsZ/D/rbUAOtZbgwqgUwQA==", "dev": true, - "dependencies": { - "@ethersproject/abi": "^5.5.0", - "ethers": "^5.5.2" - }, "engines": { "node": ">=10.0" + }, + "peerDependencies": { + "ethers": "*" } }, "node_modules/@ethereum-waffle/provider": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@ethereum-waffle/provider/-/provider-3.4.1.tgz", - "integrity": "sha512-5iDte7c9g9N1rTRE/P4npwk1Hus/wA2yH850X6sP30mr1IrwSG9NKn6/2SOQkAVJnh9jqyLVg2X9xCODWL8G4A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@ethereum-waffle/provider/-/provider-4.0.5.tgz", + "integrity": "sha512-40uzfyzcrPh+Gbdzv89JJTMBlZwzya1YLDyim8mVbEqYLP5VRYWoGp0JMyaizgV3hMoUFRqJKVmIUw4v7r3hYw==", "dev": true, "dependencies": { - "@ethereum-waffle/ens": "^3.3.1", - "ethers": "^5.5.2", - "ganache-core": "^2.13.2", - "patch-package": "^6.2.2", - "postinstall-postinstall": "^2.1.0" + "@ethereum-waffle/ens": "4.0.3", + "@ganache/ethereum-options": "0.1.4", + "debug": "^4.3.4", + "ganache": "7.4.3" }, "engines": { "node": ">=10.0" + }, + "peerDependencies": { + "ethers": "*" } }, "node_modules/@ethereumjs/block": { @@ -1713,6 +1739,129 @@ "@ethersproject/strings": "^5.5.0" } }, + "node_modules/@ganache/ethereum-address": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/ethereum-address/-/ethereum-address-0.1.4.tgz", + "integrity": "sha512-sTkU0M9z2nZUzDeHRzzGlW724xhMLXo2LeX1hixbnjHWY1Zg1hkqORywVfl+g5uOO8ht8T0v+34IxNxAhmWlbw==", + "dev": true, + "dependencies": { + "@ganache/utils": "0.1.4" + } + }, + "node_modules/@ganache/ethereum-options": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/ethereum-options/-/ethereum-options-0.1.4.tgz", + "integrity": "sha512-i4l46taoK2yC41FPkcoDlEVoqHS52wcbHPqJtYETRWqpOaoj9hAg/EJIHLb1t6Nhva2CdTO84bG+qlzlTxjAHw==", + "dev": true, + "dependencies": { + "@ganache/ethereum-address": "0.1.4", + "@ganache/ethereum-utils": "0.1.4", + "@ganache/options": "0.1.4", + "@ganache/utils": "0.1.4", + "bip39": "3.0.4", + "seedrandom": "3.0.5" + } + }, + "node_modules/@ganache/ethereum-utils": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/ethereum-utils/-/ethereum-utils-0.1.4.tgz", + "integrity": "sha512-FKXF3zcdDrIoCqovJmHLKZLrJ43234Em2sde/3urUT/10gSgnwlpFmrv2LUMAmSbX3lgZhW/aSs8krGhDevDAg==", + "dev": true, + "dependencies": { + "@ethereumjs/common": "2.6.0", + "@ethereumjs/tx": "3.4.0", + "@ethereumjs/vm": "5.6.0", + "@ganache/ethereum-address": "0.1.4", + "@ganache/rlp": "0.1.4", + "@ganache/utils": "0.1.4", + "emittery": "0.10.0", + "ethereumjs-abi": "0.6.8", + "ethereumjs-util": "7.1.3" + } + }, + "node_modules/@ganache/ethereum-utils/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, + "node_modules/@ganache/ethereum-utils/node_modules/ethereumjs-util": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", + "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", + "dev": true, + "dependencies": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@ganache/options": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/options/-/options-0.1.4.tgz", + "integrity": "sha512-zAe/craqNuPz512XQY33MOAG6Si1Xp0hCvfzkBfj2qkuPcbJCq6W/eQ5MB6SbXHrICsHrZOaelyqjuhSEmjXRw==", + "dev": true, + "dependencies": { + "@ganache/utils": "0.1.4", + "bip39": "3.0.4", + "seedrandom": "3.0.5" + } + }, + "node_modules/@ganache/rlp": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/rlp/-/rlp-0.1.4.tgz", + "integrity": "sha512-Do3D1H6JmhikB+6rHviGqkrNywou/liVeFiKIpOBLynIpvZhRCgn3SEDxyy/JovcaozTo/BynHumfs5R085MFQ==", + "dev": true, + "dependencies": { + "@ganache/utils": "0.1.4", + "rlp": "2.2.6" + } + }, + "node_modules/@ganache/rlp/node_modules/rlp": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.6.tgz", + "integrity": "sha512-HAfAmL6SDYNWPUOJNrM500x4Thn4PZsEy5pijPh40U9WfNk0z15hUYzO9xVIMAdIHdFtD8CBDHd75Td1g36Mjg==", + "dev": true, + "dependencies": { + "bn.js": "^4.11.1" + }, + "bin": { + "rlp": "bin/rlp" + } + }, + "node_modules/@ganache/utils": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/utils/-/utils-0.1.4.tgz", + "integrity": "sha512-oatUueU3XuXbUbUlkyxeLLH3LzFZ4y5aSkNbx6tjSIhVTPeh+AuBKYt4eQ73FFcTB3nj/gZoslgAh5CN7O369w==", + "dev": true, + "dependencies": { + "emittery": "0.10.0", + "keccak": "3.0.1", + "seedrandom": "3.0.5" + }, + "optionalDependencies": { + "@trufflesuite/bigint-buffer": "1.1.9" + } + }, + "node_modules/@ganache/utils/node_modules/keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@nomiclabs/hardhat-ethers": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.4.tgz", @@ -1742,17 +1891,14 @@ } }, "node_modules/@nomiclabs/hardhat-waffle": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.1.tgz", - "integrity": "sha512-2YR2V5zTiztSH9n8BYWgtv3Q+EL0N5Ltm1PAr5z20uAY4SkkfylJ98CIqt18XFvxTD5x4K2wKBzddjV9ViDAZQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.6.tgz", + "integrity": "sha512-+Wz0hwmJGSI17B+BhU/qFRZ1l6/xMW82QGXE/Gi+WTmwgJrQefuBs1lIf7hzQ1hLk6hpkvb/zwcNkpVKRYTQYg==", "dev": true, - "dependencies": { - "@types/sinon-chai": "^3.2.3", - "@types/web3": "1.0.19" - }, "peerDependencies": { "@nomiclabs/hardhat-ethers": "^2.0.0", - "ethereum-waffle": "^3.2.0", + "@types/sinon-chai": "^3.2.3", + "ethereum-waffle": "*", "ethers": "^5.0.0", "hardhat": "^2.0.0" } @@ -1951,6 +2097,7 @@ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", "dev": true, + "peer": true, "dependencies": { "type-detect": "4.0.8" } @@ -1960,6 +2107,7 @@ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", "dev": true, + "peer": true, "dependencies": { "@sinonjs/commons": "^1.7.0" } @@ -1987,6 +2135,20 @@ "ethereumjs-wallet": "^1.0.1" } }, + "node_modules/@trufflesuite/bigint-buffer": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@trufflesuite/bigint-buffer/-/bigint-buffer-1.1.9.tgz", + "integrity": "sha512-bdM5cEGCOhDSwminryHJbRmXc1x7dPKg6Pqns3qyTwFlxsqUgxE29lsERS3PlIW1HTjoIGMUqsk1zQQwST1Yxw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "4.3.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@trufflesuite/eth-json-rpc-filters": { "version": "4.1.2-1", "resolved": "https://registry.npmjs.org/@trufflesuite/eth-json-rpc-filters/-/eth-json-rpc-filters-4.1.2-1.tgz", @@ -2161,16 +2323,20 @@ } }, "node_modules/@typechain/ethers-v5": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-2.0.0.tgz", - "integrity": "sha512-0xdCkyGOzdqh4h5JSf+zoWx85IusEjDcPIwNEHP8mrWSnCae4rvrqB+/gtpdNfX7zjlFlZiMeePn2r63EI3Lrw==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-10.2.1.tgz", + "integrity": "sha512-n3tQmCZjRE6IU4h6lqUGiQ1j866n5MTCBJreNEHHVWXa2u9GJTaeYyU1/k+1qLutkyw+sS6VAN+AbeiTqsxd/A==", "dev": true, "dependencies": { - "ethers": "^5.0.2" + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" }, "peerDependencies": { - "ethers": "^5.0.0", - "typechain": "^3.0.0" + "@ethersproject/abi": "^5.0.0", + "@ethersproject/providers": "^5.0.0", + "ethers": "^5.1.3", + "typechain": "^8.1.1", + "typescript": ">=4.3.0" } }, "node_modules/@types/abstract-leveldown": { @@ -2191,7 +2357,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/level-errors": { "version": "3.0.0", @@ -2231,9 +2398,9 @@ "integrity": "sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==" }, "node_modules/@types/node-fetch": { - "version": "2.5.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", - "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", "dev": true, "dependencies": { "@types/node": "*", @@ -2249,20 +2416,11 @@ } }, "node_modules/@types/prettier": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.3.tgz", - "integrity": "sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "dev": true }, - "node_modules/@types/resolve": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", - "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/secp256k1": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.3.tgz", @@ -2276,6 +2434,7 @@ "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.6.tgz", "integrity": "sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==", "dev": true, + "peer": true, "dependencies": { "@sinonjs/fake-timers": "^7.1.0" } @@ -2285,33 +2444,12 @@ "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.8.tgz", "integrity": "sha512-d4ImIQbT/rKMG8+AXpmcan5T2/PNeSjrYhvkwet6z0p8kzYtfgA32xzOBlbU0yqJfq+/0Ml805iFoODO0LP5/g==", "dev": true, + "peer": true, "dependencies": { "@types/chai": "*", "@types/sinon": "*" } }, - "node_modules/@types/underscore": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.4.tgz", - "integrity": "sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg==", - "dev": true - }, - "node_modules/@types/web3": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/@types/web3/-/web3-1.0.19.tgz", - "integrity": "sha512-fhZ9DyvDYDwHZUp5/STa9XW2re0E8GxoioYJ4pEUZ13YHpApSagixj7IAdoYH5uAK+UalGq6Ml8LYzmgRA/q+A==", - "dev": true, - "dependencies": { - "@types/bn.js": "*", - "@types/underscore": "*" - } - }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2481,15 +2619,12 @@ } }, "node_modules/array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", "dev": true, - "dependencies": { - "typical": "^2.6.1" - }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/asn1": { @@ -2703,6 +2838,24 @@ "node": ">=8" } }, + "node_modules/bip39": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", + "integrity": "sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==", + "dev": true, + "dependencies": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + } + }, + "node_modules/bip39/node_modules/@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==", + "dev": true + }, "node_modules/blakejs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.1.tgz", @@ -2712,7 +2865,8 @@ "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/bn.js": { "version": "4.12.0", @@ -3143,8 +3297,9 @@ "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3180,17 +3335,51 @@ "dev": true }, "node_modules/command-line-args": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-4.0.7.tgz", - "integrity": "sha512-aUdPvQRAyBvQd2n7jXcsMDz68ckBJELXNzBybCHOibUWEg0mWTnaYCSRU8h9R+aNRSvDihJtssSRCiDRpLaezA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", "dev": true, "dependencies": { - "array-back": "^2.0.0", - "find-replace": "^1.0.3", - "typical": "^2.6.1" + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" }, - "bin": { - "command-line-args": "bin/cli.js" + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "dev": true, + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/commander": { @@ -3383,9 +3572,9 @@ } }, "node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { "ms": "2.1.2" }, @@ -3419,6 +3608,15 @@ "node": ">=0.12" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3549,6 +3747,18 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/emittery": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.0.tgz", + "integrity": "sha512-AGvFfs+d0JKCJQ4o01ASQLGPmSCxgfU9RFXvzPvZdjKK8oscynksuJhWrSTSw7j7Ep/sZct5b5ZhYCi8S/t0HQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -3936,8 +4146,9 @@ "node_modules/eth-ens-namehash": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz", - "integrity": "sha1-IprEbsqG1S4MmR58sq74P/D2i88=", + "integrity": "sha512-VWEI1+KJfz4Km//dadyvBBoBeSQ0MHTXPvr8UIXiLW6IanxvAV+DmlZAijZwAyggqGUfwQBeHf7tc9wzc1piSw==", "dev": true, + "peer": true, "dependencies": { "idna-uts46-hx": "^2.3.1", "js-sha3": "^0.5.7" @@ -3946,8 +4157,9 @@ "node_modules/eth-ens-namehash/node_modules/js-sha3": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", - "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=", - "dev": true + "integrity": "sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g==", + "dev": true, + "peer": true }, "node_modules/eth-json-rpc-errors": { "version": "2.0.2", @@ -4008,6 +4220,7 @@ "resolved": "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.10.tgz", "integrity": "sha512-rxJ5OFN3RwjQxDcFP2Z5+Q9ho4eIdEmSc2ht0fCu8Se9nbXjZ7/031uXoUYJ87KHCOdVeiUuwSnoS7hmYAGVHA==", "dev": true, + "peer": true, "dependencies": { "js-sha3": "^0.8.0" } @@ -4045,22 +4258,26 @@ "integrity": "sha512-3KLX1mHuEsBW0dKG+c6EOJS1NBNqdCICvZW9sInmZTt5aY0oxmHVggYRE0lJu1tcnMD1K+AKHdLi6U43Awm1Vg==" }, "node_modules/ethereum-waffle": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ethereum-waffle/-/ethereum-waffle-3.4.0.tgz", - "integrity": "sha512-ADBqZCkoSA5Isk486ntKJVjFEawIiC+3HxNqpJqONvh3YXBTNiRfXvJtGuAFLXPG91QaqkGqILEHANAo7j/olQ==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/ethereum-waffle/-/ethereum-waffle-4.0.10.tgz", + "integrity": "sha512-iw9z1otq7qNkGDNcMoeNeLIATF9yKl1M8AIeu42ElfNBplq0e+5PeasQmm8ybY/elkZ1XyRO0JBQxQdVRb8bqQ==", "dev": true, "dependencies": { - "@ethereum-waffle/chai": "^3.4.0", - "@ethereum-waffle/compiler": "^3.4.0", - "@ethereum-waffle/mock-contract": "^3.3.0", - "@ethereum-waffle/provider": "^3.4.0", - "ethers": "^5.0.1" + "@ethereum-waffle/chai": "4.0.10", + "@ethereum-waffle/compiler": "4.0.3", + "@ethereum-waffle/mock-contract": "4.0.4", + "@ethereum-waffle/provider": "4.0.5", + "solc": "0.8.15", + "typechain": "^8.0.0" }, "bin": { "waffle": "bin/waffle" }, "engines": { "node": ">=10.0" + }, + "peerDependencies": { + "ethers": "*" } }, "node_modules/ethereumjs-abi": { @@ -4308,8 +4525,9 @@ "node_modules/ethjs-unit": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", - "integrity": "sha1-xmWSHkduh7ziqdWIpv4EBbLEFpk=", + "integrity": "sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==", "dev": true, + "peer": true, "dependencies": { "bn.js": "4.11.6", "number-to-bn": "1.7.0" @@ -4322,8 +4540,9 @@ "node_modules/ethjs-unit/node_modules/bn.js": { "version": "4.11.6", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=", - "dev": true + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "dev": true, + "peer": true }, "node_modules/ethjs-util": { "version": "0.1.6", @@ -4489,30 +4708,17 @@ } }, "node_modules/find-replace": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz", - "integrity": "sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", "dev": true, "dependencies": { - "array-back": "^1.0.4", - "test-value": "^2.1.0" + "array-back": "^3.0.1" }, "engines": { "node": ">=4.0.0" } }, - "node_modules/find-replace/node_modules/array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", - "dev": true, - "dependencies": { - "typical": "^2.6.0" - }, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -4525,15 +4731,6 @@ "node": ">=4" } }, - "node_modules/find-yarn-workspace-root": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "dev": true, - "dependencies": { - "micromatch": "^4.0.2" - } - }, "node_modules/flat": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", @@ -4670,24334 +4867,9193 @@ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, - "node_modules/ganache-core": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/ganache-core/-/ganache-core-2.13.2.tgz", - "integrity": "sha512-tIF5cR+ANQz0+3pHWxHjIwHqFXcVo0Mb+kcsNhglNFALcYo49aQpnS9dqHartqPfMFjiHh/qFoD3mYK0d/qGgw==", + "node_modules/ganache": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/ganache/-/ganache-7.4.3.tgz", + "integrity": "sha512-RpEDUiCkqbouyE7+NMXG26ynZ+7sGiODU84Kz+FVoXUnQ4qQM4M8wif3Y4qUCt+D/eM1RVeGq0my62FPD6Y1KA==", "bundleDependencies": [ - "keccak" + "@trufflesuite/bigint-buffer", + "emittery", + "keccak", + "leveldown", + "secp256k1", + "@types/bn.js", + "@types/lru-cache", + "@types/seedrandom" ], "dev": true, "hasShrinkwrap": true, "dependencies": { - "abstract-leveldown": "3.0.0", - "async": "2.6.2", - "bip39": "2.5.0", - "cachedown": "1.0.0", - "clone": "2.1.2", - "debug": "3.2.6", - "encoding-down": "5.0.4", - "eth-sig-util": "3.0.0", - "ethereumjs-abi": "0.6.8", - "ethereumjs-account": "3.0.0", - "ethereumjs-block": "2.2.2", - "ethereumjs-common": "1.5.0", - "ethereumjs-tx": "2.1.2", - "ethereumjs-util": "6.2.1", - "ethereumjs-vm": "4.2.0", - "heap": "0.2.6", - "keccak": "3.0.1", - "level-sublevel": "6.6.4", - "levelup": "3.1.1", - "lodash": "4.17.20", - "lru-cache": "5.1.1", - "merkle-patricia-tree": "3.0.0", - "patch-package": "6.2.2", - "seedrandom": "3.0.1", - "source-map-support": "0.5.12", - "tmp": "0.1.0", - "web3-provider-engine": "14.2.1", - "websocket": "1.0.32" + "@trufflesuite/bigint-buffer": "1.1.10", + "@types/bn.js": "^5.1.0", + "@types/lru-cache": "5.1.1", + "@types/seedrandom": "3.0.1", + "emittery": "0.10.0", + "keccak": "3.0.2", + "leveldown": "6.1.0", + "secp256k1": "4.0.3" }, - "engines": { - "node": ">=8.9.0" + "bin": { + "ganache": "dist/node/cli.js", + "ganache-cli": "dist/node/cli.js" }, "optionalDependencies": { - "ethereumjs-wallet": "0.6.5", - "web3": "1.2.11" + "bufferutil": "4.0.5", + "utf-8-validate": "5.0.7" } }, - "node_modules/ganache-core/node_modules/@ethersproject/abi": { - "version": "5.0.0-beta.153", + "node_modules/ganache/node_modules/@trufflesuite/bigint-buffer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@trufflesuite/bigint-buffer/-/bigint-buffer-1.1.10.tgz", + "integrity": "sha512-pYIQC5EcMmID74t26GCC67946mgTJFiLXOT/BYozgrd4UEY2JHEGLhWi9cMiQCt5BSqFEvKkCHNnoj82SRjiEw==", "dev": true, - "license": "MIT", - "optional": true, + "hasInstallScript": true, + "inBundle": true, + "license": "Apache-2.0", "dependencies": { - "@ethersproject/address": ">=5.0.0-beta.128", - "@ethersproject/bignumber": ">=5.0.0-beta.130", - "@ethersproject/bytes": ">=5.0.0-beta.129", - "@ethersproject/constants": ">=5.0.0-beta.128", - "@ethersproject/hash": ">=5.0.0-beta.128", - "@ethersproject/keccak256": ">=5.0.0-beta.127", - "@ethersproject/logger": ">=5.0.0-beta.129", - "@ethersproject/properties": ">=5.0.0-beta.131", - "@ethersproject/strings": ">=5.0.0-beta.130" + "node-gyp-build": "4.4.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/ganache-core/node_modules/@ethersproject/abstract-provider": { - "version": "5.0.8", + "node_modules/ganache/node_modules/@trufflesuite/bigint-buffer/node_modules/node-gyp-build": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz", + "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "inBundle": true, "license": "MIT", - "optional": true, - "dependencies": { - "@ethersproject/bignumber": "^5.0.13", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/networks": "^5.0.7", - "@ethersproject/properties": "^5.0.7", - "@ethersproject/transactions": "^5.0.9", - "@ethersproject/web": "^5.0.12" + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" } }, - "node_modules/ganache-core/node_modules/@ethersproject/abstract-signer": { - "version": "5.0.10", + "node_modules/ganache/node_modules/@types/bn.js": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.0.tgz", + "integrity": "sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/abstract-provider": "^5.0.8", - "@ethersproject/bignumber": "^5.0.13", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/properties": "^5.0.7" + "@types/node": "*" } }, - "node_modules/ganache-core/node_modules/@ethersproject/address": { - "version": "5.0.9", + "node_modules/ganache/node_modules/@types/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/ganache/node_modules/@types/node": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.0.tgz", + "integrity": "sha512-eMhwJXc931Ihh4tkU+Y7GiLzT/y/DBNpNtr4yU9O2w3SYBsr9NaOPhQlLKRmoWtI54uNwuo0IOUFQjVOTZYRvw==", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/ganache/node_modules/@types/seedrandom": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.1.tgz", + "integrity": "sha512-giB9gzDeiCeloIXDgzFBCgjj1k4WxcDrZtGl6h1IqmUPlxF+Nx8Ve+96QCyDZ/HseB/uvDsKbpib9hU5cU53pw==", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/ganache/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true, - "dependencies": { - "@ethersproject/bignumber": "^5.0.13", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/keccak256": "^5.0.7", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/rlp": "^5.0.7" - } + "inBundle": true, + "license": "MIT" }, - "node_modules/ganache-core/node_modules/@ethersproject/base64": { - "version": "5.0.7", + "node_modules/ganache/node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/ganache/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, "funding": [ { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/bytes": "^5.0.9" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/ganache-core/node_modules/@ethersproject/bignumber": { - "version": "5.0.13", + "node_modules/ganache/node_modules/bufferutil": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.5.tgz", + "integrity": "sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", "optional": true, "dependencies": { - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8", - "bn.js": "^4.4.0" + "node-gyp-build": "^4.3.0" } }, - "node_modules/ganache-core/node_modules/@ethersproject/bytes": { - "version": "5.0.9", + "node_modules/ganache/node_modules/catering": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.0.tgz", + "integrity": "sha512-M5imwzQn6y+ODBfgi+cfgZv2hIUI6oYU/0f35Mdb1ujGeqeoI5tOnl9Q13DTH7LW+7er+NYq8stNOKZD/Z3U/A==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/logger": "^5.0.8" + "queue-tick": "^1.0.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/@ethersproject/constants": { - "version": "5.0.8", + "node_modules/ganache/node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/bignumber": "^5.0.13" + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/@ethersproject/hash": { - "version": "5.0.10", + "node_modules/ganache/node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/ganache/node_modules/emittery": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.0.tgz", + "integrity": "sha512-AGvFfs+d0JKCJQ4o01ASQLGPmSCxgfU9RFXvzPvZdjKK8oscynksuJhWrSTSw7j7Ep/sZct5b5ZhYCi8S/t0HQ==", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/ganache/node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/abstract-signer": "^5.0.10", - "@ethersproject/address": "^5.0.9", - "@ethersproject/bignumber": "^5.0.13", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/keccak256": "^5.0.7", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/properties": "^5.0.7", - "@ethersproject/strings": "^5.0.8" + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/@ethersproject/keccak256": { - "version": "5.0.7", + "node_modules/ganache/node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/bytes": "^5.0.9", - "js-sha3": "0.5.7" + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/@ethersproject/logger": { - "version": "5.0.8", + "node_modules/ganache/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true + "inBundle": true, + "license": "BSD-3-Clause" }, - "node_modules/ganache-core/node_modules/@ethersproject/networks": { - "version": "5.0.7", + "node_modules/ganache/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/ganache/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", "dev": true, "funding": [ { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], + "inBundle": true, "license": "MIT", - "optional": true, - "dependencies": { - "@ethersproject/logger": "^5.0.8" + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/@ethersproject/properties": { - "version": "5.0.7", + "node_modules/ganache/node_modules/keccak": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", + "integrity": "sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "hasInstallScript": true, + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/logger": "^5.0.8" + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" } }, - "node_modules/ganache-core/node_modules/@ethersproject/rlp": { - "version": "5.0.7", + "node_modules/ganache/node_modules/leveldown": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-6.1.0.tgz", + "integrity": "sha512-8C7oJDT44JXxh04aSSsfcMI8YiaGRhOFI9/pMEL7nWJLVsWajDPTRxsSHTM2WcTVY5nXM+SuRHzPPi0GbnDX+w==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "hasInstallScript": true, + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8" + "abstract-leveldown": "^7.2.0", + "napi-macros": "~2.0.0", + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=10.12.0" } }, - "node_modules/ganache-core/node_modules/@ethersproject/signing-key": { - "version": "5.0.8", + "node_modules/ganache/node_modules/leveldown/node_modules/abstract-leveldown": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz", + "integrity": "sha512-DnhQwcFEaYsvYDnACLZhMmCWd3rkOeEvglpa4q5i/5Jlm3UIsWaxVzuXvDLFCSCWRO3yy2/+V/G7FusFgejnfQ==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/properties": "^5.0.7", - "elliptic": "6.5.3" + "buffer": "^6.0.3", + "catering": "^2.0.0", + "is-buffer": "^2.0.5", + "level-concat-iterator": "^3.0.0", + "level-supports": "^2.0.1", + "queue-microtask": "^1.2.3" + }, + "engines": { + "node": ">=10" } }, - "node_modules/ganache-core/node_modules/@ethersproject/strings": { - "version": "5.0.8", + "node_modules/ganache/node_modules/leveldown/node_modules/level-concat-iterator": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-3.1.0.tgz", + "integrity": "sha512-BWRCMHBxbIqPxJ8vHOvKUsaO0v1sLYZtjN3K2iZJsRBYtp+ONsY6Jfi6hy9K3+zolgQRryhIn2NRZjZnWJ9NmQ==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/constants": "^5.0.8", - "@ethersproject/logger": "^5.0.8" + "catering": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ganache/node_modules/leveldown/node_modules/level-supports": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-2.1.0.tgz", + "integrity": "sha512-E486g1NCjW5cF78KGPrMDRBYzPuueMZ6VBXHT6gC7A8UYWGiM14fGgp+s/L1oFfDWSPV/+SFkYCmZ0SiESkRKA==", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ganache/node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/ganache/node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/ganache/node_modules/napi-macros": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/ganache/node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/ganache/node_modules/node-gyp-build": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", + "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" } }, - "node_modules/ganache-core/node_modules/@ethersproject/transactions": { - "version": "5.0.9", + "node_modules/ganache/node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/ganache/node_modules/queue-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.0.tgz", + "integrity": "sha512-ULWhjjE8BmiICGn3G8+1L9wFpERNxkf8ysxkAer4+TFdRefDaXOCV5m92aMB9FtBVmn/8sETXLXY6BfW7hyaWQ==", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/ganache/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "@ethersproject/address": "^5.0.9", - "@ethersproject/bignumber": "^5.0.13", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/constants": "^5.0.8", - "@ethersproject/keccak256": "^5.0.7", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/properties": "^5.0.7", - "@ethersproject/rlp": "^5.0.7", - "@ethersproject/signing-key": "^5.0.8" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/ganache-core/node_modules/@ethersproject/web": { - "version": "5.0.12", + "node_modules/ganache/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true, - "dependencies": { - "@ethersproject/base64": "^5.0.7", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/properties": "^5.0.7", - "@ethersproject/strings": "^5.0.8" - } + "inBundle": true, + "license": "MIT" }, - "node_modules/ganache-core/node_modules/@sindresorhus/is": { - "version": "0.14.0", + "node_modules/ganache/node_modules/secp256k1": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", + "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", "dev": true, + "hasInstallScript": true, + "inBundle": true, "license": "MIT", - "optional": true, + "dependencies": { + "elliptic": "^6.5.4", + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + }, "engines": { - "node": ">=6" + "node": ">=10.0.0" } }, - "node_modules/ganache-core/node_modules/@szmarczak/http-timer": { - "version": "1.1.2", + "node_modules/ganache/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "defer-to-connect": "^1.0.1" - }, - "engines": { - "node": ">=6" + "safe-buffer": "~5.2.0" } }, - "node_modules/ganache-core/node_modules/@types/bn.js": { - "version": "4.11.6", + "node_modules/ganache/node_modules/utf-8-validate": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz", + "integrity": "sha512-vLt1O5Pp+flcArHGIyKEQq883nBt8nN8tVBcoL0qUXj2XT1n7p70yGIq2VK98I5FdZ1YHc0wk/koOnHjnXWk1Q==", "dev": true, - "license": "MIT", + "optional": true, "dependencies": { - "@types/node": "*" + "node-gyp-build": "^4.3.0" } }, - "node_modules/ganache-core/node_modules/@types/node": { - "version": "14.14.20", + "node_modules/ganache/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, + "inBundle": true, "license": "MIT" }, - "node_modules/ganache-core/node_modules/@types/pbkdf2": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "peer": true, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/ganache-core/node_modules/@types/secp256k1": { - "version": "4.0.1", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" + "engines": { + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/ganache-core/node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true, - "license": "BSD-2-Clause" + "engines": { + "node": "*" + } }, - "node_modules/ganache-core/node_modules/abstract-leveldown": { - "version": "3.0.0", + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "dev": true, - "license": "MIT", "dependencies": { - "xtend": "~4.0.0" + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/accepts": { - "version": "1.3.7", + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/aes-js": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/ajv": { - "version": "6.12.6", + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, - "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/ganache-core/node_modules/ansi-styles": { - "version": "3.2.1", + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=4" + "node": ">= 6" } }, - "node_modules/ganache-core/node_modules/arr-diff": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" } }, - "node_modules/ganache-core/node_modules/arr-flatten": { - "version": "1.1.0", - "dev": true, - "license": "MIT", + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/arr-union": { - "version": "3.1.0", + "node_modules/graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=4.x" } }, - "node_modules/ganache-core/node_modules/array-flatten": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/array-unique": { - "version": "0.3.2", - "dev": true, - "license": "MIT", + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/asn1": { - "version": "0.2.4", - "dev": true, - "license": "MIT", + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", "dependencies": { - "safer-buffer": "~2.1.0" + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/asn1.js": { - "version": "5.4.1", + "node_modules/hardhat": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.8.2.tgz", + "integrity": "sha512-cBUqzZGOi+lwKHArWl5Be7zeFIwlu1IUXOna6k5XhORZ8hAWDVbAJBVfxgmjkcX5GffIf0C5g841zRxo36sQ5g==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" + "@ethereumjs/block": "^3.6.0", + "@ethereumjs/blockchain": "^5.5.0", + "@ethereumjs/common": "^2.6.0", + "@ethereumjs/tx": "^3.4.0", + "@ethereumjs/vm": "^5.6.0", + "@ethersproject/abi": "^5.1.2", + "@sentry/node": "^5.18.1", + "@solidity-parser/parser": "^0.14.0", + "@types/bn.js": "^5.1.0", + "@types/lru-cache": "^5.1.0", + "abort-controller": "^3.0.0", + "adm-zip": "^0.4.16", + "ansi-escapes": "^4.3.0", + "chalk": "^2.4.2", + "chokidar": "^3.4.0", + "ci-info": "^2.0.0", + "debug": "^4.1.1", + "enquirer": "^2.3.0", + "env-paths": "^2.2.0", + "eth-sig-util": "^2.5.2", + "ethereum-cryptography": "^0.1.2", + "ethereumjs-abi": "^0.6.8", + "ethereumjs-util": "^7.1.3", + "find-up": "^2.1.0", + "fp-ts": "1.19.3", + "fs-extra": "^7.0.1", + "glob": "^7.1.3", + "https-proxy-agent": "^5.0.0", + "immutable": "^4.0.0-rc.12", + "io-ts": "1.10.4", + "lodash": "^4.17.11", + "merkle-patricia-tree": "^4.2.2", + "mnemonist": "^0.38.0", + "mocha": "^7.2.0", + "node-fetch": "^2.6.0", + "qs": "^6.7.0", + "raw-body": "^2.4.1", + "resolve": "1.17.0", + "semver": "^6.3.0", + "slash": "^3.0.0", + "solc": "0.7.3", + "source-map-support": "^0.5.13", + "stacktrace-parser": "^0.1.10", + "true-case-path": "^2.2.1", + "tsort": "0.0.1", + "uuid": "^8.3.2", + "ws": "^7.4.6" + }, + "bin": { + "hardhat": "internal/cli/cli.js" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/ganache-core/node_modules/assert-plus": { - "version": "1.0.0", + "node_modules/hardhat/node_modules/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true + }, + "node_modules/hardhat/node_modules/ethereumjs-util": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", + "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", "dev": true, - "license": "MIT", + "dependencies": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + }, "engines": { - "node": ">=0.8" + "node": ">=10.0.0" } }, - "node_modules/ganache-core/node_modules/assign-symbols": { - "version": "1.0.0", + "node_modules/hardhat/node_modules/jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/ganache-core/node_modules/async": { - "version": "2.6.2", + "node_modules/hardhat/node_modules/level-ws": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/level-ws/-/level-ws-2.0.0.tgz", + "integrity": "sha512-1iv7VXx0G9ec1isqQZ7y5LmoZo/ewAsyDHNA8EFDW5hqH2Kqovm33nSFkSdnLLAK+I5FlT+lo5Cw9itGe+CpQA==", "dev": true, - "license": "MIT", "dependencies": { - "lodash": "^4.17.11" + "inherits": "^2.0.3", + "readable-stream": "^3.1.0", + "xtend": "^4.0.1" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/async-eventemitter": { - "version": "0.2.4", + "node_modules/hardhat/node_modules/merkle-patricia-tree": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-4.2.2.tgz", + "integrity": "sha512-eqZYNTshcYx9aESkSPr71EqwsR/QmpnObDEV4iLxkt/x/IoLYZYjJvKY72voP/27Vy61iMOrfOG6jrn7ttXD+Q==", "dev": true, - "license": "MIT", "dependencies": { - "async": "^2.4.0" + "@types/levelup": "^4.3.0", + "ethereumjs-util": "^7.1.2", + "level-mem": "^5.0.1", + "level-ws": "^2.0.0", + "readable-stream": "^3.6.0", + "rlp": "^2.2.4", + "semaphore-async-await": "^1.5.1" } }, - "node_modules/ganache-core/node_modules/async-limiter": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/asynckit": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/atob": { - "version": "2.1.2", + "node_modules/hardhat/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">= 4.5.0" + "node": ">= 6" } }, - "node_modules/ganache-core/node_modules/aws-sign2": { - "version": "0.7.0", + "node_modules/hardhat/node_modules/resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/aws4": { - "version": "1.11.0", + "node_modules/hardhat/node_modules/solc": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.7.3.tgz", + "integrity": "sha512-GAsWNAjGzIDg7VxzP6mPjdurby3IkGCjQcM8GFYZT6RyaoUZKmMU6Y7YwG+tFGhv7dwZ8rmR4iwFDrrD99JwqA==", "dev": true, - "license": "MIT" + "dependencies": { + "command-exists": "^1.2.8", + "commander": "3.0.2", + "follow-redirects": "^1.12.1", + "fs-extra": "^0.30.0", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "require-from-string": "^2.0.0", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solcjs" + }, + "engines": { + "node": ">=8.0.0" + } }, - "node_modules/ganache-core/node_modules/babel-code-frame": { - "version": "6.26.0", + "node_modules/hardhat/node_modules/solc/node_modules/fs-extra": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", "dev": true, - "license": "MIT", "dependencies": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0", + "path-is-absolute": "^1.0.0", + "rimraf": "^2.2.8" } }, - "node_modules/ganache-core/node_modules/babel-code-frame/node_modules/ansi-regex": { - "version": "2.1.1", + "node_modules/hardhat/node_modules/solc/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "bin": { + "semver": "bin/semver" } }, - "node_modules/ganache-core/node_modules/babel-code-frame/node_modules/ansi-styles": { - "version": "2.2.1", + "node_modules/hardhat/node_modules/ws": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/ganache-core/node_modules/babel-code-frame/node_modules/chalk": { - "version": "1.1.3", - "dev": true, - "license": "MIT", + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "function-bind": "^1.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4.0" } }, - "node_modules/ganache-core/node_modules/babel-code-frame/node_modules/js-tokens": { - "version": "3.0.2", + "node_modules/has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "dev": true, - "license": "MIT" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/ganache-core/node_modules/babel-code-frame/node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/babel-code-frame/node_modules/supports-color": { - "version": "2.0.0", + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "dev": true, - "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-core": { - "version": "6.26.3", + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", "dev": true, - "license": "MIT", "dependencies": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-core/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", + "node_modules/hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/ganache-core/node_modules/babel-core/node_modules/json5": { - "version": "0.5.1", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/babel-core/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/babel-core/node_modules/slash": { - "version": "1.0.0", - "dev": true, - "license": "MIT", + "node_modules/hash-base/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, - "node_modules/ganache-core/node_modules/babel-generator": { - "version": "6.26.1", - "dev": true, - "license": "MIT", + "node_modules/hash-base/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "dependencies": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/babel-generator/node_modules/jsesc": { - "version": "1.3.0", + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, - "license": "MIT", "bin": { - "jsesc": "bin/jsesc" + "he": "bin/he" } }, - "node_modules/ganache-core/node_modules/babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "dev": true, - "license": "MIT", + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "dependencies": { - "babel-helper-explode-assignable-expression": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/babel-helper-call-delegate": { - "version": "6.24.1", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true }, - "node_modules/ganache-core/node_modules/babel-helper-define-map": { - "version": "6.26.0", + "node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, - "license": "MIT", "dependencies": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/ganache-core/node_modules/babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "dev": true, - "license": "MIT", + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "dependencies": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" } }, - "node_modules/ganache-core/node_modules/babel-helper-function-name": { - "version": "6.24.1", + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "dev": true, - "license": "MIT", "dependencies": { - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/ganache-core/node_modules/babel-helper-get-function-arity": { - "version": "6.24.1", + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/babel-helper-hoist-variables": { - "version": "6.24.1", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "node_modules/ganache-core/node_modules/babel-helper-optimise-call-expression": { - "version": "6.24.1", + "node_modules/idna-uts46-hx": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz", + "integrity": "sha512-PWoF9Keq6laYdIRwwCdhTPl60xRqAloYNMQLiyUnG42VjT53oW07BXIRM+NK7eQjzXjAk2gUvX9caRxlnF9TAA==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "punycode": "2.1.0" + }, + "engines": { + "node": ">=4.0.0" } }, - "node_modules/ganache-core/node_modules/babel-helper-regex": { - "version": "6.26.0", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "node_modules/ganache-core/node_modules/babel-helper-remap-async-to-generator": { - "version": "6.24.1", + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true, - "license": "MIT", - "dependencies": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "engines": { + "node": ">= 4" } }, - "node_modules/ganache-core/node_modules/babel-helper-replace-supers": { - "version": "6.24.1", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==" + }, + "node_modules/immutable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", + "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", + "dev": true }, - "node_modules/ganache-core/node_modules/babel-helpers": { - "version": "6.24.1", + "node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/babel-messages": { - "version": "6.23.0", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true, - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.22.0" + "engines": { + "node": ">=0.8.19" } }, - "node_modules/ganache-core/node_modules/babel-plugin-check-es2015-constants": { - "version": "6.22.0", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.22.0" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/ganache-core/node_modules/babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "dev": true, - "license": "MIT" + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-async-to-generator": { - "version": "6.24.1", + "node_modules/inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", "dev": true, - "license": "MIT", "dependencies": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-functions": "^6.8.0", - "babel-runtime": "^6.22.0" + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", + "node_modules/inquirer/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", "dev": true, - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.22.0" + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true, - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.22.0" + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-block-scoping": { - "version": "6.26.0", + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-classes": { - "version": "6.24.1", + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", "dev": true, - "license": "MIT", "dependencies": { - "babel-helper-define-map": "^6.24.1", - "babel-helper-function-name": "^6.24.1", - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-helper-replace-supers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", + "node_modules/invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", "dev": true, - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "peer": true, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", + "node_modules/io-ts": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz", + "integrity": "sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.22.0" + "fp-ts": "^1.0.0" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-duplicate-keys": { - "version": "6.24.1", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.22.0" + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, - "license": "MIT", "dependencies": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-literals": { - "version": "6.22.0", + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.22.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-modules-amd": { - "version": "6.24.1", + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.26.2", + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-transform-strict-mode": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-types": "^6.26.0" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-modules-systemjs": { - "version": "6.24.1", - "dev": true, - "license": "MIT", + "node_modules/is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", "dependencies": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-modules-umd": { - "version": "6.24.1", + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, - "license": "MIT", "dependencies": { - "babel-plugin-transform-es2015-modules-amd": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-helper-replace-supers": "^6.24.1", - "babel-runtime": "^6.22.0" + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", "dev": true, - "license": "MIT", - "dependencies": { - "babel-helper-call-delegate": "^6.24.1", - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true, - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.22.0" + "node_modules/is-fn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fn/-/is-fn-1.0.0.tgz", + "integrity": "sha1-lUPV3nvPWwiiLsiiC65uKG1RDYw=", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true, - "license": "MIT", - "dependencies": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.22.0" - } + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-typeof-symbol": { - "version": "6.23.0", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.22.0" + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "regexpu-core": "^2.0.0" + "node_modules/is-hex-prefixed": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=", + "engines": { + "node": ">=6.5.0", + "npm": ">=3" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true, - "license": "MIT", - "dependencies": { - "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", - "babel-plugin-syntax-exponentiation-operator": "^6.8.0", - "babel-runtime": "^6.22.0" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-regenerator": { - "version": "6.26.0", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT", - "dependencies": { - "regenerator-transform": "^0.10.0" + "engines": { + "node": ">=0.12.0" } }, - "node_modules/ganache-core/node_modules/babel-plugin-transform-strict-mode": { - "version": "6.24.1", + "node_modules/is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-preset-env": { - "version": "1.7.0", + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, - "license": "MIT", "dependencies": { - "babel-plugin-check-es2015-constants": "^6.22.0", - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-to-generator": "^6.22.0", - "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoping": "^6.23.0", - "babel-plugin-transform-es2015-classes": "^6.23.0", - "babel-plugin-transform-es2015-computed-properties": "^6.22.0", - "babel-plugin-transform-es2015-destructuring": "^6.23.0", - "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0", - "babel-plugin-transform-es2015-for-of": "^6.23.0", - "babel-plugin-transform-es2015-function-name": "^6.22.0", - "babel-plugin-transform-es2015-literals": "^6.22.0", - "babel-plugin-transform-es2015-modules-amd": "^6.22.0", - "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", - "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0", - "babel-plugin-transform-es2015-modules-umd": "^6.23.0", - "babel-plugin-transform-es2015-object-super": "^6.22.0", - "babel-plugin-transform-es2015-parameters": "^6.23.0", - "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0", - "babel-plugin-transform-es2015-spread": "^6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "^6.22.0", - "babel-plugin-transform-es2015-template-literals": "^6.22.0", - "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0", - "babel-plugin-transform-es2015-unicode-regex": "^6.22.0", - "babel-plugin-transform-exponentiation-operator": "^6.22.0", - "babel-plugin-transform-regenerator": "^6.22.0", - "browserslist": "^3.2.6", - "invariant": "^2.2.2", - "semver": "^5.3.0" - } - }, - "node_modules/ganache-core/node_modules/babel-preset-env/node_modules/semver": { - "version": "5.7.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-register": { - "version": "6.26.0", + "node_modules/is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", "dev": true, - "license": "MIT", - "dependencies": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-register/node_modules/source-map-support": { - "version": "0.4.18", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "^0.5.6" + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/babel-runtime": { - "version": "6.26.0", + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "dev": true, - "license": "MIT", "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-template": { - "version": "6.26.0", + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-traverse": { - "version": "6.26.0", + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", "dev": true, - "license": "MIT", - "dependencies": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - } + "peer": true }, - "node_modules/ganache-core/node_modules/babel-traverse/node_modules/debug": { - "version": "2.6.9", + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, - "license": "MIT", "dependencies": { - "ms": "2.0.0" + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/babel-traverse/node_modules/globals": { - "version": "9.18.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, - "node_modules/ganache-core/node_modules/babel-traverse/node_modules/ms": { + "node_modules/isexe": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, - "node_modules/ganache-core/node_modules/babel-types": { - "version": "6.26.0", + "node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "dev": true, - "license": "MIT", "dependencies": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/ganache-core/node_modules/babel-types/node_modules/to-fast-properties": { - "version": "1.0.3", - "dev": true, - "license": "MIT", + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/babelify": { - "version": "7.3.0", + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "dev": true, - "license": "MIT", "dependencies": { - "babel-core": "^6.0.14", - "object-assign": "^4.0.0" - } - }, - "node_modules/ganache-core/node_modules/babylon": { - "version": "6.18.0", - "dev": true, - "license": "MIT", - "bin": { - "babylon": "bin/babylon.js" + "bignumber.js": "^9.0.0" } }, - "node_modules/ganache-core/node_modules/backoff": { - "version": "2.5.0", - "dev": true, - "license": "MIT", + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-rpc-engine": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-5.4.0.tgz", + "integrity": "sha512-rAffKbPoNDjuRnXkecTjnsE3xLLrb00rEkdgalINhaYVYIxDwWtvYBr9UFbhTvPB1B2qUOLoFd/cV6f4Q7mh7g==", "dependencies": { - "precond": "0.2" - }, - "engines": { - "node": ">= 0.6" + "eth-rpc-errors": "^3.0.0", + "safe-event-emitter": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/balanced-match": { - "version": "1.0.0", - "dev": true, - "license": "MIT" + "node_modules/json-rpc-random-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz", + "integrity": "sha1-uknZat7RRE27jaPSA3SKy7zeyMg=" }, - "node_modules/ganache-core/node_modules/base": { - "version": "0.11.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, - "node_modules/ganache-core/node_modules/base-x": { - "version": "3.0.8", - "dev": true, - "license": "MIT", + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", "dependencies": { - "safe-buffer": "^5.0.1" + "jsonify": "~0.0.0" } }, - "node_modules/ganache-core/node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "node_modules/json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "peer": true, "dependencies": { - "is-descriptor": "^1.0.0" + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/base64-js": { - "version": "1.5.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/bcrypt-pbkdf": { - "version": "1.0.2", + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/ganache-core/node_modules/bcrypt-pbkdf/node_modules/tweetnacl": { - "version": "0.14.5", - "dev": true, - "license": "Unlicense" - }, - "node_modules/ganache-core/node_modules/bignumber.js": { - "version": "9.0.1", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", "engines": { "node": "*" } }, - "node_modules/ganache-core/node_modules/bip39": { - "version": "2.5.0", - "dev": true, - "license": "ISC", + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dependencies": { - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1", - "safe-buffer": "^5.0.1", - "unorm": "^1.3.3" + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" } }, - "node_modules/ganache-core/node_modules/blakejs": { - "version": "1.1.0", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/ganache-core/node_modules/bluebird": { - "version": "3.7.2", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/bn.js": { - "version": "4.11.9", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/body-parser": { - "version": "1.19.0", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/keccak": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", + "integrity": "sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==", + "hasInstallScript": true, "dependencies": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" }, "engines": { - "node": ">= 0.8" + "node": ">=10.0.0" } }, - "node_modules/ganache-core/node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/keccak/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dependencies": { - "ms": "2.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/ganache-core/node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", + "node_modules/klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", "dev": true, - "license": "MIT", - "optional": true + "optionalDependencies": { + "graceful-fs": "^4.1.9" + } }, - "node_modules/ganache-core/node_modules/body-parser/node_modules/qs": { - "version": "6.7.0", + "node_modules/lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", "dev": true, - "license": "BSD-3-Clause", - "optional": true, + "peer": true, + "dependencies": { + "invert-kv": "^1.0.0" + }, "engines": { - "node": ">=0.6" + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/brace-expansion": { - "version": "1.1.11", + "node_modules/level-codec": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", "dev": true, - "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/brorand": { - "version": "1.1.0", + "node_modules/level-concat-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=6" + } }, - "node_modules/ganache-core/node_modules/browserify-aes": { - "version": "1.2.0", + "node_modules/level-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", "dev": true, - "license": "MIT", "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "errno": "~0.1.1" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/browserify-cipher": { - "version": "1.0.1", + "node_modules/level-iterator-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" + "inherits": "^2.0.4", + "readable-stream": "^3.4.0", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/browserify-des": { - "version": "1.0.2", + "node_modules/level-iterator-stream/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/ganache-core/node_modules/browserify-rsa": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "node_modules/ganache-core/node_modules/browserify-rsa/node_modules/bn.js": { - "version": "5.1.3", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/browserify-sign": { - "version": "4.2.1", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "node_modules/ganache-core/node_modules/browserify-sign/node_modules/bn.js": { - "version": "5.1.3", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/browserify-sign/node_modules/readable-stream": { - "version": "3.6.0", + "node_modules/level-mem": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/level-mem/-/level-mem-5.0.1.tgz", + "integrity": "sha512-qd+qUJHXsGSFoHTziptAKXoLX87QjR7v2KMbqncDXPxQuCdsQlzmyX+gwrEHhlzn08vkf8TyipYyMmiC6Gobzg==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "level-packager": "^5.0.3", + "memdown": "^5.0.0" }, "engines": { - "node": ">= 6" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/browserslist": { - "version": "3.2.8", + "node_modules/level-packager": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", + "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", "dev": true, - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30000844", - "electron-to-chromium": "^1.3.47" + "encoding-down": "^6.3.0", + "levelup": "^4.3.2" }, - "bin": { - "browserslist": "cli.js" - } - }, - "node_modules/ganache-core/node_modules/bs58": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "base-x": "^3.0.2" + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/bs58check": { - "version": "2.1.2", + "node_modules/level-supports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", "dev": true, - "license": "MIT", "dependencies": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/buffer": { - "version": "5.7.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", + "node_modules/level-ws": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/level-ws/-/level-ws-0.0.0.tgz", + "integrity": "sha1-Ny5RIXeSSgBCSwtDrvK7QkltIos=", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "readable-stream": "~1.0.15", + "xtend": "~2.1.1" } }, - "node_modules/ganache-core/node_modules/buffer-from": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/buffer-to-arraybuffer": { - "version": "0.0.5", - "dev": true, - "license": "MIT", - "optional": true + "node_modules/level-ws/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, - "node_modules/ganache-core/node_modules/buffer-xor": { - "version": "1.0.3", - "dev": true, - "license": "MIT" + "node_modules/level-ws/node_modules/object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" }, - "node_modules/ganache-core/node_modules/bufferutil": { - "version": "4.0.3", - "dev": true, - "hasInstallScript": true, - "license": "MIT", + "node_modules/level-ws/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dependencies": { - "node-gyp-build": "^4.2.0" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" } }, - "node_modules/ganache-core/node_modules/bytes": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.8" - } + "node_modules/level-ws/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, - "node_modules/ganache-core/node_modules/bytewise": { - "version": "1.1.0", - "dev": true, - "license": "MIT", + "node_modules/level-ws/node_modules/xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", "dependencies": { - "bytewise-core": "^1.2.2", - "typewise": "^1.0.3" + "object-keys": "~0.4.0" + }, + "engines": { + "node": ">=0.4" } }, - "node_modules/ganache-core/node_modules/bytewise-core": { - "version": "1.2.3", + "node_modules/levelup": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", "dev": true, - "license": "MIT", "dependencies": { - "typewise-core": "^1.2" + "deferred-leveldown": "~5.3.0", + "level-errors": "~2.0.0", + "level-iterator-stream": "~4.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/cache-base": { - "version": "1.0.1", + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "dev": true, - "license": "MIT", "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8.0" } }, - "node_modules/ganache-core/node_modules/cacheable-request": { - "version": "6.1.0", + "node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "dev": true, - "license": "MIT", - "optional": true, + "peer": true, "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/cacheable-request/node_modules/lowercase-keys": { - "version": "2.0.0", + "node_modules/load-json-file/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, - "license": "MIT", - "optional": true, + "peer": true, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/cachedown": { - "version": "1.0.0", + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, - "license": "MIT", "dependencies": { - "abstract-leveldown": "^2.4.1", - "lru-cache": "^3.2.0" + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/cachedown/node_modules/abstract-leveldown": { - "version": "2.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "~4.0.0" - } + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/ganache-core/node_modules/cachedown/node_modules/lru-cache": { - "version": "3.2.0", + "node_modules/lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==", "dev": true, - "license": "ISC", - "dependencies": { - "pseudomap": "^1.0.1" - } + "peer": true }, - "node_modules/ganache-core/node_modules/call-bind": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true }, - "node_modules/ganache-core/node_modules/caniuse-lite": { - "version": "1.0.30001174", - "dev": true, - "license": "CC-BY-4.0" + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, - "node_modules/ganache-core/node_modules/caseless": { - "version": "0.12.0", - "dev": true, - "license": "Apache-2.0" + "node_modules/lodash.flatmap": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", + "integrity": "sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=" }, - "node_modules/ganache-core/node_modules/chalk": { - "version": "2.4.2", + "node_modules/log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", "dev": true, - "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "chalk": "^2.4.2" }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/ganache-core/node_modules/checkpoint-store": { - "version": "1.1.0", + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "license": "ISC", "dependencies": { - "functional-red-black-tree": "^1.0.1" + "yallist": "^3.0.2" } }, - "node_modules/ganache-core/node_modules/chownr": { - "version": "1.1.4", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/ganache-core/node_modules/ci-info": { - "version": "2.0.0", - "dev": true, - "license": "MIT" + "node_modules/ltgt": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" }, - "node_modules/ganache-core/node_modules/cids": { - "version": "0.7.5", + "node_modules/mcl-wasm": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/mcl-wasm/-/mcl-wasm-0.7.9.tgz", + "integrity": "sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "class-is": "^1.1.0", - "multibase": "~0.6.0", - "multicodec": "^1.0.0", - "multihashes": "~0.4.15" - }, "engines": { - "node": ">=4.0.0", - "npm": ">=3.0.0" - } - }, - "node_modules/ganache-core/node_modules/cids/node_modules/multicodec": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.6.0", - "varint": "^5.0.0" + "node": ">=8.9.0" } }, - "node_modules/ganache-core/node_modules/cipher-base": { - "version": "1.0.4", - "dev": true, - "license": "MIT", + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dependencies": { + "hash-base": "^3.0.0", "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ganache-core/node_modules/class-is": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/class-utils": { - "version": "0.3.6", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" + "safe-buffer": "^5.1.2" } }, - "node_modules/ganache-core/node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", + "node_modules/memdown": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/memdown/-/memdown-5.1.0.tgz", + "integrity": "sha512-B3J+UizMRAlEArDjWHTMmadet+UKwHd3UjMgGBkZcKAxAYVPS9o0Yeiha4qvz7iGiL2Sb3igUft6p7nbFWctpw==", "dev": true, - "license": "MIT", "dependencies": { - "is-descriptor": "^0.1.0" + "abstract-leveldown": "~6.2.1", + "functional-red-black-tree": "~1.0.1", + "immediate": "~3.2.3", + "inherits": "~2.0.1", + "ltgt": "~2.2.0", + "safe-buffer": "~5.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/class-utils/node_modules/is-accessor-descriptor": { - "version": "0.1.6", + "node_modules/memdown/node_modules/abstract-leveldown": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", "dev": true, - "license": "MIT", "dependencies": { - "kind-of": "^3.0.2" + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } + "node_modules/memdown/node_modules/immediate": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", + "integrity": "sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=", + "dev": true }, - "node_modules/ganache-core/node_modules/class-utils/node_modules/is-buffer": { - "version": "1.1.6", + "node_modules/memdown/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, - "license": "MIT" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "node_modules/ganache-core/node_modules/class-utils/node_modules/is-data-descriptor": { - "version": "0.1.4", + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10.0" } }, - "node_modules/ganache-core/node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", + "node_modules/merkle-patricia-tree": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-2.3.2.tgz", + "integrity": "sha512-81PW5m8oz/pz3GvsAwbauj7Y00rqm81Tzad77tHBwU7pIAtN+TJnMSOJhxBKflSVYhptMMb9RskhqHqrSm1V+g==", "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "async": "^1.4.2", + "ethereumjs-util": "^5.0.0", + "level-ws": "0.0.0", + "levelup": "^1.2.1", + "memdown": "^1.0.0", + "readable-stream": "^2.0.0", + "rlp": "^2.0.0", + "semaphore": ">=1.0.1" } }, - "node_modules/ganache-core/node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.6", - "dev": true, - "license": "MIT", + "node_modules/merkle-patricia-tree/node_modules/abstract-leveldown": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz", + "integrity": "sha512-2++wDf/DYqkPR3o5tbfdhF96EfMApo1GpPfzOsR/ZYXdkSmELlvOOEAl9iKkRsktMPHdGjO4rtkBpf2I7TiTeA==", "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" + "xtend": "~4.0.0" } }, - "node_modules/ganache-core/node_modules/class-utils/node_modules/kind-of": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/merkle-patricia-tree/node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" }, - "node_modules/ganache-core/node_modules/clone": { - "version": "2.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" + "node_modules/merkle-patricia-tree/node_modules/deferred-leveldown": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-1.2.2.tgz", + "integrity": "sha512-uukrWD2bguRtXilKt6cAWKyoXrTSMo5m7crUdLfWQmu8kIm88w3QZoUL+6nhpfKVmhHANER6Re3sKoNoZ3IKMA==", + "dependencies": { + "abstract-leveldown": "~2.6.0" } }, - "node_modules/ganache-core/node_modules/clone-response": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/merkle-patricia-tree/node_modules/ethereumjs-util": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", + "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", "dependencies": { - "mimic-response": "^1.0.0" + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "ethjs-util": "^0.1.3", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1" } }, - "node_modules/ganache-core/node_modules/collection-visit": { - "version": "1.0.0", - "dev": true, - "license": "MIT", + "node_modules/merkle-patricia-tree/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/merkle-patricia-tree/node_modules/level-codec": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-7.0.1.tgz", + "integrity": "sha512-Ua/R9B9r3RasXdRmOtd+t9TCOEIIlts+TN/7XTT2unhDaL6sJn83S3rUyljbr6lVtw49N3/yA0HHjpV6Kzb2aQ==" + }, + "node_modules/merkle-patricia-tree/node_modules/level-errors": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-1.0.5.tgz", + "integrity": "sha512-/cLUpQduF6bNrWuAC4pwtUKA5t669pCsCi2XbmojG2tFeOr9j6ShtdDCtFFQO1DRt+EVZhx9gPzP9G2bUaG4ig==", "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "errno": "~0.1.1" } }, - "node_modules/ganache-core/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", + "node_modules/merkle-patricia-tree/node_modules/level-iterator-stream": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-1.3.1.tgz", + "integrity": "sha1-5Dt4sagUPm+pek9IXrjqUwNS8u0=", "dependencies": { - "color-name": "1.1.3" + "inherits": "^2.0.1", + "level-errors": "^1.0.3", + "readable-stream": "^1.0.33", + "xtend": "^4.0.0" } }, - "node_modules/ganache-core/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/combined-stream": { - "version": "1.0.8", - "dev": true, - "license": "MIT", + "node_modules/merkle-patricia-tree/node_modules/level-iterator-stream/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" } }, - "node_modules/ganache-core/node_modules/component-emitter": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" + "node_modules/merkle-patricia-tree/node_modules/levelup": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-1.3.9.tgz", + "integrity": "sha512-VVGHfKIlmw8w1XqpGOAGwq6sZm2WwWLmlDcULkKWQXEA5EopA8OBNJ2Ck2v6bdk8HeEZSbCSEgzXadyQFm76sQ==", + "dependencies": { + "deferred-leveldown": "~1.2.1", + "level-codec": "~7.0.0", + "level-errors": "~1.0.3", + "level-iterator-stream": "~1.3.0", + "prr": "~1.0.1", + "semver": "~5.4.1", + "xtend": "~4.0.0" + } }, - "node_modules/ganache-core/node_modules/concat-stream": { - "version": "1.6.2", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "license": "MIT", + "node_modules/merkle-patricia-tree/node_modules/memdown": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/memdown/-/memdown-1.4.1.tgz", + "integrity": "sha1-tOThkhdGZP+65BNhqlAPMRnv4hU=", "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" + "abstract-leveldown": "~2.7.1", + "functional-red-black-tree": "^1.0.1", + "immediate": "^3.2.3", + "inherits": "~2.0.1", + "ltgt": "~2.2.0", + "safe-buffer": "~5.1.1" } }, - "node_modules/ganache-core/node_modules/content-disposition": { - "version": "0.5.3", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/merkle-patricia-tree/node_modules/memdown/node_modules/abstract-leveldown": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.7.2.tgz", + "integrity": "sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w==", "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.6" + "xtend": "~4.0.0" } }, - "node_modules/ganache-core/node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "optional": true + "node_modules/merkle-patricia-tree/node_modules/semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "bin": { + "semver": "bin/semver" + } }, - "node_modules/ganache-core/node_modules/content-hash": { - "version": "2.5.2", + "node_modules/merkle-patricia-tree/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", "dev": true, - "license": "ISC", - "optional": true, "dependencies": { - "cids": "^0.7.1", - "multicodec": "^0.5.5", - "multihashes": "^0.4.15" + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" } }, - "node_modules/ganache-core/node_modules/content-type": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "engines": { "node": ">= 0.6" } }, - "node_modules/ganache-core/node_modules/convert-source-map": { - "version": "1.7.0", - "dev": true, - "license": "MIT", + "node_modules/mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/ganache-core/node_modules/convert-source-map/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/cookie": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "optional": true, + "mime-db": "1.51.0" + }, "engines": { "node": ">= 0.6" } }, - "node_modules/ganache-core/node_modules/cookie-signature": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/cookiejar": { - "version": "2.1.2", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/copy-descriptor": { - "version": "0.1.1", + "node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/core-js": { - "version": "2.6.12", - "dev": true, - "hasInstallScript": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/core-js-pure": { - "version": "3.8.2", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "dependencies": { + "dom-walk": "^0.1.0" } }, - "node_modules/ganache-core/node_modules/core-util-is": { - "version": "1.0.2", - "dev": true, - "license": "MIT" + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, - "node_modules/ganache-core/node_modules/cors": { - "version": "2.8.5", + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "object-assign": "^4", - "vary": "^1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 0.10" + "node": "*" } }, - "node_modules/ganache-core/node_modules/create-ecdh": { - "version": "4.0.4", + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" } }, - "node_modules/ganache-core/node_modules/create-hash": { - "version": "1.2.0", + "node_modules/mnemonist": { + "version": "0.38.5", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", + "integrity": "sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==", "dev": true, - "license": "MIT", "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" + "obliterator": "^2.0.0" } }, - "node_modules/ganache-core/node_modules/create-hmac": { - "version": "1.1.7", + "node_modules/mocha": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", + "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", "dev": true, - "license": "MIT", "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "chokidar": "3.3.0", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.5", + "ms": "2.1.1", + "node-environment-flags": "1.0.6", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" } }, - "node_modules/ganache-core/node_modules/cross-fetch": { - "version": "2.2.3", + "node_modules/mocha/node_modules/ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", "dev": true, - "license": "MIT", - "dependencies": { - "node-fetch": "2.1.2", - "whatwg-fetch": "2.0.4" + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/crypto-browserify": { - "version": "3.12.0", + "node_modules/mocha/node_modules/chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" }, "engines": { - "node": "*" + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.1.1" } }, - "node_modules/ganache-core/node_modules/d": { - "version": "1.0.1", + "node_modules/mocha/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", "dev": true, - "license": "ISC", "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" + "ms": "^2.1.1" } }, - "node_modules/ganache-core/node_modules/dashdash": { - "version": "1.14.1", + "node_modules/mocha/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, - "license": "MIT", "dependencies": { - "assert-plus": "^1.0.0" + "locate-path": "^3.0.0" }, "engines": { - "node": ">=0.10" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/debug": { - "version": "3.2.6", + "node_modules/mocha/node_modules/fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "deprecated": "\"Please update to latest v2.3 or v2.2\"", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/ganache-core/node_modules/decode-uri-component": { - "version": "0.2.0", + "node_modules/mocha/node_modules/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, - "license": "MIT", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, "engines": { - "node": ">=0.10" + "node": "*" } }, - "node_modules/ganache-core/node_modules/decompress-response": { - "version": "3.3.0", + "node_modules/mocha/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "mimic-response": "^1.0.0" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/deep-equal": { - "version": "1.1.1", + "node_modules/mocha/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "node_modules/mocha/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "license": "MIT", "dependencies": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ganache-core/node_modules/defer-to-connect": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/deferred-leveldown": { - "version": "4.0.2", + "node_modules/mocha/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, - "license": "MIT", "dependencies": { - "abstract-leveldown": "~5.0.0", - "inherits": "^2.0.3" + "p-limit": "^2.0.0" }, "engines": { "node": ">=6" } }, - "node_modules/ganache-core/node_modules/deferred-leveldown/node_modules/abstract-leveldown": { - "version": "5.0.0", + "node_modules/mocha/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "~4.0.0" - }, "engines": { "node": ">=6" } }, - "node_modules/ganache-core/node_modules/define-properties": { - "version": "1.1.3", + "node_modules/mocha/node_modules/readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", "dev": true, - "license": "MIT", "dependencies": { - "object-keys": "^1.0.12" + "picomatch": "^2.0.4" }, "engines": { - "node": ">= 0.4" + "node": ">= 8" } }, - "node_modules/ganache-core/node_modules/define-property": { - "version": "2.0.2", + "node_modules/mocha/node_modules/supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "dev": true, - "license": "MIT", "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/defined": { - "version": "1.0.0", - "dev": true, - "license": "MIT" + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/ganache-core/node_modules/delayed-stream": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } + "node_modules/mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true }, - "node_modules/ganache-core/node_modules/depd": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true }, - "node_modules/ganache-core/node_modules/des.js": { - "version": "1.0.1", + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" + }, + "node_modules/node-environment-flags": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", + "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" } }, - "node_modules/ganache-core/node_modules/destroy": { - "version": "1.0.4", + "node_modules/node-environment-flags/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true, - "license": "MIT", - "optional": true + "bin": { + "semver": "bin/semver" + } }, - "node_modules/ganache-core/node_modules/detect-indent": { - "version": "4.0.0", + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dev": true, - "license": "MIT", "dependencies": { - "repeating": "^2.0.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/ganache-core/node_modules/diffie-hellman": { - "version": "5.0.3", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" + "node_modules/node-gyp-build": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", + "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" } }, - "node_modules/ganache-core/node_modules/dom-walk": { - "version": "0.1.2", - "dev": true + "node_modules/node-releases": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==" }, - "node_modules/ganache-core/node_modules/dotignore": { - "version": "0.1.2", + "node_modules/nofilter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", + "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", "dev": true, - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.4" - }, - "bin": { - "ignored": "bin/ignored" + "engines": { + "node": ">=8" } }, - "node_modules/ganache-core/node_modules/duplexer3": { - "version": "0.1.4", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/ganache-core/node_modules/ecc-jsbn": { - "version": "0.1.2", + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, - "node_modules/ganache-core/node_modules/ee-first": { - "version": "1.1.1", + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "MIT", - "optional": true + "peer": true, + "bin": { + "semver": "bin/semver" + } }, - "node_modules/ganache-core/node_modules/electron-to-chromium": { - "version": "1.3.636", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, - "license": "ISC" - }, - "node_modules/ganache-core/node_modules/elliptic": { - "version": "6.5.3", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, - "node_modules/ganache-core/node_modules/encodeurl": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "optional": true, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ganache-core/node_modules/encoding": { - "version": "0.1.13", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.2" + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/encoding-down": { - "version": "5.0.4", + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", "dev": true, - "license": "MIT", - "dependencies": { - "abstract-leveldown": "^5.0.0", - "inherits": "^2.0.3", - "level-codec": "^9.0.0", - "level-errors": "^2.0.0", - "xtend": "^4.0.1" - }, + "peer": true, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/encoding-down/node_modules/abstract-leveldown": { - "version": "5.0.0", + "node_modules/number-to-bn": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", + "integrity": "sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "xtend": "~4.0.0" + "bn.js": "4.11.6", + "strip-hex-prefix": "1.0.0" }, "engines": { - "node": ">=6" + "node": ">=6.5.0", + "npm": ">=3" } }, - "node_modules/ganache-core/node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.2", + "node_modules/number-to-bn/node_modules/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, + "peer": true + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/ganache-core/node_modules/end-of-stream": { - "version": "1.4.4", + "node_modules/object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/errno": { - "version": "0.1.8", + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, - "license": "MIT", - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" + "engines": { + "node": ">= 0.4" } }, - "node_modules/ganache-core/node_modules/es-abstract": { - "version": "1.18.0-next.1", + "node_modules/object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", "dev": true, - "license": "MIT", "dependencies": { - "es-to-primitive": "^1.2.1", + "define-properties": "^1.1.2", "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/es-to-primitive": { - "version": "1.2.1", + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz", + "integrity": "sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==", "dev": true, - "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.8" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/es5-ext": { - "version": "0.10.53", + "node_modules/obliterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.1.tgz", + "integrity": "sha512-XnkiCrrBcIZQitJPAI36mrrpEUvatbte8hLcTcQwKA1v9NkCKasSi+UAguLsLDs/out7MoRzAlmz7VXvY6ph6w==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "license": "ISC", "dependencies": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" + "wrappy": "1" } }, - "node_modules/ganache-core/node_modules/es6-iterator": { - "version": "2.0.3", + "node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", "dev": true, - "license": "MIT", "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/es6-symbol": { - "version": "3.1.3", + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, - "license": "ISC", "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/ganache-core/node_modules/escape-html": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/escape-string-regexp": { - "version": "1.0.5", + "node_modules/os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", "dev": true, - "license": "MIT", + "peer": true, + "dependencies": { + "lcid": "^1.0.0" + }, "engines": { - "node": ">=0.8.0" + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/esutils": { - "version": "2.0.3", + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/etag": { - "version": "1.8.1", + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, - "license": "MIT", - "optional": true, + "dependencies": { + "p-try": "^1.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/eth-block-tracker": { - "version": "3.0.1", + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, - "license": "MIT", "dependencies": { - "eth-query": "^2.1.0", - "ethereumjs-tx": "^1.3.3", - "ethereumjs-util": "^5.1.3", - "ethjs-util": "^0.1.3", - "json-rpc-engine": "^3.6.0", - "pify": "^2.3.0", - "tape": "^4.6.3" + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/eth-block-tracker/node_modules/ethereumjs-tx": { - "version": "1.3.7", + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/eth-block-tracker/node_modules/ethereumjs-util": { - "version": "5.2.1", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "MPL-2.0", "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/eth-block-tracker/node_modules/pify": { - "version": "2.3.0", + "node_modules/parent-module/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/eth-ens-namehash": { - "version": "2.0.8", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "idna-uts46-hx": "^2.3.1", - "js-sha3": "^0.5.7" - } + "node_modules/parse-headers": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.4.tgz", + "integrity": "sha512-psZ9iZoCNFLrgRjZ1d8mn0h9WRqJwFxM9q3x7iUjN/YT2OksthDJ5TiPCu2F38kS4zutqfW+YdVVkBZZx3/1aw==" }, - "node_modules/ganache-core/node_modules/eth-json-rpc-infura": { - "version": "3.2.1", + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", "dev": true, - "license": "ISC", + "peer": true, "dependencies": { - "cross-fetch": "^2.1.1", - "eth-json-rpc-middleware": "^1.5.0", - "json-rpc-engine": "^3.4.0", - "json-rpc-error": "^2.0.0" + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware": { - "version": "1.6.0", - "dev": true, - "license": "ISC", - "dependencies": { - "async": "^2.5.0", - "eth-query": "^2.1.2", - "eth-tx-summary": "^3.1.2", - "ethereumjs-block": "^1.6.0", - "ethereumjs-tx": "^1.3.3", - "ethereumjs-util": "^5.1.2", - "ethereumjs-vm": "^2.1.0", - "fetch-ponyfill": "^4.0.0", - "json-rpc-engine": "^3.6.0", - "json-rpc-error": "^2.0.0", - "json-stable-stringify": "^1.0.1", - "promise-to-callback": "^1.0.0", - "tape": "^4.6.3" - } + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/abstract-leveldown": { - "version": "2.6.3", + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "~4.0.0" + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/deferred-leveldown": { - "version": "1.2.2", + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~2.6.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ethereumjs-account": { - "version": "2.0.5", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "ethereumjs-util": "^5.0.0", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ethereumjs-block": { - "version": "1.7.1", + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "async": "^2.0.1", - "ethereum-common": "0.2.0", - "ethereumjs-tx": "^1.2.2", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ethereumjs-block/node_modules/ethereum-common": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ethereumjs-tx": { - "version": "1.3.7", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" - } + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ethereumjs-util": { - "version": "5.2.1", + "node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", "dev": true, - "license": "MPL-2.0", + "peer": true, "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ethereumjs-vm": { - "version": "2.6.0", + "node_modules/path-type/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "async": "^2.1.2", - "async-eventemitter": "^0.2.2", - "ethereumjs-account": "^2.0.3", - "ethereumjs-block": "~2.2.0", - "ethereumjs-common": "^1.1.0", - "ethereumjs-util": "^6.0.0", - "fake-merkle-patricia-tree": "^1.0.1", - "functional-red-black-tree": "^1.0.1", - "merkle-patricia-tree": "^2.3.2", - "rustbn.js": "~0.2.0", - "safe-buffer": "^5.1.1" + "peer": true, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ethereumjs-vm/node_modules/ethereumjs-block": { - "version": "2.2.2", + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "async": "^2.0.1", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.1", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" + "engines": { + "node": "*" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ethereumjs-vm/node_modules/ethereumjs-block/node_modules/ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "license": "MPL-2.0", + "node_modules/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", "dependencies": { - "bn.js": "^4.11.0", "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ethereumjs-vm/node_modules/ethereumjs-tx": { - "version": "2.1.2", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.0.0" + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ethereumjs-vm/node_modules/ethereumjs-util": { - "version": "6.2.1", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" - } + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/isarray": { - "version": "0.0.1", - "dev": true, - "license": "MIT" + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/level-codec": { - "version": "7.0.1", + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/level-errors": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "errno": "~0.1.1" + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/level-iterator-stream": { - "version": "1.3.1", + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" + "peer": true, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/level-iterator-stream/node_modules/readable-stream": { - "version": "1.1.14", + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/level-ws": { - "version": "0.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" + "node_modules/precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=", + "engines": { + "node": ">= 0.6" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/level-ws/node_modules/readable-stream": { - "version": "1.0.34", + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/level-ws/node_modules/xtend": { - "version": "2.1.2", + "node_modules/prettier": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", + "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", "dev": true, - "dependencies": { - "object-keys": "~0.4.0" + "bin": { + "prettier": "bin-prettier.js" }, "engines": { - "node": ">=0.4" + "node": ">=10.13.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/levelup": { - "version": "1.3.9", + "node_modules/printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", "dev": true, - "license": "MIT", - "dependencies": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/ltgt": { - "version": "2.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/memdown": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "engines": { + "node": ">= 0.6.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/memdown/node_modules/abstract-leveldown": { - "version": "2.7.2", + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "~4.0.0" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/merkle-patricia-tree": { - "version": "2.3.2", - "dev": true, - "license": "MPL-2.0", + "node_modules/promise-to-callback": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/promise-to-callback/-/promise-to-callback-1.0.0.tgz", + "integrity": "sha1-XSp0kBC/tn2WNZj805YHRqaP7vc=", "dependencies": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" + "is-fn": "^1.0.0", + "set-immediate-shim": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/merkle-patricia-tree/node_modules/async": { - "version": "1.5.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/object-keys": { - "version": "0.4.0", - "dev": true, - "license": "MIT" + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/semver": { - "version": "5.4.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node_modules/punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=", + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/eth-json-rpc-middleware/node_modules/string_decoder": { - "version": "0.10.31", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/eth-lib": { - "version": "0.1.29", + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "nano-json-stream-parser": "^0.1.2", - "servify": "^0.1.12", - "ws": "^3.0.0", - "xhr-request-promise": "^0.1.2" + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/eth-query": { - "version": "2.1.2", + "node_modules/querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", "dev": true, - "license": "ISC", - "dependencies": { - "json-rpc-random-id": "^1.0.0", - "xtend": "^4.0.1" + "engines": { + "node": ">=0.4.x" } }, - "node_modules/ganache-core/node_modules/eth-sig-util": { - "version": "3.0.0", - "dev": true, - "license": "ISC", + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dependencies": { - "buffer": "^5.2.1", - "elliptic": "^6.4.0", - "ethereumjs-abi": "0.6.5", - "ethereumjs-util": "^5.1.1", - "tweetnacl": "^1.0.0", - "tweetnacl-util": "^0.15.0" + "safe-buffer": "^5.1.0" } }, - "node_modules/ganache-core/node_modules/eth-sig-util/node_modules/ethereumjs-abi": { - "version": "0.6.5", + "node_modules/raw-body": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", + "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", "dev": true, - "license": "MIT", "dependencies": { - "bn.js": "^4.10.0", - "ethereumjs-util": "^4.3.0" + "bytes": "3.1.1", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/ganache-core/node_modules/eth-sig-util/node_modules/ethereumjs-abi/node_modules/ethereumjs-util": { - "version": "4.5.1", + "node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", "dev": true, - "license": "MPL-2.0", + "peer": true, "dependencies": { - "bn.js": "^4.8.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.0.0" + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-sig-util/node_modules/ethereumjs-util": { - "version": "5.2.1", + "node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", "dev": true, - "license": "MPL-2.0", + "peer": true, "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary": { - "version": "3.2.4", + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "dev": true, - "license": "ISC", + "peer": true, "dependencies": { - "async": "^2.1.2", - "clone": "^2.0.0", - "concat-stream": "^1.5.1", - "end-of-stream": "^1.1.0", - "eth-query": "^2.0.2", - "ethereumjs-block": "^1.4.1", - "ethereumjs-tx": "^1.1.1", - "ethereumjs-util": "^5.0.1", - "ethereumjs-vm": "^2.6.0", - "through2": "^2.0.3" + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/abstract-leveldown": { - "version": "2.6.3", + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "xtend": "~4.0.0" + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/deferred-leveldown": { - "version": "1.2.2", - "dev": true, - "license": "MIT", + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dependencies": { - "abstract-leveldown": "~2.6.0" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ethereumjs-account": { - "version": "2.0.5", + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "license": "MPL-2.0", "dependencies": { - "ethereumjs-util": "^5.0.0", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ethereumjs-block": { - "version": "1.7.1", + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "async": "^2.0.1", - "ethereum-common": "0.2.0", - "ethereumjs-tx": "^1.2.2", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ethereumjs-block/node_modules/ethereum-common": { - "version": "0.2.0", - "dev": true, - "license": "MIT" + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ethereumjs-tx": { - "version": "1.3.7", + "node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" + "engines": { + "node": ">=6.5.0" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "license": "MPL-2.0", + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ethereumjs-vm": { - "version": "2.6.0", - "dev": true, - "license": "MPL-2.0", + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dependencies": { - "async": "^2.1.2", - "async-eventemitter": "^0.2.2", - "ethereumjs-account": "^2.0.3", - "ethereumjs-block": "~2.2.0", - "ethereumjs-common": "^1.1.0", - "ethereumjs-util": "^6.0.0", - "fake-merkle-patricia-tree": "^1.0.1", - "functional-red-black-tree": "^1.0.1", - "merkle-patricia-tree": "^2.3.2", - "rustbn.js": "~0.2.0", - "safe-buffer": "^5.1.1" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ethereumjs-vm/node_modules/ethereumjs-block": { - "version": "2.2.2", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "async": "^2.0.1", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.1", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ethereumjs-vm/node_modules/ethereumjs-block/node_modules/ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ethereumjs-vm/node_modules/ethereumjs-tx": { - "version": "2.1.2", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.0.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ethereumjs-vm/node_modules/ethereumjs-util": { - "version": "6.2.1", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/isarray": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/level-codec": { - "version": "7.0.1", - "dev": true, - "license": "MIT" + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/level-errors": { - "version": "1.0.5", - "dev": true, - "license": "MIT", + "node_modules/resolve": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", + "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", "dependencies": { - "errno": "~0.1.1" + "is-core-module": "^2.8.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/level-iterator-stream": { - "version": "1.3.1", + "node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/level-iterator-stream/node_modules/readable-stream": { - "version": "1.1.14", + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", "dev": true, - "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/level-ws": { - "version": "0.0.0", + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, - "license": "MIT", "dependencies": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/level-ws/node_modules/readable-stream": { - "version": "1.0.34", - "dev": true, - "license": "MIT", + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "hash-base": "^3.0.0", + "inherits": "^2.0.1" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/level-ws/node_modules/xtend": { - "version": "2.1.2", - "dev": true, + "node_modules/rlp": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.7.tgz", + "integrity": "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==", "dependencies": { - "object-keys": "~0.4.0" + "bn.js": "^5.2.0" }, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/levelup": { - "version": "1.3.9", - "dev": true, - "license": "MIT", - "dependencies": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" + "bin": { + "rlp": "bin/rlp" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/ltgt": { - "version": "2.2.1", - "dev": true, - "license": "MIT" + "node_modules/rlp/node_modules/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/memdown": { - "version": "1.4.1", + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "dev": true, - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" + "engines": { + "node": ">=0.12.0" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/memdown/node_modules/abstract-leveldown": { - "version": "2.7.2", + "node_modules/rustbn.js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/rustbn.js/-/rustbn.js-0.2.0.tgz", + "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==" + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, - "license": "MIT", "dependencies": { - "xtend": "~4.0.0" + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/merkle-patricia-tree": { - "version": "2.3.2", - "dev": true, - "license": "MPL-2.0", + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-event-emitter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-event-emitter/-/safe-event-emitter-1.0.1.tgz", + "integrity": "sha512-e1wFe99A91XYYxoQbcq2ZJUWurxEyP8vfz7A7vuUe1s95q8r5ebraVaA1BukYJcpM6V16ugWoD9vngi8Ccu5fg==", + "deprecated": "Renamed to @metamask/safe-event-emitter", "dependencies": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" + "events": "^3.0.0" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/merkle-patricia-tree/node_modules/async": { - "version": "1.5.2", - "dev": true, - "license": "MIT" + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/object-keys": { - "version": "0.4.0", - "dev": true, - "license": "MIT" + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==" }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" + "node_modules/secp256k1": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", + "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", + "hasInstallScript": true, + "dependencies": { + "elliptic": "^6.5.4", + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=10.0.0" + } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/semver": { - "version": "5.4.1", + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true + }, + "node_modules/semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/semaphore-async-await": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/semaphore-async-await/-/semaphore-async-await-1.5.1.tgz", + "integrity": "sha1-hXvvXjZEYBykuVcLh+nfXKEpdPo=", "dev": true, - "license": "ISC", + "engines": { + "node": ">=4.1" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "bin": { - "semver": "bin/semver" + "semver": "bin/semver.js" } }, - "node_modules/ganache-core/node_modules/eth-tx-summary/node_modules/string_decoder": { - "version": "0.10.31", - "dev": true, - "license": "MIT" + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true }, - "node_modules/ganache-core/node_modules/ethashjs": { - "version": "0.0.8", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "async": "^2.1.2", - "buffer-xor": "^2.0.1", - "ethereumjs-util": "^7.0.2", - "miller-rabin": "^4.0.0" + "node_modules/set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/ethashjs/node_modules/bn.js": { - "version": "5.1.3", - "dev": true, - "license": "MIT" + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, - "node_modules/ganache-core/node_modules/ethashjs/node_modules/buffer-xor": { - "version": "2.0.2", - "dev": true, - "license": "MIT", + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dependencies": { - "safe-buffer": "^5.1.1" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" } }, - "node_modules/ganache-core/node_modules/ethashjs/node_modules/ethereumjs-util": { - "version": "7.0.7", + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, - "license": "MPL-2.0", "dependencies": { - "@types/bn.js": "^4.11.3", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.4" + "shebang-regex": "^1.0.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/ethereum-bloom-filters": { - "version": "1.0.7", + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "js-sha3": "^0.8.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/ethereum-bloom-filters/node_modules/js-sha3": { - "version": "0.8.0", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/ethereum-common": { - "version": "0.0.18", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/ethereum-cryptography": { - "version": "0.1.3", + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "dev": true, - "license": "MIT", "dependencies": { - "@types/pbkdf2": "^3.0.0", - "@types/secp256k1": "^4.0.1", - "blakejs": "^1.1.0", - "browserify-aes": "^1.2.0", - "bs58check": "^2.1.2", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "hash.js": "^1.1.7", - "keccak": "^3.0.0", - "pbkdf2": "^3.0.17", - "randombytes": "^2.1.0", - "safe-buffer": "^5.1.2", - "scrypt-js": "^3.0.0", - "secp256k1": "^4.0.1", - "setimmediate": "^1.0.5" + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/ethereumjs-abi": { - "version": "0.6.8", - "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^4.11.8", - "ethereumjs-util": "^6.0.0" - } + "node_modules/signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", + "dev": true }, - "node_modules/ganache-core/node_modules/ethereumjs-account": { + "node_modules/slash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "ethereumjs-util": "^6.0.0", - "rlp": "^2.2.1", - "safe-buffer": "^5.1.1" + "engines": { + "node": ">=8" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block": { - "version": "2.2.2", + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", "dev": true, - "license": "MPL-2.0", "dependencies": { - "async": "^2.0.1", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.1", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/abstract-leveldown": { - "version": "2.6.3", + "node_modules/solc": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.15.tgz", + "integrity": "sha512-Riv0GNHNk/SddN/JyEuFKwbcWcEeho15iyupTSHw5Np6WuXA5D8kEHbyzDHi6sqmvLzu2l+8b1YmL8Ytple+8w==", "dev": true, - "license": "MIT", "dependencies": { - "xtend": "~4.0.0" + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=10.0.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/deferred-leveldown": { - "version": "1.2.2", + "node_modules/solc/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~2.6.0" + "engines": { + "node": ">= 12" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/ethereumjs-util": { - "version": "5.2.1", + "node_modules/solc/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" + "bin": { + "semver": "bin/semver" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/isarray": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/level-codec": { - "version": "7.0.1", + "node_modules/solhint": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.3.6.tgz", + "integrity": "sha512-HWUxTAv2h7hx3s3hAab3ifnlwb02ZWhwFU/wSudUHqteMS3ll9c+m1FlGn9V8ztE2rf3Z82fQZA005Wv7KpcFA==", "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/level-errors": { - "version": "1.0.5", - "dev": true, - "license": "MIT", "dependencies": { - "errno": "~0.1.1" + "@solidity-parser/parser": "^0.13.2", + "ajv": "^6.6.1", + "antlr4": "4.7.1", + "ast-parents": "0.0.1", + "chalk": "^2.4.2", + "commander": "2.18.0", + "cosmiconfig": "^5.0.7", + "eslint": "^5.6.0", + "fast-diff": "^1.1.2", + "glob": "^7.1.3", + "ignore": "^4.0.6", + "js-yaml": "^3.12.0", + "lodash": "^4.17.11", + "semver": "^6.3.0" + }, + "bin": { + "solhint": "solhint.js" + }, + "optionalDependencies": { + "prettier": "^1.14.3" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/level-iterator-stream": { - "version": "1.3.1", + "node_modules/solhint/node_modules/@solidity-parser/parser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.13.2.tgz", + "integrity": "sha512-RwHnpRnfrnD2MSPveYoPh8nhofEvX7fgjHk1Oq+NNvCcLx4r1js91CO9o+F/F3fBzOCyvm8kKRTriFICX/odWw==", "dev": true, - "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" + "antlr4ts": "^0.5.0-alpha.4" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/level-iterator-stream/node_modules/readable-stream": { - "version": "1.1.14", + "node_modules/solhint/node_modules/commander": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", + "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", + "dev": true + }, + "node_modules/solhint/node_modules/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/level-ws": { - "version": "0.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/level-ws/node_modules/readable-stream": { - "version": "1.0.34", + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/level-ws/node_modules/xtend": { - "version": "2.1.2", + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "dependencies": { - "object-keys": "~0.4.0" - }, "engines": { - "node": ">=0.4" + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/levelup": { - "version": "1.3.9", + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/ltgt": { - "version": "2.2.1", + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true, - "license": "MIT" + "peer": true }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/memdown": { - "version": "1.4.1", + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/memdown/node_modules/abstract-leveldown": { - "version": "2.7.2", + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true, - "license": "MIT", + "peer": true + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", "dependencies": { - "xtend": "~4.0.0" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/merkle-patricia-tree": { - "version": "2.3.2", + "node_modules/sshpk/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "node_modules/stacktrace-parser": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", + "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", "dev": true, - "license": "MPL-2.0", "dependencies": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/merkle-patricia-tree/node_modules/async": { - "version": "1.5.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/object-keys": { - "version": "0.4.0", + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=8" + } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/safe-buffer": { - "version": "5.1.2", + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true, - "license": "MIT" + "engines": { + "node": ">= 0.6" + } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/semver": { - "version": "5.4.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-block/node_modules/string_decoder": { - "version": "0.10.31", - "dev": true, - "license": "MIT" + "node_modules/string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", + "dev": true }, - "node_modules/ganache-core/node_modules/ethereumjs-blockchain": { - "version": "4.0.4", + "node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, - "license": "MPL-2.0", "dependencies": { - "async": "^2.6.1", - "ethashjs": "~0.0.7", - "ethereumjs-block": "~2.2.2", - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.1.0", - "flow-stoplight": "^1.0.0", - "level-mem": "^3.0.1", - "lru-cache": "^5.1.1", - "rlp": "^2.2.2", - "semaphore": "^1.1.0" + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/ethereumjs-common": { - "version": "1.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/ethereumjs-tx": { - "version": "2.1.2", + "node_modules/string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", "dev": true, - "license": "MPL-2.0", "dependencies": { - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.0.0" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/ethereumjs-util": { - "version": "6.2.1", + "node_modules/string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", "dev": true, - "license": "MPL-2.0", "dependencies": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm": { - "version": "4.2.0", + "node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, - "license": "MPL-2.0", "dependencies": { - "async": "^2.1.2", - "async-eventemitter": "^0.2.2", - "core-js-pure": "^3.0.1", - "ethereumjs-account": "^3.0.0", - "ethereumjs-block": "^2.2.2", - "ethereumjs-blockchain": "^4.0.3", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.2", - "ethereumjs-util": "^6.2.0", - "fake-merkle-patricia-tree": "^1.0.1", - "functional-red-black-tree": "^1.0.1", - "merkle-patricia-tree": "^2.3.2", - "rustbn.js": "~0.2.0", - "safe-buffer": "^5.1.1", - "util.promisify": "^1.0.0" + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/abstract-leveldown": { - "version": "2.6.3", + "node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "xtend": "~4.0.0" + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/deferred-leveldown": { - "version": "1.2.2", - "dev": true, - "license": "MIT", + "node_modules/strip-hex-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha1-DF8VX+8RUTczd96du1iNoFUA428=", "dependencies": { - "abstract-leveldown": "~2.6.0" + "is-hex-prefixed": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/isarray": { - "version": "0.0.1", + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, - "license": "MIT" + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/level-codec": { - "version": "7.0.1", - "dev": true, - "license": "MIT" + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/level-errors": { - "version": "1.0.5", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", "dev": true, - "license": "MIT", "dependencies": { - "errno": "~0.1.1" + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/level-iterator-stream": { - "version": "1.3.1", + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", "dev": true, - "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/level-iterator-stream/node_modules/readable-stream": { - "version": "1.1.14", + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "engines": { + "node": ">=8" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/level-ws": { - "version": "0.0.0", + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" + "engines": { + "node": ">=8" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/level-ws/node_modules/readable-stream": { - "version": "1.0.34", + "node_modules/table/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/level-ws/node_modules/xtend": { - "version": "2.1.2", + "node_modules/table/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "dependencies": { - "object-keys": "~0.4.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" }, "engines": { - "node": ">=0.4" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/levelup": { - "version": "1.3.9", + "node_modules/table/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, - "license": "MIT", "dependencies": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/ltgt": { - "version": "2.2.1", + "node_modules/testrpc": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/testrpc/-/testrpc-0.0.1.tgz", + "integrity": "sha512-afH1hO+SQ/VPlmaLUFj2636QMeDvPCeQMc/9RBMW0IfjNe9gFD9Ra3ShqYkB7py0do1ZcCna/9acHyzTJ+GcNA==", + "deprecated": "testrpc has been renamed to ganache-cli, please use this package from now on.", "dev": true, - "license": "MIT" + "peer": true }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/memdown": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" - } + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/memdown/node_modules/abstract-leveldown": { - "version": "2.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "~4.0.0" - } + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/merkle-patricia-tree": { - "version": "2.3.2", + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, - "license": "MPL-2.0", "dependencies": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/merkle-patricia-tree/node_modules/async": { - "version": "1.5.2", - "dev": true, - "license": "MIT" + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "engines": { + "node": ">=4" + } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/merkle-patricia-tree/node_modules/ethereumjs-util": { - "version": "5.2.1", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "MPL-2.0", "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/object-keys": { - "version": "0.4.0", + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=0.6" + } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/semver": { - "version": "5.4.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node_modules/tough-cookie/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/ethereumjs-vm/node_modules/string_decoder": { - "version": "0.10.31", - "dev": true, - "license": "MIT" + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true }, - "node_modules/ganache-core/node_modules/ethereumjs-wallet": { - "version": "0.6.5", + "node_modules/true-case-path": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-2.2.1.tgz", + "integrity": "sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q==", + "dev": true + }, + "node_modules/truffle-plugin-verify": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/truffle-plugin-verify/-/truffle-plugin-verify-0.5.20.tgz", + "integrity": "sha512-s6zG7QbVK5tWPAhRz1oKi/M8SXdRgcWR4PRuHM/BB0qZBcE/82WmnqyC2D/qfqEY+BCgUUWXfc/hyzsgH4dyNw==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "aes-js": "^3.1.1", - "bs58check": "^2.1.2", - "ethereum-cryptography": "^0.1.3", - "ethereumjs-util": "^6.0.0", - "randombytes": "^2.0.6", - "safe-buffer": "^5.1.2", - "scryptsy": "^1.2.1", - "utf8": "^3.0.0", - "uuid": "^3.3.2" + "axios": "^0.21.1", + "cli-logger": "^0.5.40", + "delay": "^5.0.0", + "querystring": "^0.2.1" } }, - "node_modules/ganache-core/node_modules/ethjs-unit": { - "version": "0.1.6", + "node_modules/ts-command-line-args": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", + "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "bn.js": "4.11.6", - "number-to-bn": "1.7.0" + "chalk": "^4.1.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.1.0", + "string-format": "^2.0.0" }, - "engines": { - "node": ">=6.5.0", - "npm": ">=3" + "bin": { + "write-markdown": "dist/write-markdown.js" } }, - "node_modules/ganache-core/node_modules/ethjs-unit/node_modules/bn.js": { - "version": "4.11.6", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/ethjs-util": { - "version": "0.1.6", + "node_modules/ts-command-line-args/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { - "is-hex-prefixed": "1.0.0", - "strip-hex-prefix": "1.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=6.5.0", - "npm": ">=3" - } - }, - "node_modules/ganache-core/node_modules/eventemitter3": { - "version": "4.0.4", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/events": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ganache-core/node_modules/evp_bytestokey": { - "version": "1.0.3", + "node_modules/ts-command-line-args/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/ganache-core/node_modules/expand-brackets": { - "version": "2.1.4", + "node_modules/ts-command-line-args/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "color-name": "~1.1.4" }, "engines": { - "node": ">=0.10.0" + "node": ">=7.0.0" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/debug": { - "version": "2.6.9", + "node_modules/ts-command-line-args/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-command-line-args/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", + "node_modules/ts-command-line-args/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { - "is-descriptor": "^0.1.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", + "node_modules/ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", "dev": true, - "license": "MIT", + "peerDependencies": { + "typescript": ">=3.7.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsort": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz", + "integrity": "sha1-4igPXoF/i/QnVlf9D5rr1E9aJ4Y=", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "dependencies": { - "is-extendable": "^0.1.0" + "safe-buffer": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/is-accessor-descriptor": { - "version": "0.1.6", + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dev": true + }, + "node_modules/tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "dev": true, - "license": "MIT", "dependencies": { - "kind-of": "^3.0.2" + "prelude-ls": "~1.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8.0" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/is-buffer": { - "version": "1.1.6", + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/is-data-descriptor": { - "version": "0.1.4", + "node_modules/typechain": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.1.tgz", + "integrity": "sha512-fA7clol2IP/56yq6vkMTR+4URF1nGjV82Wx6Rf09EsqD4tkzMAvEaqYxVFCavJm/1xaRga/oD55K+4FtuXwQOQ==", "dev": true, - "license": "MIT", "dependencies": { - "kind-of": "^3.0.2" + "@types/prettier": "^2.1.1", + "debug": "^4.3.1", + "fs-extra": "^7.0.0", + "glob": "7.1.7", + "js-sha3": "^0.8.0", + "lodash": "^4.17.15", + "mkdirp": "^1.0.4", + "prettier": "^2.3.1", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" }, - "engines": { - "node": ">=0.10.0" + "bin": { + "typechain": "dist/cli/cli.js" + }, + "peerDependencies": { + "typescript": ">=4.3.0" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", + "node_modules/typechain/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "dev": true, - "license": "MIT", "dependencies": { - "is-buffer": "^1.1.5" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.6", + "node_modules/typechain/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "bin": { + "mkdirp": "bin/cmd.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/is-extendable": { - "version": "0.1.1", + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, - "license": "MIT", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=0.10.0" + "node": ">=14.17" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/kind-of": { - "version": "5.1.0", + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", "dev": true, - "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/ganache-core/node_modules/expand-brackets/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/express": { - "version": "4.17.1", + "node_modules/unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" }, - "engines": { - "node": ">= 0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/express/node_modules/debug": { - "version": "2.6.9", + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">= 4.0.0" } }, - "node_modules/ganache-core/node_modules/express/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/express/node_modules/qs": { - "version": "6.7.0", + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true, - "license": "BSD-3-Clause", - "optional": true, "engines": { - "node": ">=0.6" + "node": ">= 0.8" } }, - "node_modules/ganache-core/node_modules/express/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "optional": true + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } }, - "node_modules/ganache-core/node_modules/ext": { - "version": "1.4.0", + "node_modules/url": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.1.tgz", + "integrity": "sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==", "dev": true, - "license": "ISC", "dependencies": { - "type": "^2.0.0" + "punycode": "^1.4.1", + "qs": "^6.11.0" } }, - "node_modules/ganache-core/node_modules/ext/node_modules/type": { - "version": "2.1.0", - "dev": true, - "license": "ISC" + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true }, - "node_modules/ganache-core/node_modules/extend": { - "version": "3.0.2", - "dev": true, - "license": "MIT" + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" }, - "node_modules/ganache-core/node_modules/extend-shallow": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" } }, - "node_modules/ganache-core/node_modules/extglob": { - "version": "2.0.4", + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" } }, - "node_modules/ganache-core/node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" } }, - "node_modules/ganache-core/node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/web3-utils": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.0.tgz", + "integrity": "sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "is-extendable": "^0.1.0" + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereumjs-util": "^7.1.0", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/extglob/node_modules/is-extendable": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": ">=8.0.0" } }, - "node_modules/ganache-core/node_modules/extsprintf": { - "version": "1.3.0", + "node_modules/web3-utils/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" + "peer": true }, - "node_modules/ganache-core/node_modules/fake-merkle-patricia-tree": { - "version": "1.0.1", + "node_modules/web3-utils/node_modules/ethereumjs-util": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz", + "integrity": "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==", "dev": true, - "license": "ISC", + "peer": true, "dependencies": { - "checkpoint-store": "^1.1.0" + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + }, + "engines": { + "node": ">=10.0.0" } }, - "node_modules/ganache-core/node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/fetch-ponyfill": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "node-fetch": "~1.7.1" - } + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true }, - "node_modules/ganache-core/node_modules/fetch-ponyfill/node_modules/is-stream": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/whatwg-fetch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" }, - "node_modules/ganache-core/node_modules/fetch-ponyfill/node_modules/node-fetch": { - "version": "1.7.3", + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "dev": true, - "license": "MIT", "dependencies": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, - "node_modules/ganache-core/node_modules/finalhandler": { - "version": "1.1.2", + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" + "isexe": "^2.0.0" }, - "engines": { - "node": ">= 0.8" + "bin": { + "which": "bin/which" } }, - "node_modules/ganache-core/node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "ms": "2.0.0" + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ganache-core/node_modules/finalhandler/node_modules/ms": { + "node_modules/which-module": { "version": "2.0.0", - "dev": true, - "license": "MIT", - "optional": true + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root": { - "version": "1.2.1", + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "fs-extra": "^4.0.3", - "micromatch": "^3.1.4" + "string-width": "^1.0.2 || 2" } }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/braces": { - "version": "2.3.2", + "node_modules/window-size": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", + "integrity": "sha512-UD7d8HFA2+PZsbKyaOCEy8gMh1oDtHgJh1LfgjQ4zVXmYjAT/kvz3PueITKuqDiIXQe7yzpPnxX3lNc+AhQMyw==", "dev": true, - "license": "MIT", - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "peer": true, + "bin": { + "window-size": "cli.js" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10.0" } }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/fill-range": { - "version": "4.0.0", + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", "dev": true, - "license": "MIT", "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.0.0" } }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/fs-extra": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/is-buffer": { - "version": "1.1.6", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/is-extendable": { - "version": "0.1.1", + "node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "dev": true, - "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/is-number": { - "version": "3.0.0", + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, - "license": "MIT", "dependencies": { - "is-buffer": "^1.1.5" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/micromatch": { - "version": "3.1.10", + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, - "license": "MIT", "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "ansi-regex": "^4.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/find-yarn-workspace-root/node_modules/to-regex-range": { - "version": "2.1.1", + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", "dev": true, - "license": "MIT", "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "mkdirp": "^0.5.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/ganache-core/node_modules/flow-stoplight": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/ganache-core/node_modules/for-each": { - "version": "0.3.3", - "dev": true, - "license": "MIT", + "node_modules/ws": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", + "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", "dependencies": { - "is-callable": "^1.1.3" + "async-limiter": "~1.0.0" } }, - "node_modules/ganache-core/node_modules/for-in": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" } }, - "node_modules/ganache-core/node_modules/forever-agent": { - "version": "0.6.1", - "dev": true, - "license": "Apache-2.0", + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "engines": { - "node": "*" + "node": ">=0.4" } }, - "node_modules/ganache-core/node_modules/form-data": { - "version": "2.3.3", + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, - "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" } }, - "node_modules/ganache-core/node_modules/forwarded": { - "version": "0.1.2", + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } }, - "node_modules/ganache-core/node_modules/fragment-cache": { - "version": "0.2.1", + "node_modules/yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", "dev": true, - "license": "MIT", "dependencies": { - "map-cache": "^0.2.2" + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/fresh": { - "version": "0.5.2", + "node_modules/yargs/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true, - "license": "MIT", - "optional": true, "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/fs-extra": { - "version": "7.0.1", + "node_modules/yargs/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, - "license": "MIT", "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "locate-path": "^3.0.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/ganache-core/node_modules/function-bind": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/functional-red-black-tree": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/get-intrinsic": { - "version": "1.0.2", + "node_modules/yargs/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, - "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/get-stream": { - "version": "5.2.0", + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "pump": "^3.0.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ganache-core/node_modules/get-value": { - "version": "2.0.6", + "node_modules/yargs/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, - "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/getpass": { - "version": "0.1.7", + "node_modules/yargs/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" + "engines": { + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/glob": { - "version": "7.1.3", + "node_modules/yargs/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, - "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" }, "engines": { - "node": "*" + "node": ">=6" } }, - "node_modules/ganache-core/node_modules/global": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "node_modules/ganache-core/node_modules/got": { - "version": "9.6.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/ganache-core/node_modules/got/node_modules/get-stream": { - "version": "4.1.0", + "node_modules/yargs/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "pump": "^3.0.0" + "ansi-regex": "^4.1.0" }, "engines": { "node": ">=6" } - }, - "node_modules/ganache-core/node_modules/graceful-fs": { - "version": "4.2.4", - "dev": true, - "license": "ISC" - }, - "node_modules/ganache-core/node_modules/har-schema": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4" + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "requires": { + "@babel/highlight": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/har-validator": { - "version": "5.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" + "@babel/compat-data": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.8.tgz", + "integrity": "sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q==" + }, + "@babel/core": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.7.tgz", + "integrity": "sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==", + "peer": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.16.7", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helpers": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" } }, - "node_modules/ganache-core/node_modules/has": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" + "@babel/generator": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz", + "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==", + "requires": { + "@babel/types": "^7.16.8", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" } }, - "node_modules/ganache-core/node_modules/has-ansi": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "@babel/helper-compilation-targets": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", + "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", + "requires": { + "@babel/compat-data": "^7.16.4", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.17.5", + "semver": "^6.3.0" } }, - "node_modules/ganache-core/node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "@babel/helper-define-polyfill-provider": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz", + "integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==", + "requires": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" } }, - "node_modules/ganache-core/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" + "@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", + "requires": { + "@babel/types": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/has-symbol-support-x": { - "version": "1.4.2", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": "*" + "@babel/helper-function-name": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", + "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", + "requires": { + "@babel/helper-get-function-arity": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/types": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/has-symbols": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@babel/helper-get-function-arity": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", + "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", + "requires": { + "@babel/types": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/has-to-string-tag-x": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "has-symbol-support-x": "^1.4.1" - }, - "engines": { - "node": "*" + "@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "requires": { + "@babel/types": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/has-value": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "requires": { + "@babel/types": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/has-values": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" + "@babel/helper-module-transforms": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", + "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", + "peer": true, + "requires": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/has-values/node_modules/is-buffer": { - "version": "1.1.6", - "dev": true, - "license": "MIT" + "@babel/helper-plugin-utils": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==" }, - "node_modules/ganache-core/node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "@babel/helper-simple-access": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", + "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", + "peer": true, + "requires": { + "@babel/types": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "requires": { + "@babel/types": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==" }, - "node_modules/ganache-core/node_modules/hash-base": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } + "@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==" }, - "node_modules/ganache-core/node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "@babel/helpers": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.7.tgz", + "integrity": "sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==", + "peer": true, + "requires": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/hash.js": { - "version": "1.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" + "@babel/highlight": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz", + "integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==", + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" } }, - "node_modules/ganache-core/node_modules/heap": { - "version": "0.2.6", - "dev": true + "@babel/parser": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", + "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==" }, - "node_modules/ganache-core/node_modules/hmac-drbg": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" + "@babel/plugin-transform-runtime": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.8.tgz", + "integrity": "sha512-6Kg2XHPFnIarNweZxmzbgYnnWsXxkx9WQUVk2sksBRL80lBC1RAQV3wQagWxdCHiYHqPN+oenwNIuttlYgIbQQ==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "semver": "^6.3.0" } }, - "node_modules/ganache-core/node_modules/home-or-tmp": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "@babel/runtime": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", + "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", + "requires": { + "regenerator-runtime": "^0.13.4" } }, - "node_modules/ganache-core/node_modules/http-cache-semantics": { - "version": "4.1.0", - "dev": true, - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/ganache-core/node_modules/http-errors": { - "version": "1.7.2", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" + "@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" } }, - "node_modules/ganache-core/node_modules/http-errors/node_modules/inherits": { - "version": "2.0.3", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/ganache-core/node_modules/http-https": { - "version": "1.0.0", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/ganache-core/node_modules/http-signature": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" + "@babel/traverse": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.8.tgz", + "integrity": "sha512-xe+H7JlvKsDQwXRsBhSnq1/+9c+LlQcCK3Tn/l5sbx02HYns/cn7ibp9+RV1sIUqu7hKg91NWsgHurO9dowITQ==", + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.16.8", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.16.8", + "@babel/types": "^7.16.8", + "debug": "^4.1.0", + "globals": "^11.1.0" } }, - "node_modules/ganache-core/node_modules/iconv-lite": { - "version": "0.4.24", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" + "@babel/types": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz", + "integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==", + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" } }, - "node_modules/ganache-core/node_modules/idna-uts46-hx": { - "version": "2.3.1", + "@ensdomains/ens": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@ensdomains/ens/-/ens-0.4.5.tgz", + "integrity": "sha512-JSvpj1iNMFjK6K+uVl4unqMoa9rf5jopb8cya5UGBWz23Nw8hSNT7efgUx4BTlAPAgpNlEioUfeTyQ6J9ZvTVw==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "punycode": "2.1.0" + "peer": true, + "requires": { + "bluebird": "^3.5.2", + "eth-ens-namehash": "^2.0.8", + "solc": "^0.4.20", + "testrpc": "0.0.1", + "web3-utils": "^1.0.0-beta.31" }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/ganache-core/node_modules/idna-uts46-hx/node_modules/punycode": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ganache-core/node_modules/ieee754": { - "version": "1.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "peer": true }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "dev": true, + "peer": true }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ganache-core/node_modules/immediate": { - "version": "3.2.3", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/ganache-core/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "node_modules/ganache-core/node_modules/invariant": { - "version": "2.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/ganache-core/node_modules/ipaddr.js": { - "version": "1.9.1", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/ganache-core/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/is-arguments": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ganache-core/node_modules/is-callable": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ganache-core/node_modules/is-ci": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^2.0.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/ganache-core/node_modules/is-data-descriptor": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/is-date-object": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", + "dev": true, + "peer": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "fs-extra": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "integrity": "sha512-UvSPKyhMn6LEd/WpUaV9C9t3zATuqoqfWc3QdPhPLb58prN9tqYPlPWi8Krxi44loBoUzlobqZ3+8tGpxxSzwA==", + "dev": true, + "peer": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0", + "path-is-absolute": "^1.0.0", + "rimraf": "^2.2.8" + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true, + "peer": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dev": true, + "peer": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", + "dev": true, + "peer": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha512-H7AkJWMobeskkttHyhTVtS0fxpFLjxhbfMa6Bk3wimP7sdPRGL3EyCg3sAQenFfAe+xQ+oAc85Nmtvq0ROM83Q==", + "dev": true, + "peer": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "dev": true, + "peer": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "peer": true + }, + "solc": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.4.26.tgz", + "integrity": "sha512-o+c6FpkiHd+HPjmjEVpQgH7fqZ14tJpXhho+/bQXlXbliLIS/xjXb42Vxh+qQY1WCSTMQ0+a5vR9vi0MfhU6mA==", + "dev": true, + "peer": true, + "requires": { + "fs-extra": "^0.30.0", + "memorystream": "^0.3.1", + "require-from-string": "^1.1.0", + "semver": "^5.3.0", + "yargs": "^4.7.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dev": true, + "peer": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "peer": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", + "dev": true, + "peer": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "dev": true, + "peer": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "y18n": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "dev": true, + "peer": true + }, + "yargs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", + "integrity": "sha512-LqodLrnIDM3IFT+Hf/5sxBnEGECrfdC1uIbgZeJmESCSo4HoCAaKEus8MylXHAkdacGc0ye+Qa+dpkuom8uVYA==", + "dev": true, + "peer": true, + "requires": { + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "lodash.assign": "^4.0.3", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.1", + "which-module": "^1.0.0", + "window-size": "^0.2.0", + "y18n": "^3.2.1", + "yargs-parser": "^2.4.1" + } + }, + "yargs-parser": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", + "integrity": "sha512-9pIKIJhnI5tonzG6OnCFlz/yln8xHYcGl+pn3xR0Vzff0vzN1PbNRaelgfgRUwZ3s4i3jvxT9WhmUGL4whnasA==", + "dev": true, + "peer": true, + "requires": { + "camelcase": "^3.0.0", + "lodash.assign": "^4.0.6" + } + } } }, - "node_modules/ganache-core/node_modules/is-descriptor": { - "version": "1.0.2", + "@ensdomains/resolver": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@ensdomains/resolver/-/resolver-0.2.4.tgz", + "integrity": "sha512-bvaTH34PMCbv6anRa9I/0zjLJgY4EuznbEMgbV77JBCQ9KNC46rzi0avuxpOfu+xDjPEtSFGqVEOr5GlUSGudA==", "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } + "peer": true }, - "node_modules/ganache-core/node_modules/is-extendable": { - "version": "1.0.1", + "@ethereum-waffle/chai": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@ethereum-waffle/chai/-/chai-4.0.10.tgz", + "integrity": "sha512-X5RepE7Dn8KQLFO7HHAAe+KeGaX/by14hn90wePGBhzL54tq4Y8JscZFu+/LCwCl6TnkAAy5ebiMoqJ37sFtWw==", "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "@ethereum-waffle/provider": "4.0.5", + "debug": "^4.3.4", + "json-bigint": "^1.0.0" } }, - "node_modules/ganache-core/node_modules/is-finite": { - "version": "1.1.0", + "@ethereum-waffle/compiler": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@ethereum-waffle/compiler/-/compiler-4.0.3.tgz", + "integrity": "sha512-5x5U52tSvEVJS6dpCeXXKvRKyf8GICDwiTwUvGD3/WD+DpvgvaoHOL82XqpTSUHgV3bBq6ma5/8gKUJUIAnJCw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "@resolver-engine/imports": "^0.3.3", + "@resolver-engine/imports-fs": "^0.3.3", + "@typechain/ethers-v5": "^10.0.0", + "@types/mkdirp": "^0.5.2", + "@types/node-fetch": "^2.6.1", + "mkdirp": "^0.5.1", + "node-fetch": "^2.6.7" } }, - "node_modules/ganache-core/node_modules/is-fn": { - "version": "1.0.0", + "@ethereum-waffle/ens": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@ethereum-waffle/ens/-/ens-4.0.3.tgz", + "integrity": "sha512-PVLcdnTbaTfCrfSOrvtlA9Fih73EeDvFS28JQnT5M5P4JMplqmchhcZB1yg/fCtx4cvgHlZXa0+rOCAk2Jk0Jw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "requires": {} }, - "node_modules/ganache-core/node_modules/is-function": { - "version": "1.0.2", + "@ethereum-waffle/mock-contract": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@ethereum-waffle/mock-contract/-/mock-contract-4.0.4.tgz", + "integrity": "sha512-LwEj5SIuEe9/gnrXgtqIkWbk2g15imM/qcJcxpLyAkOj981tQxXmtV4XmQMZsdedEsZ/D/rbUAOtZbgwqgUwQA==", "dev": true, - "license": "MIT" + "requires": {} }, - "node_modules/ganache-core/node_modules/is-hex-prefixed": { - "version": "1.0.0", + "@ethereum-waffle/provider": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@ethereum-waffle/provider/-/provider-4.0.5.tgz", + "integrity": "sha512-40uzfyzcrPh+Gbdzv89JJTMBlZwzya1YLDyim8mVbEqYLP5VRYWoGp0JMyaizgV3hMoUFRqJKVmIUw4v7r3hYw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.5.0", - "npm": ">=3" + "requires": { + "@ethereum-waffle/ens": "4.0.3", + "@ganache/ethereum-options": "0.1.4", + "debug": "^4.3.4", + "ganache": "7.4.3" } }, - "node_modules/ganache-core/node_modules/is-negative-zero": { - "version": "2.0.1", + "@ethereumjs/block": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/block/-/block-3.6.0.tgz", + "integrity": "sha512-dqLo1LtsLG+Oelu5S5tWUDG0pah3QUwV5TJZy2cm19BXDr4ka/S9XBSgao0i09gTcuPlovlHgcs6d7EZ37urjQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "requires": { + "@ethereumjs/common": "^2.6.0", + "@ethereumjs/tx": "^3.4.0", + "ethereumjs-util": "^7.1.3", + "merkle-patricia-tree": "^4.2.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ganache-core/node_modules/is-object": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ganache-core/node_modules/is-plain-obj": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/is-plain-object": { - "version": "2.0.4", - "dev": true, - "license": "MIT", "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true + }, + "ethereumjs-util": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", + "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + } + }, + "level-ws": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/level-ws/-/level-ws-2.0.0.tgz", + "integrity": "sha512-1iv7VXx0G9ec1isqQZ7y5LmoZo/ewAsyDHNA8EFDW5hqH2Kqovm33nSFkSdnLLAK+I5FlT+lo5Cw9itGe+CpQA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^3.1.0", + "xtend": "^4.0.1" + } + }, + "merkle-patricia-tree": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-4.2.2.tgz", + "integrity": "sha512-eqZYNTshcYx9aESkSPr71EqwsR/QmpnObDEV4iLxkt/x/IoLYZYjJvKY72voP/27Vy61iMOrfOG6jrn7ttXD+Q==", + "dev": true, + "requires": { + "@types/levelup": "^4.3.0", + "ethereumjs-util": "^7.1.2", + "level-mem": "^5.0.1", + "level-ws": "^2.0.0", + "readable-stream": "^3.6.0", + "rlp": "^2.2.4", + "semaphore-async-await": "^1.5.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, - "node_modules/ganache-core/node_modules/is-regex": { - "version": "1.1.1", + "@ethereumjs/blockchain": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/blockchain/-/blockchain-5.5.1.tgz", + "integrity": "sha512-JS2jeKxl3tlaa5oXrZ8mGoVBCz6YqsGG350XVNtHAtNZXKk7pU3rH4xzF2ru42fksMMqzFLzKh9l4EQzmNWDqA==", "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" + "requires": { + "@ethereumjs/block": "^3.6.0", + "@ethereumjs/common": "^2.6.0", + "@ethereumjs/ethash": "^1.1.0", + "debug": "^2.2.0", + "ethereumjs-util": "^7.1.3", + "level-mem": "^5.0.1", + "lru-cache": "^5.1.1", + "semaphore-async-await": "^1.5.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ethereumjs-util": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", + "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, - "node_modules/ganache-core/node_modules/is-retry-allowed": { - "version": "1.2.0", + "@ethereumjs/common": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.6.0.tgz", + "integrity": "sha512-Cq2qS0FTu6O2VU1sgg+WyU9Ps0M6j/BEMHN+hRaECXCV/r0aI78u4N6p52QW/BDVhwWZpCdrvG8X7NJdzlpNUA==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "crc-32": "^1.2.0", + "ethereumjs-util": "^7.1.3" + }, + "dependencies": { + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true + }, + "ethereumjs-util": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", + "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + } + } } }, - "node_modules/ganache-core/node_modules/is-symbol": { - "version": "1.0.3", + "@ethereumjs/ethash": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/ethash/-/ethash-1.1.0.tgz", + "integrity": "sha512-/U7UOKW6BzpA+Vt+kISAoeDie1vAvY4Zy2KF5JJb+So7+1yKmJeJEHOGSnQIj330e9Zyl3L5Nae6VZyh2TJnAA==", "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" + "requires": { + "@ethereumjs/block": "^3.5.0", + "@types/levelup": "^4.3.0", + "buffer-xor": "^2.0.1", + "ethereumjs-util": "^7.1.1", + "miller-rabin": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true + }, + "buffer-xor": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-2.0.2.tgz", + "integrity": "sha512-eHslX0bin3GB+Lx2p7lEYRShRewuNZL3fUl4qlVJGGiwoPGftmt8JQgk2Y9Ji5/01TnVDo33E5b5O3vUB1HdqQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.1" + } + }, + "ethereumjs-util": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", + "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + } + } } }, - "node_modules/ganache-core/node_modules/is-typedarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/is-windows": { - "version": "1.0.2", + "@ethereumjs/tx": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.4.0.tgz", + "integrity": "sha512-WWUwg1PdjHKZZxPPo274ZuPsJCWV3SqATrEKQP1n2DrVYVP1aZIYpo/mFaA0BDoE0tIQmBeimRCEA0Lgil+yYw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "requires": { + "@ethereumjs/common": "^2.6.0", + "ethereumjs-util": "^7.1.3" + }, + "dependencies": { + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true + }, + "ethereumjs-util": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", + "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + } + } } }, - "node_modules/ganache-core/node_modules/isarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/ganache-core/node_modules/isobject": { - "version": "3.0.1", + "@ethereumjs/vm": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/vm/-/vm-5.6.0.tgz", + "integrity": "sha512-J2m/OgjjiGdWF2P9bj/4LnZQ1zRoZhY8mRNVw/N3tXliGI8ai1sI1mlDPkLpeUUM4vq54gH6n0ZlSpz8U/qlYQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/isstream": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/isurl": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "has-to-string-tag-x": "^1.2.0", - "is-object": "^1.0.1" + "requires": { + "@ethereumjs/block": "^3.6.0", + "@ethereumjs/blockchain": "^5.5.0", + "@ethereumjs/common": "^2.6.0", + "@ethereumjs/tx": "^3.4.0", + "async-eventemitter": "^0.2.4", + "core-js-pure": "^3.0.1", + "debug": "^2.2.0", + "ethereumjs-util": "^7.1.3", + "functional-red-black-tree": "^1.0.1", + "mcl-wasm": "^0.7.1", + "merkle-patricia-tree": "^4.2.2", + "rustbn.js": "~0.2.0" }, - "engines": { - "node": ">= 4" + "dependencies": { + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ethereumjs-util": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", + "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + } + }, + "level-ws": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/level-ws/-/level-ws-2.0.0.tgz", + "integrity": "sha512-1iv7VXx0G9ec1isqQZ7y5LmoZo/ewAsyDHNA8EFDW5hqH2Kqovm33nSFkSdnLLAK+I5FlT+lo5Cw9itGe+CpQA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^3.1.0", + "xtend": "^4.0.1" + } + }, + "merkle-patricia-tree": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-4.2.2.tgz", + "integrity": "sha512-eqZYNTshcYx9aESkSPr71EqwsR/QmpnObDEV4iLxkt/x/IoLYZYjJvKY72voP/27Vy61iMOrfOG6jrn7ttXD+Q==", + "dev": true, + "requires": { + "@types/levelup": "^4.3.0", + "ethereumjs-util": "^7.1.2", + "level-mem": "^5.0.1", + "level-ws": "^2.0.0", + "readable-stream": "^3.6.0", + "rlp": "^2.2.4", + "semaphore-async-await": "^1.5.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, - "node_modules/ganache-core/node_modules/js-sha3": { - "version": "0.5.7", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/js-tokens": { - "version": "4.0.0", + "@ethersproject/abi": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.5.0.tgz", + "integrity": "sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w==", "dev": true, - "license": "MIT" + "requires": { + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } }, - "node_modules/ganache-core/node_modules/jsbn": { - "version": "0.1.1", + "@ethersproject/abstract-provider": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz", + "integrity": "sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg==", "dev": true, - "license": "MIT" + "requires": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/networks": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/web": "^5.5.0" + } }, - "node_modules/ganache-core/node_modules/json-buffer": { - "version": "3.0.0", + "@ethersproject/abstract-signer": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz", + "integrity": "sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA==", "dev": true, - "license": "MIT", - "optional": true + "requires": { + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0" + } }, - "node_modules/ganache-core/node_modules/json-rpc-engine": { - "version": "3.8.0", + "@ethersproject/address": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.5.0.tgz", + "integrity": "sha512-l4Nj0eWlTUh6ro5IbPTgbpT4wRbdH5l8CQf7icF7sb/SI3Nhd9Y9HzhonTSTi6CefI0necIw7LJqQPopPLZyWw==", "dev": true, - "license": "ISC", - "dependencies": { - "async": "^2.0.1", - "babel-preset-env": "^1.7.0", - "babelify": "^7.3.0", - "json-rpc-error": "^2.0.0", - "promise-to-callback": "^1.0.0", - "safe-event-emitter": "^1.0.1" + "requires": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/rlp": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/json-rpc-error": { - "version": "2.0.0", + "@ethersproject/base64": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.5.0.tgz", + "integrity": "sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA==", "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1" + "requires": { + "@ethersproject/bytes": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/json-rpc-random-id": { - "version": "1.0.1", + "@ethersproject/basex": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.5.0.tgz", + "integrity": "sha512-ZIodwhHpVJ0Y3hUCfUucmxKsWQA5TMnavp5j/UOuDdzZWzJlRmuOjcTMIGgHCYuZmHt36BfiSyQPSRskPxbfaQ==", "dev": true, - "license": "ISC" - }, - "node_modules/ganache-core/node_modules/json-schema": { - "version": "0.2.3", - "dev": true + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/properties": "^5.5.0" + } }, - "node_modules/ganache-core/node_modules/json-schema-traverse": { - "version": "0.4.1", + "@ethersproject/bignumber": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.5.0.tgz", + "integrity": "sha512-6Xytlwvy6Rn3U3gKEc1vP7nR92frHkv6wtVr95LFR3jREXiCPzdWxKQ1cx4JGQBXxcguAwjA8murlYN2TSiEbg==", "dev": true, - "license": "MIT" + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "bn.js": "^4.11.9" + } }, - "node_modules/ganache-core/node_modules/json-stable-stringify": { - "version": "1.0.1", + "@ethersproject/bytes": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.5.0.tgz", + "integrity": "sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog==", "dev": true, - "license": "MIT", - "dependencies": { - "jsonify": "~0.0.0" + "requires": { + "@ethersproject/logger": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/json-stringify-safe": { - "version": "5.0.1", + "@ethersproject/constants": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.5.0.tgz", + "integrity": "sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ==", "dev": true, - "license": "ISC" + "requires": { + "@ethersproject/bignumber": "^5.5.0" + } }, - "node_modules/ganache-core/node_modules/jsonfile": { - "version": "4.0.0", + "@ethersproject/contracts": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.5.0.tgz", + "integrity": "sha512-2viY7NzyvJkh+Ug17v7g3/IJC8HqZBDcOjYARZLdzRxrfGlRgmYgl6xPRKVbEzy1dWKw/iv7chDcS83pg6cLxg==", "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "requires": { + "@ethersproject/abi": "^5.5.0", + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/transactions": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/jsonify": { - "version": "0.0.0", + "@ethersproject/hash": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.5.0.tgz", + "integrity": "sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg==", "dev": true, - "license": "Public Domain" + "requires": { + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } }, - "node_modules/ganache-core/node_modules/jsprim": { - "version": "1.4.1", + "@ethersproject/hdnode": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.5.0.tgz", + "integrity": "sha512-mcSOo9zeUg1L0CoJH7zmxwUG5ggQHU1UrRf8jyTYy6HxdZV+r0PBoL1bxr+JHIPXRzS6u/UW4mEn43y0tmyF8Q==", "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" + "requires": { + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/basex": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/pbkdf2": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/sha2": "^5.5.0", + "@ethersproject/signing-key": "^5.5.0", + "@ethersproject/strings": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/wordlists": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/keccak": { - "version": "3.0.1", + "@ethersproject/json-wallets": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz", + "integrity": "sha512-9lA21XQnCdcS72xlBn1jfQdj2A1VUxZzOzi9UkNdnokNKke/9Ya2xA9aIK1SC3PQyBDLt4C+dfps7ULpkvKikQ==", "dev": true, - "hasInstallScript": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" + "requires": { + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/hdnode": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/pbkdf2": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/random": "^5.5.0", + "@ethersproject/strings": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/ganache-core/node_modules/keyv": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "json-buffer": "3.0.0" + "aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0=", + "dev": true + } } }, - "node_modules/ganache-core/node_modules/kind-of": { - "version": "6.0.3", + "@ethersproject/keccak256": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.5.0.tgz", + "integrity": "sha512-5VoFCTjo2rYbBe1l2f4mccaRFN/4VQEYFwwn04aJV2h7qf4ZvI2wFxUE1XOX+snbwCLRzIeikOqtAoPwMza9kg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "requires": { + "@ethersproject/bytes": "^5.5.0", + "js-sha3": "0.8.0" } }, - "node_modules/ganache-core/node_modules/klaw-sync": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11" - } + "@ethersproject/logger": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.5.0.tgz", + "integrity": "sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg==", + "dev": true }, - "node_modules/ganache-core/node_modules/level-codec": { - "version": "9.0.2", + "@ethersproject/networks": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.5.2.tgz", + "integrity": "sha512-NEqPxbGBfy6O3x4ZTISb90SjEDkWYDUbEeIFhJly0F7sZjoQMnj5KYzMSkMkLKZ+1fGpx00EDpHQCy6PrDupkQ==", "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.6.0" - }, - "engines": { - "node": ">=6" + "requires": { + "@ethersproject/logger": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/level-errors": { - "version": "2.0.1", + "@ethersproject/pbkdf2": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.5.0.tgz", + "integrity": "sha512-SaDvQFvXPnz1QGpzr6/HToLifftSXGoXrbpZ6BvoZhmx4bNLHrxDe8MZisuecyOziP1aVEwzC2Hasj+86TgWVg==", "dev": true, - "license": "MIT", - "dependencies": { - "errno": "~0.1.1" - }, - "engines": { - "node": ">=6" + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/sha2": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/level-iterator-stream": { - "version": "2.0.3", + "@ethersproject/properties": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.5.0.tgz", + "integrity": "sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA==", "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.5", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=4" + "requires": { + "@ethersproject/logger": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/level-mem": { - "version": "3.0.1", + "@ethersproject/providers": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.5.2.tgz", + "integrity": "sha512-hkbx7x/MKcRjyrO4StKXCzCpWer6s97xnm34xkfPiarhtEUVAN4TBBpamM+z66WcTt7H5B53YwbRj1n7i8pZoQ==", "dev": true, - "license": "MIT", - "dependencies": { - "level-packager": "~4.0.0", - "memdown": "~3.0.0" + "requires": { + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/basex": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/networks": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/random": "^5.5.0", + "@ethersproject/rlp": "^5.5.0", + "@ethersproject/sha2": "^5.5.0", + "@ethersproject/strings": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/web": "^5.5.0", + "bech32": "1.1.4", + "ws": "7.4.6" }, - "engines": { - "node": ">=6" - } - }, - "node_modules/ganache-core/node_modules/level-mem/node_modules/abstract-leveldown": { - "version": "5.0.0", - "dev": true, - "license": "MIT", "dependencies": { - "xtend": "~4.0.0" - }, - "engines": { - "node": ">=6" + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "dev": true, + "requires": {} + } } }, - "node_modules/ganache-core/node_modules/level-mem/node_modules/ltgt": { - "version": "2.2.1", + "@ethersproject/random": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.5.1.tgz", + "integrity": "sha512-YaU2dQ7DuhL5Au7KbcQLHxcRHfgyNgvFV4sQOo0HrtW3Zkrc9ctWNz8wXQ4uCSfSDsqX2vcjhroxU5RQRV0nqA==", "dev": true, - "license": "MIT" + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0" + } }, - "node_modules/ganache-core/node_modules/level-mem/node_modules/memdown": { - "version": "3.0.0", + "@ethersproject/rlp": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.5.0.tgz", + "integrity": "sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA==", "dev": true, - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~5.0.0", - "functional-red-black-tree": "~1.0.1", - "immediate": "~3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" - }, - "engines": { - "node": ">=6" + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/level-mem/node_modules/safe-buffer": { - "version": "5.1.2", + "@ethersproject/sha2": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.5.0.tgz", + "integrity": "sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA==", "dev": true, - "license": "MIT" + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "hash.js": "1.1.7" + } }, - "node_modules/ganache-core/node_modules/level-packager": { - "version": "4.0.1", + "@ethersproject/signing-key": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.5.0.tgz", + "integrity": "sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng==", "dev": true, - "license": "MIT", - "dependencies": { - "encoding-down": "~5.0.0", - "levelup": "^3.0.0" - }, - "engines": { - "node": ">=6" + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "bn.js": "^4.11.9", + "elliptic": "6.5.4", + "hash.js": "1.1.7" } }, - "node_modules/ganache-core/node_modules/level-post": { - "version": "1.0.7", + "@ethersproject/solidity": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.5.0.tgz", + "integrity": "sha512-9NgZs9LhGMj6aCtHXhtmFQ4AN4sth5HuFXVvAQtzmm0jpSCNOTGtrHZJAeYTh7MBjRR8brylWZxBZR9zDStXbw==", "dev": true, - "license": "MIT", - "dependencies": { - "ltgt": "^2.1.2" + "requires": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/sha2": "^5.5.0", + "@ethersproject/strings": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/level-sublevel": { - "version": "6.6.4", + "@ethersproject/strings": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.5.0.tgz", + "integrity": "sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ==", "dev": true, - "license": "MIT", - "dependencies": { - "bytewise": "~1.1.0", - "level-codec": "^9.0.0", - "level-errors": "^2.0.0", - "level-iterator-stream": "^2.0.3", - "ltgt": "~2.1.1", - "pull-defer": "^0.2.2", - "pull-level": "^2.0.3", - "pull-stream": "^3.6.8", - "typewiselite": "~1.0.0", - "xtend": "~4.0.0" + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/logger": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/level-ws": { - "version": "1.0.0", + "@ethersproject/transactions": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.5.0.tgz", + "integrity": "sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA==", "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "readable-stream": "^2.2.8", - "xtend": "^4.0.1" - }, - "engines": { - "node": ">=6" + "requires": { + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/rlp": "^5.5.0", + "@ethersproject/signing-key": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/levelup": { - "version": "3.1.1", + "@ethersproject/units": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.5.0.tgz", + "integrity": "sha512-7+DpjiZk4v6wrikj+TCyWWa9dXLNU73tSTa7n0TSJDxkYbV3Yf1eRh9ToMLlZtuctNYu9RDNNy2USq3AdqSbag==", "dev": true, - "license": "MIT", - "dependencies": { - "deferred-leveldown": "~4.0.0", - "level-errors": "~2.0.0", - "level-iterator-stream": "~3.0.0", - "xtend": "~4.0.0" - }, - "engines": { - "node": ">=6" + "requires": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/logger": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/levelup/node_modules/level-iterator-stream": { - "version": "3.0.1", + "@ethersproject/wallet": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.5.0.tgz", + "integrity": "sha512-Mlu13hIctSYaZmUOo7r2PhNSd8eaMPVXe1wxrz4w4FCE4tDYBywDH+bAR1Xz2ADyXGwqYMwstzTrtUVIsKDO0Q==", "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=6" + "requires": { + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/hdnode": "^5.5.0", + "@ethersproject/json-wallets": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/random": "^5.5.0", + "@ethersproject/signing-key": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/wordlists": "^5.5.0" } }, - "node_modules/ganache-core/node_modules/lodash": { - "version": "4.17.20", + "@ethersproject/web": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.5.1.tgz", + "integrity": "sha512-olvLvc1CB12sREc1ROPSHTdFCdvMh0J5GSJYiQg2D0hdD4QmJDy8QYDb1CvoqD/bF1c++aeKv2sR5uduuG9dQg==", "dev": true, - "license": "MIT" + "requires": { + "@ethersproject/base64": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } }, - "node_modules/ganache-core/node_modules/looper": { - "version": "2.0.0", + "@ethersproject/wordlists": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.5.0.tgz", + "integrity": "sha512-bL0UTReWDiaQJJYOC9sh/XcRu/9i2jMrzf8VLRmPKx58ckSlOJiohODkECCO50dtLZHcGU6MLXQ4OOrgBwP77Q==", "dev": true, - "license": "MIT" + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } }, - "node_modules/ganache-core/node_modules/loose-envify": { - "version": "1.4.0", + "@ganache/ethereum-address": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/ethereum-address/-/ethereum-address-0.1.4.tgz", + "integrity": "sha512-sTkU0M9z2nZUzDeHRzzGlW724xhMLXo2LeX1hixbnjHWY1Zg1hkqORywVfl+g5uOO8ht8T0v+34IxNxAhmWlbw==", "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" + "requires": { + "@ganache/utils": "0.1.4" } }, - "node_modules/ganache-core/node_modules/lowercase-keys": { - "version": "1.0.1", + "@ganache/ethereum-options": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/ethereum-options/-/ethereum-options-0.1.4.tgz", + "integrity": "sha512-i4l46taoK2yC41FPkcoDlEVoqHS52wcbHPqJtYETRWqpOaoj9hAg/EJIHLb1t6Nhva2CdTO84bG+qlzlTxjAHw==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "@ganache/ethereum-address": "0.1.4", + "@ganache/ethereum-utils": "0.1.4", + "@ganache/options": "0.1.4", + "@ganache/utils": "0.1.4", + "bip39": "3.0.4", + "seedrandom": "3.0.5" } }, - "node_modules/ganache-core/node_modules/lru-cache": { - "version": "5.1.1", + "@ganache/ethereum-utils": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/ethereum-utils/-/ethereum-utils-0.1.4.tgz", + "integrity": "sha512-FKXF3zcdDrIoCqovJmHLKZLrJ43234Em2sde/3urUT/10gSgnwlpFmrv2LUMAmSbX3lgZhW/aSs8krGhDevDAg==", "dev": true, - "license": "ISC", + "requires": { + "@ethereumjs/common": "2.6.0", + "@ethereumjs/tx": "3.4.0", + "@ethereumjs/vm": "5.6.0", + "@ganache/ethereum-address": "0.1.4", + "@ganache/rlp": "0.1.4", + "@ganache/utils": "0.1.4", + "emittery": "0.10.0", + "ethereumjs-abi": "0.6.8", + "ethereumjs-util": "7.1.3" + }, "dependencies": { - "yallist": "^3.0.2" + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, + "ethereumjs-util": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", + "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + } + } } }, - "node_modules/ganache-core/node_modules/ltgt": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/map-cache": { - "version": "0.2.2", + "@ganache/options": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/options/-/options-0.1.4.tgz", + "integrity": "sha512-zAe/craqNuPz512XQY33MOAG6Si1Xp0hCvfzkBfj2qkuPcbJCq6W/eQ5MB6SbXHrICsHrZOaelyqjuhSEmjXRw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "requires": { + "@ganache/utils": "0.1.4", + "bip39": "3.0.4", + "seedrandom": "3.0.5" } }, - "node_modules/ganache-core/node_modules/map-visit": { - "version": "1.0.0", + "@ganache/rlp": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/rlp/-/rlp-0.1.4.tgz", + "integrity": "sha512-Do3D1H6JmhikB+6rHviGqkrNywou/liVeFiKIpOBLynIpvZhRCgn3SEDxyy/JovcaozTo/BynHumfs5R085MFQ==", "dev": true, - "license": "MIT", - "dependencies": { - "object-visit": "^1.0.0" + "requires": { + "@ganache/utils": "0.1.4", + "rlp": "2.2.6" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "rlp": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.6.tgz", + "integrity": "sha512-HAfAmL6SDYNWPUOJNrM500x4Thn4PZsEy5pijPh40U9WfNk0z15hUYzO9xVIMAdIHdFtD8CBDHd75Td1g36Mjg==", + "dev": true, + "requires": { + "bn.js": "^4.11.1" + } + } } }, - "node_modules/ganache-core/node_modules/md5.js": { - "version": "1.3.5", + "@ganache/utils": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@ganache/utils/-/utils-0.1.4.tgz", + "integrity": "sha512-oatUueU3XuXbUbUlkyxeLLH3LzFZ4y5aSkNbx6tjSIhVTPeh+AuBKYt4eQ73FFcTB3nj/gZoslgAh5CN7O369w==", "dev": true, - "license": "MIT", + "requires": { + "@trufflesuite/bigint-buffer": "1.1.9", + "emittery": "0.10.0", + "keccak": "3.0.1", + "seedrandom": "3.0.5" + }, "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "dev": true, + "requires": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + } + } } }, - "node_modules/ganache-core/node_modules/media-typer": { - "version": "0.3.0", + "@nomiclabs/hardhat-ethers": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.4.tgz", + "integrity": "sha512-7LMR344TkdCYkMVF9LuC9VU2NBIi84akQiwqm7OufpWaDgHbWhuanY53rk3SVAW0E4HBk5xn5wl5+bN5f+Mq5w==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" + "requires": {} + }, + "@nomiclabs/hardhat-etherscan": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-2.1.8.tgz", + "integrity": "sha512-0+rj0SsZotVOcTLyDOxnOc3Gulo8upo0rsw/h+gBPcmtj91YqYJNhdARHoBxOhhE8z+5IUQPx+Dii04lXT14PA==", + "dev": true, + "requires": { + "@ethersproject/abi": "^5.1.2", + "@ethersproject/address": "^5.0.2", + "cbor": "^5.0.2", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "node-fetch": "^2.6.0", + "semver": "^6.3.0" } }, - "node_modules/ganache-core/node_modules/merge-descriptors": { - "version": "1.0.1", + "@nomiclabs/hardhat-waffle": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.6.tgz", + "integrity": "sha512-+Wz0hwmJGSI17B+BhU/qFRZ1l6/xMW82QGXE/Gi+WTmwgJrQefuBs1lIf7hzQ1hLk6hpkvb/zwcNkpVKRYTQYg==", "dev": true, - "license": "MIT", - "optional": true + "requires": {} }, - "node_modules/ganache-core/node_modules/merkle-patricia-tree": { - "version": "3.0.0", + "@openzeppelin/contracts": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-3.4.2.tgz", + "integrity": "sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA==", + "dev": true + }, + "@resolver-engine/core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@resolver-engine/core/-/core-0.3.3.tgz", + "integrity": "sha512-eB8nEbKDJJBi5p5SrvrvILn4a0h42bKtbCTri3ZxCGt6UvoQyp7HnGOfki944bUjBSHKK3RvgfViHn+kqdXtnQ==", "dev": true, - "license": "MPL-2.0", + "requires": { + "debug": "^3.1.0", + "is-url": "^1.2.4", + "request": "^2.85.0" + }, "dependencies": { - "async": "^2.6.1", - "ethereumjs-util": "^5.2.0", - "level-mem": "^3.0.1", - "level-ws": "^1.0.0", - "readable-stream": "^3.0.6", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/merkle-patricia-tree/node_modules/ethereumjs-util": { - "version": "5.2.1", + "@resolver-engine/fs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@resolver-engine/fs/-/fs-0.3.3.tgz", + "integrity": "sha512-wQ9RhPUcny02Wm0IuJwYMyAG8fXVeKdmhm8xizNByD4ryZlx6PP6kRen+t/haF43cMfmaV7T3Cx6ChOdHEhFUQ==", "dev": true, - "license": "MPL-2.0", + "requires": { + "@resolver-engine/core": "^0.3.3", + "debug": "^3.1.0" + }, "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/merkle-patricia-tree/node_modules/readable-stream": { - "version": "3.6.0", + "@resolver-engine/imports": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@resolver-engine/imports/-/imports-0.3.3.tgz", + "integrity": "sha512-anHpS4wN4sRMwsAbMXhMfOD/y4a4Oo0Cw/5+rue7hSwGWsDOQaAU1ClK1OxjUC35/peazxEl8JaSRRS+Xb8t3Q==", "dev": true, - "license": "MIT", + "requires": { + "@resolver-engine/core": "^0.3.3", + "debug": "^3.1.0", + "hosted-git-info": "^2.6.0", + "path-browserify": "^1.0.0", + "url": "^0.11.0" + }, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/ganache-core/node_modules/methods": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/miller-rabin": { - "version": "4.0.1", + "@resolver-engine/imports-fs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@resolver-engine/imports-fs/-/imports-fs-0.3.3.tgz", + "integrity": "sha512-7Pjg/ZAZtxpeyCFlZR5zqYkz+Wdo84ugB5LApwriT8XFeQoLwGUj4tZFFvvCuxaNCcqZzCYbonJgmGObYBzyCA==", "dev": true, - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" + "requires": { + "@resolver-engine/fs": "^0.3.3", + "@resolver-engine/imports": "^0.3.3", + "debug": "^3.1.0" }, - "bin": { - "miller-rabin": "bin/miller-rabin" + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/mime": { - "version": "1.6.0", + "@sentry/core": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz", + "integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==", "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" + "requires": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" } }, - "node_modules/ganache-core/node_modules/mime-db": { - "version": "1.45.0", + "@sentry/hub": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz", + "integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" + "requires": { + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" } }, - "node_modules/ganache-core/node_modules/mime-types": { - "version": "2.1.28", + "@sentry/minimal": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz", + "integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==", "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.45.0" - }, - "engines": { - "node": ">= 0.6" + "requires": { + "@sentry/hub": "5.30.0", + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" } }, - "node_modules/ganache-core/node_modules/mimic-response": { - "version": "1.0.1", + "@sentry/node": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.30.0.tgz", + "integrity": "sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" + "requires": { + "@sentry/core": "5.30.0", + "@sentry/hub": "5.30.0", + "@sentry/tracing": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" } }, - "node_modules/ganache-core/node_modules/min-document": { - "version": "2.19.0", + "@sentry/tracing": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz", + "integrity": "sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==", "dev": true, - "dependencies": { - "dom-walk": "^0.1.0" + "requires": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" } }, - "node_modules/ganache-core/node_modules/minimalistic-assert": { - "version": "1.0.1", - "dev": true, - "license": "ISC" - }, - "node_modules/ganache-core/node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "dev": true, - "license": "MIT" + "@sentry/types": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", + "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==", + "dev": true }, - "node_modules/ganache-core/node_modules/minimatch": { - "version": "3.0.4", + "@sentry/utils": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", + "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "requires": { + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" } }, - "node_modules/ganache-core/node_modules/minimist": { - "version": "1.2.5", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/minizlib": { - "version": "1.3.3", + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^2.9.0" + "peer": true, + "requires": { + "type-detect": "4.0.8" } }, - "node_modules/ganache-core/node_modules/minizlib/node_modules/minipass": { - "version": "2.9.0", + "@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "peer": true, + "requires": { + "@sinonjs/commons": "^1.7.0" } }, - "node_modules/ganache-core/node_modules/mixin-deep": { - "version": "1.3.2", + "@solidity-parser/parser": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.0.tgz", + "integrity": "sha512-cX0JJRcmPtNUJpzD2K7FdA7qQsTOk1UZnFx2k7qAg9ZRvuaH5NBe5IEdBMXGlmf2+FmjhqbygJ26H8l2SV7aKQ==", "dev": true, - "license": "MIT", - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "antlr4ts": "^0.5.0-alpha.4" } }, - "node_modules/ganache-core/node_modules/mkdirp": { - "version": "0.5.5", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" + "@truffle/hdwallet-provider": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@truffle/hdwallet-provider/-/hdwallet-provider-1.4.3.tgz", + "integrity": "sha512-Oo8ORAQLfcbLYp6HwG1mpOx6IpVkHv8IkKy25LZUN5Q5bCCqxdlMF0F7CnSXPBdQ+UqZY9+RthC0VrXv9gXiPQ==", + "requires": { + "@trufflesuite/web3-provider-engine": "15.0.13-1", + "ethereum-cryptography": "^0.1.3", + "ethereum-protocol": "^1.0.1", + "ethereumjs-common": "^1.5.0", + "ethereumjs-tx": "^2.1.2", + "ethereumjs-util": "^6.1.0", + "ethereumjs-wallet": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/mkdirp-promise": { - "version": "5.0.1", + "@trufflesuite/bigint-buffer": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@trufflesuite/bigint-buffer/-/bigint-buffer-1.1.9.tgz", + "integrity": "sha512-bdM5cEGCOhDSwminryHJbRmXc1x7dPKg6Pqns3qyTwFlxsqUgxE29lsERS3PlIW1HTjoIGMUqsk1zQQwST1Yxw==", "dev": true, - "license": "ISC", "optional": true, - "dependencies": { - "mkdirp": "*" - }, - "engines": { - "node": ">=4" + "requires": { + "node-gyp-build": "4.3.0" } }, - "node_modules/ganache-core/node_modules/mock-fs": { - "version": "4.13.0", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/multibase": { - "version": "0.6.1", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "base-x": "^3.0.8", - "buffer": "^5.5.0" + "@trufflesuite/eth-json-rpc-filters": { + "version": "4.1.2-1", + "resolved": "https://registry.npmjs.org/@trufflesuite/eth-json-rpc-filters/-/eth-json-rpc-filters-4.1.2-1.tgz", + "integrity": "sha512-/MChvC5dw2ck9NU1cZmdovCz2VKbOeIyR4tcxDvA5sT+NaL0rA2/R5U0yI7zsbo1zD+pgqav77rQHTzpUdDNJQ==", + "requires": { + "@trufflesuite/eth-json-rpc-middleware": "^4.4.2-0", + "await-semaphore": "^0.1.3", + "eth-query": "^2.1.2", + "json-rpc-engine": "^5.1.3", + "lodash.flatmap": "^4.5.0", + "safe-event-emitter": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/multicodec": { - "version": "0.5.7", - "dev": true, - "license": "MIT", - "optional": true, + "@trufflesuite/eth-json-rpc-infura": { + "version": "4.0.3-0", + "resolved": "https://registry.npmjs.org/@trufflesuite/eth-json-rpc-infura/-/eth-json-rpc-infura-4.0.3-0.tgz", + "integrity": "sha512-xaUanOmo0YLqRsL0SfXpFienhdw5bpQ1WEXxMTRi57az4lwpZBv4tFUDvcerdwJrxX9wQqNmgUgd1BrR01dumw==", + "requires": { + "@trufflesuite/eth-json-rpc-middleware": "^4.4.2-1", + "cross-fetch": "^2.1.1", + "eth-json-rpc-errors": "^1.0.1", + "json-rpc-engine": "^5.1.3" + }, "dependencies": { - "varint": "^5.0.0" + "eth-json-rpc-errors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eth-json-rpc-errors/-/eth-json-rpc-errors-1.1.1.tgz", + "integrity": "sha512-WT5shJ5KfNqHi9jOZD+ID8I1kuYWNrigtZat7GOQkvwo99f8SzAVaEcWhJUv656WiZOAg3P1RiJQANtUmDmbIg==", + "requires": { + "fast-safe-stringify": "^2.0.6" + } + } } }, - "node_modules/ganache-core/node_modules/multihashes": { - "version": "0.4.21", - "dev": true, - "license": "MIT", - "optional": true, + "@trufflesuite/eth-json-rpc-middleware": { + "version": "4.4.2-1", + "resolved": "https://registry.npmjs.org/@trufflesuite/eth-json-rpc-middleware/-/eth-json-rpc-middleware-4.4.2-1.tgz", + "integrity": "sha512-iEy9H8ja7/8aYES5HfrepGBKU9n/Y4OabBJEklVd/zIBlhCCBAWBqkIZgXt11nBXO/rYAeKwYuE3puH3ByYnLA==", + "requires": { + "@trufflesuite/eth-sig-util": "^1.4.2", + "btoa": "^1.2.1", + "clone": "^2.1.1", + "eth-json-rpc-errors": "^1.0.1", + "eth-query": "^2.1.2", + "ethereumjs-block": "^1.6.0", + "ethereumjs-tx": "^1.3.7", + "ethereumjs-util": "^5.1.2", + "ethereumjs-vm": "^2.6.0", + "fetch-ponyfill": "^4.0.0", + "json-rpc-engine": "^5.1.3", + "json-stable-stringify": "^1.0.1", + "pify": "^3.0.0", + "safe-event-emitter": "^1.0.1" + }, "dependencies": { - "buffer": "^5.5.0", - "multibase": "^0.7.0", - "varint": "^5.0.0" + "eth-json-rpc-errors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eth-json-rpc-errors/-/eth-json-rpc-errors-1.1.1.tgz", + "integrity": "sha512-WT5shJ5KfNqHi9jOZD+ID8I1kuYWNrigtZat7GOQkvwo99f8SzAVaEcWhJUv656WiZOAg3P1RiJQANtUmDmbIg==", + "requires": { + "fast-safe-stringify": "^2.0.6" + } + }, + "ethereum-common": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", + "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" + }, + "ethereumjs-tx": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", + "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==", + "requires": { + "ethereum-common": "^0.0.18", + "ethereumjs-util": "^5.0.0" + } + }, + "ethereumjs-util": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", + "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "ethjs-util": "^0.1.3", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/multihashes/node_modules/multibase": { - "version": "0.7.0", - "dev": true, - "license": "MIT", - "optional": true, + "@trufflesuite/eth-sig-util": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@trufflesuite/eth-sig-util/-/eth-sig-util-1.4.2.tgz", + "integrity": "sha512-+GyfN6b0LNW77hbQlH3ufZ/1eCON7mMrGym6tdYf7xiNw9Vv3jBO72bmmos1EId2NgBvPMhmYYm6DSLQFTmzrA==", + "requires": { + "ethereumjs-abi": "^0.6.8", + "ethereumjs-util": "^5.1.1" + }, "dependencies": { - "base-x": "^3.0.8", - "buffer": "^5.5.0" + "ethereumjs-util": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", + "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "ethjs-util": "^0.1.3", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/nano-json-stream-parser": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/nanomatch": { - "version": "1.2.13", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "@trufflesuite/web3-provider-engine": { + "version": "15.0.13-1", + "resolved": "https://registry.npmjs.org/@trufflesuite/web3-provider-engine/-/web3-provider-engine-15.0.13-1.tgz", + "integrity": "sha512-6u3x/iIN5fyj8pib5QTUDmIOUiwAGhaqdSTXdqCu6v9zo2BEwdCqgEJd1uXDh3DBmPRDfiZ/ge8oUPy7LerpHg==", + "requires": { + "@trufflesuite/eth-json-rpc-filters": "^4.1.2-1", + "@trufflesuite/eth-json-rpc-infura": "^4.0.3-0", + "@trufflesuite/eth-json-rpc-middleware": "^4.4.2-1", + "@trufflesuite/eth-sig-util": "^1.4.2", + "async": "^2.5.0", + "backoff": "^2.5.0", + "clone": "^2.0.0", + "cross-fetch": "^2.1.0", + "eth-block-tracker": "^4.4.2", + "eth-json-rpc-errors": "^2.0.2", + "ethereumjs-block": "^1.2.2", + "ethereumjs-tx": "^1.2.0", + "ethereumjs-util": "^5.1.5", + "ethereumjs-vm": "^2.3.4", + "json-stable-stringify": "^1.0.1", + "promise-to-callback": "^1.0.0", + "readable-stream": "^2.2.9", + "request": "^2.85.0", + "semaphore": "^1.0.3", + "ws": "^5.1.1", + "xhr": "^2.2.0", + "xtend": "^4.0.1" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "ethereum-common": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", + "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" + }, + "ethereumjs-tx": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", + "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==", + "requires": { + "ethereum-common": "^0.0.18", + "ethereumjs-util": "^5.0.0" + } + }, + "ethereumjs-util": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", + "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "ethjs-util": "^0.1.3", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/negotiator": { - "version": "0.6.2", + "@typechain/ethers-v5": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-10.2.1.tgz", + "integrity": "sha512-n3tQmCZjRE6IU4h6lqUGiQ1j866n5MTCBJreNEHHVWXa2u9GJTaeYyU1/k+1qLutkyw+sS6VAN+AbeiTqsxd/A==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" + "requires": { + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" } }, - "node_modules/ganache-core/node_modules/next-tick": { - "version": "1.0.0", - "dev": true, - "license": "MIT" + "@types/abstract-leveldown": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz", + "integrity": "sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ==", + "dev": true }, - "node_modules/ganache-core/node_modules/nice-try": { - "version": "1.0.5", - "dev": true, - "license": "MIT" + "@types/bn.js": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.0.tgz", + "integrity": "sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA==", + "requires": { + "@types/node": "*" + } }, - "node_modules/ganache-core/node_modules/node-addon-api": { - "version": "2.0.2", + "@types/chai": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", + "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", "dev": true, - "inBundle": true, - "license": "MIT" + "peer": true }, - "node_modules/ganache-core/node_modules/node-fetch": { - "version": "2.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": "4.x || >=6.0.0" - } + "@types/level-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/level-errors/-/level-errors-3.0.0.tgz", + "integrity": "sha512-/lMtoq/Cf/2DVOm6zE6ORyOM+3ZVm/BvzEZVxUhf6bgh8ZHglXlBqxbxSlJeVp8FCbD3IVvk/VbsaNmDjrQvqQ==", + "dev": true }, - "node_modules/ganache-core/node_modules/node-gyp-build": { - "version": "4.2.3", + "@types/levelup": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/levelup/-/levelup-4.3.3.tgz", + "integrity": "sha512-K+OTIjJcZHVlZQN1HmU64VtrC0jC3dXWQozuEIR9zVvltIk90zaGPM2AgT+fIkChpzHhFE3YnvFLCbLtzAmexA==", "dev": true, - "inBundle": true, - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "requires": { + "@types/abstract-leveldown": "*", + "@types/level-errors": "*", + "@types/node": "*" } }, - "node_modules/ganache-core/node_modules/normalize-url": { - "version": "4.5.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } + "@types/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==", + "dev": true }, - "node_modules/ganache-core/node_modules/number-to-bn": { - "version": "1.7.0", + "@types/mkdirp": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.5.2.tgz", + "integrity": "sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bn.js": "4.11.6", - "strip-hex-prefix": "1.0.0" - }, - "engines": { - "node": ">=6.5.0", - "npm": ">=3" + "requires": { + "@types/node": "*" } }, - "node_modules/ganache-core/node_modules/number-to-bn/node_modules/bn.js": { - "version": "4.11.6", - "dev": true, - "license": "MIT", - "optional": true + "@types/node": { + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.10.tgz", + "integrity": "sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==" }, - "node_modules/ganache-core/node_modules/oauth-sign": { - "version": "0.9.0", + "@types/node-fetch": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" } }, - "node_modules/ganache-core/node_modules/object-assign": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "@types/pbkdf2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz", + "integrity": "sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ==", + "requires": { + "@types/node": "*" } }, - "node_modules/ganache-core/node_modules/object-copy": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } + "@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true }, - "node_modules/ganache-core/node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "@types/secp256k1": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.3.tgz", + "integrity": "sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==", + "requires": { + "@types/node": "*" } }, - "node_modules/ganache-core/node_modules/object-copy/node_modules/is-accessor-descriptor": { - "version": "0.1.6", + "@types/sinon": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.6.tgz", + "integrity": "sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==", "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "peer": true, + "requires": { + "@sinonjs/fake-timers": "^7.1.0" } }, - "node_modules/ganache-core/node_modules/object-copy/node_modules/is-buffer": { - "version": "1.1.6", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/object-copy/node_modules/is-data-descriptor": { - "version": "0.1.4", + "@types/sinon-chai": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.8.tgz", + "integrity": "sha512-d4ImIQbT/rKMG8+AXpmcan5T2/PNeSjrYhvkwet6z0p8kzYtfgA32xzOBlbU0yqJfq+/0Ml805iFoODO0LP5/g==", "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "peer": true, + "requires": { + "@types/chai": "*", + "@types/sinon": "*" } }, - "node_modules/ganache-core/node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.6", + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "event-target-shim": "^5.0.0" } }, - "node_modules/ganache-core/node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", + "abstract-leveldown": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz", + "integrity": "sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "requires": { + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" } }, - "node_modules/ganache-core/node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true }, - "node_modules/ganache-core/node_modules/object-inspect": { - "version": "1.9.0", + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "requires": {} }, - "node_modules/ganache-core/node_modules/object-is": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "dev": true }, - "node_modules/ganache-core/node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "aes-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", + "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" }, - "node_modules/ganache-core/node_modules/object-visit": { - "version": "1.0.1", + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "debug": "4" } }, - "node_modules/ganache-core/node_modules/object.assign": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "node_modules/ganache-core/node_modules/object.getownpropertydescriptors": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true }, - "node_modules/ganache-core/node_modules/object.pick": { - "version": "1.3.0", + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "type-fest": "^0.21.3" } }, - "node_modules/ganache-core/node_modules/oboe": { - "version": "2.1.4", - "dev": true, - "license": "BSD", - "optional": true, - "dependencies": { - "http-https": "^1.0.0" - } + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true }, - "node_modules/ganache-core/node_modules/on-finished": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" } }, - "node_modules/ganache-core/node_modules/once": { - "version": "1.4.0", + "antlr4": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.7.1.tgz", + "integrity": "sha512-haHyTW7Y9joE5MVs37P2lNYfU2RWBLfcRDD8OWldcdZm5TiCE91B5Xl1oWSwiDUSd4rlExpt2pu1fksYQjRBYQ==", + "dev": true + }, + "antlr4ts": { + "version": "0.5.0-alpha.4", + "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", + "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", + "dev": true + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" } }, - "node_modules/ganache-core/node_modules/os-homedir": { - "version": "1.0.2", + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "requires": { + "sprintf-js": "~1.0.2" } }, - "node_modules/ganache-core/node_modules/os-tmpdir": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "requires": { + "safer-buffer": "~2.1.0" } }, - "node_modules/ganache-core/node_modules/p-cancelable": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { "version": "1.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true }, - "node_modules/ganache-core/node_modules/p-timeout": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=4" - } + "ast-parents": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ast-parents/-/ast-parents-0.0.1.tgz", + "integrity": "sha1-UI/Q8F0MSHddnszaLhdEIyYejdM=", + "dev": true }, - "node_modules/ganache-core/node_modules/p-timeout/node_modules/p-finally": { + "astral-regex": { "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" } }, - "node_modules/ganache-core/node_modules/parse-asn1": { - "version": "5.1.6", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" + "async-eventemitter": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/async-eventemitter/-/async-eventemitter-0.2.4.tgz", + "integrity": "sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==", + "requires": { + "async": "^2.4.0" } }, - "node_modules/ganache-core/node_modules/parse-headers": { - "version": "2.0.3", - "dev": true, - "license": "MIT" + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, - "node_modules/ganache-core/node_modules/parseurl": { - "version": "1.3.3", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.8" - } + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "node_modules/ganache-core/node_modules/pascalcase": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "await-semaphore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/await-semaphore/-/await-semaphore-0.1.3.tgz", + "integrity": "sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==" }, - "node_modules/ganache-core/node_modules/patch-package": { - "version": "6.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^2.4.2", - "cross-spawn": "^6.0.5", - "find-yarn-workspace-root": "^1.2.1", - "fs-extra": "^7.0.1", - "is-ci": "^2.0.0", - "klaw-sync": "^6.0.0", - "minimist": "^1.2.0", - "rimraf": "^2.6.3", - "semver": "^5.6.0", - "slash": "^2.0.0", - "tmp": "^0.0.33" - }, - "bin": { - "patch-package": "index.js" - }, - "engines": { - "npm": ">5" - } + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, - "node_modules/ganache-core/node_modules/patch-package/node_modules/cross-spawn": { - "version": "6.0.5", + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" + "requires": { + "follow-redirects": "^1.14.0" } }, - "node_modules/ganache-core/node_modules/patch-package/node_modules/path-key": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" + "babel-plugin-polyfill-corejs2": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", + "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", + "requires": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.3.1", + "semver": "^6.1.1" } }, - "node_modules/ganache-core/node_modules/patch-package/node_modules/semver": { - "version": "5.7.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "babel-plugin-polyfill-corejs3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.1.tgz", + "integrity": "sha512-TihqEe4sQcb/QcPJvxe94/9RZuLQuF1+To4WqQcRvc+3J3gLCPIPgDKzGLG6zmQLfH3nn25heRuDNkS2KR4I8A==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.1", + "core-js-compat": "^3.20.0" } }, - "node_modules/ganache-core/node_modules/patch-package/node_modules/shebang-command": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "babel-plugin-polyfill-regenerator": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", + "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.1" } }, - "node_modules/ganache-core/node_modules/patch-package/node_modules/shebang-regex": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", + "requires": { + "precond": "0.2" } }, - "node_modules/ganache-core/node_modules/patch-package/node_modules/slash": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "requires": { + "safe-buffer": "^5.0.1" } }, - "node_modules/ganache-core/node_modules/patch-package/node_modules/tmp": { - "version": "0.0.33", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" }, - "engines": { - "node": ">=0.6.0" + "dependencies": { + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + } } }, - "node_modules/ganache-core/node_modules/patch-package/node_modules/which": { - "version": "1.3.1", + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "dev": true + }, + "bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bip39": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", + "integrity": "sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==", "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" + "requires": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" }, - "bin": { - "which": "bin/which" + "dependencies": { + "@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==", + "dev": true + } } }, - "node_modules/ganache-core/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "blakejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.1.tgz", + "integrity": "sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg==" }, - "node_modules/ganache-core/node_modules/path-parse": { - "version": "1.0.6", + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true, - "license": "MIT" + "peer": true }, - "node_modules/ganache-core/node_modules/path-to-regexp": { - "version": "0.1.7", - "dev": true, - "license": "MIT", - "optional": true + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, - "node_modules/ganache-core/node_modules/pbkdf2": { - "version": "3.1.1", + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/ganache-core/node_modules/performance-now": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/posix-character-classes": { - "version": "0.1.1", + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "requires": { + "fill-range": "^7.0.1" } }, - "node_modules/ganache-core/node_modules/precond": { - "version": "0.2.3", - "dev": true, - "engines": { - "node": ">= 0.6" - } + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, - "node_modules/ganache-core/node_modules/prepend-http": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true }, - "node_modules/ganache-core/node_modules/private": { - "version": "0.1.8", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, - "node_modules/ganache-core/node_modules/process": { - "version": "0.11.10", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" + "browserslist": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", + "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", + "requires": { + "caniuse-lite": "^1.0.30001286", + "electron-to-chromium": "^1.4.17", + "escalade": "^3.1.1", + "node-releases": "^2.0.1", + "picocolors": "^1.0.0" } }, - "node_modules/ganache-core/node_modules/process-nextick-args": { - "version": "2.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/promise-to-callback": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-fn": "^1.0.0", - "set-immediate-shim": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "requires": { + "base-x": "^3.0.2" } }, - "node_modules/ganache-core/node_modules/proxy-addr": { - "version": "2.0.6", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" + "bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "requires": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" } }, - "node_modules/ganache-core/node_modules/prr": { - "version": "1.0.1", - "dev": true, - "license": "MIT" + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" }, - "node_modules/ganache-core/node_modules/pseudomap": { - "version": "1.0.2", + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, - "license": "ISC" + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } }, - "node_modules/ganache-core/node_modules/psl": { - "version": "1.8.0", - "dev": true, - "license": "MIT" + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, - "node_modules/ganache-core/node_modules/public-encrypt": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, - "node_modules/ganache-core/node_modules/pull-cat": { - "version": "1.1.11", - "dev": true, - "license": "MIT" + "bytes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", + "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==", + "dev": true }, - "node_modules/ganache-core/node_modules/pull-defer": { - "version": "0.2.3", + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "dev": true, - "license": "MIT" + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } }, - "node_modules/ganache-core/node_modules/pull-level": { - "version": "2.0.4", + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", "dev": true, - "license": "MIT", - "dependencies": { - "level-post": "^1.0.7", - "pull-cat": "^1.1.9", - "pull-live": "^1.0.1", - "pull-pushable": "^2.0.0", - "pull-stream": "^3.4.0", - "pull-window": "^2.1.4", - "stream-to-pull-stream": "^1.7.1" + "requires": { + "callsites": "^2.0.0" } }, - "node_modules/ganache-core/node_modules/pull-live": { - "version": "1.0.1", + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", "dev": true, - "license": "MIT", - "dependencies": { - "pull-cat": "^1.1.9", - "pull-stream": "^3.4.0" + "requires": { + "caller-callsite": "^2.0.0" } }, - "node_modules/ganache-core/node_modules/pull-pushable": { - "version": "2.2.0", - "dev": true, - "license": "MIT" + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true }, - "node_modules/ganache-core/node_modules/pull-stream": { - "version": "3.6.14", - "dev": true, - "license": "MIT" + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true }, - "node_modules/ganache-core/node_modules/pull-window": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "looper": "^2.0.0" - } + "caniuse-lite": { + "version": "1.0.30001300", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001300.tgz", + "integrity": "sha512-cVjiJHWGcNlJi8TZVKNMnvMid3Z3TTdDHmLDzlOdIiZq138Exvo0G+G0wTdVYolxKb4AYwC+38pxodiInVtJSA==" }, - "node_modules/ganache-core/node_modules/pump": { - "version": "3.0.0", + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "cbor": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", + "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "requires": { + "bignumber.js": "^9.0.1", + "nofilter": "^1.0.4" } }, - "node_modules/ganache-core/node_modules/punycode": { - "version": "2.1.1", + "chai": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" } }, - "node_modules/ganache-core/node_modules/qs": { - "version": "6.5.2", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.6" + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, - "node_modules/ganache-core/node_modules/query-string": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "checkpoint-store": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/checkpoint-store/-/checkpoint-store-1.1.0.tgz", + "integrity": "sha1-BOTLUWuRQziTWB5tRgGnjpVS6gY=", + "requires": { + "functional-red-black-tree": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/randombytes": { - "version": "2.1.0", + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" } }, - "node_modules/ganache-core/node_modules/randomfill": { + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cipher-base": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/circular/-/circular-1.0.5.tgz", + "integrity": "sha1-fad6+Yu96c5LWzWM1Va13e0tMUk=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" + "requires": { + "restore-cursor": "^2.0.0" } }, - "node_modules/ganache-core/node_modules/range-parser": { - "version": "1.2.1", + "cli-logger": { + "version": "0.5.40", + "resolved": "https://registry.npmjs.org/cli-logger/-/cli-logger-0.5.40.tgz", + "integrity": "sha1-CX8OEbByx8aYomxH9YiinCC0iws=", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" + "requires": { + "circular": "^1.0.5", + "cli-util": "~1.1.27" } }, - "node_modules/ganache-core/node_modules/raw-body": { - "version": "2.4.0", + "cli-regexp": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cli-regexp/-/cli-regexp-0.1.2.tgz", + "integrity": "sha1-a82TsJ+y7RAl0woRVdWZeVSlNRI=", + "dev": true + }, + "cli-util": { + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/cli-util/-/cli-util-1.1.27.tgz", + "integrity": "sha1-QtaeNqBAoyH8nPhRwVE8rcUJMFQ=", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" + "requires": { + "cli-regexp": "~0.1.0" } }, - "node_modules/ganache-core/node_modules/readable-stream": { - "version": "2.3.7", + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, - "license": "MIT", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } } }, - "node_modules/ganache-core/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" }, - "node_modules/ganache-core/node_modules/regenerate": { - "version": "1.4.2", + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "dev": true, - "license": "MIT" + "peer": true }, - "node_modules/ganache-core/node_modules/regenerator-runtime": { - "version": "0.11.1", - "dev": true, - "license": "MIT" + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } }, - "node_modules/ganache-core/node_modules/regenerator-transform": { - "version": "0.10.1", - "dev": true, - "license": "BSD", - "dependencies": { - "babel-runtime": "^6.18.0", - "babel-types": "^6.19.0", - "private": "^0.1.6" + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" } }, - "node_modules/ganache-core/node_modules/regex-not": { - "version": "1.0.2", + "command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true + }, + "command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" } }, - "node_modules/ganache-core/node_modules/regexp.prototype.flags": { - "version": "1.3.0", + "command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - }, - "engines": { - "node": ">= 0.4" + "requires": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true + }, + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true + } } }, - "node_modules/ganache-core/node_modules/regexp.prototype.flags/node_modules/es-abstract": { - "version": "1.17.7", - "dev": true, - "license": "MIT", - "dependencies": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "peer": true, + "requires": { + "safe-buffer": "~5.1.1" } }, - "node_modules/ganache-core/node_modules/regexpu-core": { - "version": "2.0.0", - "dev": true, - "license": "MIT", + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "dev": true + }, + "core-js-compat": { + "version": "3.20.3", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.20.3.tgz", + "integrity": "sha512-c8M5h0IkNZ+I92QhIpuSijOxGAcj3lgpsWdkCqmUTZNwidujF4r3pi6x1DCN+Vcs5qTS2XWWMfWSuCqyupX8gw==", + "requires": { + "browserslist": "^4.19.1", + "semver": "7.0.0" + }, "dependencies": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==" + } } }, - "node_modules/ganache-core/node_modules/regjsgen": { - "version": "0.2.0", - "dev": true, - "license": "MIT" + "core-js-pure": { + "version": "3.20.3", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.20.3.tgz", + "integrity": "sha512-Q2H6tQ5MtPtcC7f3HxJ48i4Q7T9ybPKgvWyuH7JXIoNa2pm0KuBnycsET/qw1SLLZYfbsbrZQNMeIOClb+6WIA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, - "node_modules/ganache-core/node_modules/regjsparser": { - "version": "0.1.5", + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", "dev": true, - "license": "BSD", - "dependencies": { - "jsesc": "~0.5.0" + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" }, - "bin": { - "regjsparser": "bin/parser" + "dependencies": { + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } } }, - "node_modules/ganache-core/node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", + "crc-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", + "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", "dev": true, - "bin": { - "jsesc": "bin/jsesc" + "requires": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" } }, - "node_modules/ganache-core/node_modules/repeat-element": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" } }, - "node_modules/ganache-core/node_modules/repeat-string": { - "version": "1.6.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" } }, - "node_modules/ganache-core/node_modules/repeating": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-finite": "^1.0.0" + "cross-fetch": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.5.tgz", + "integrity": "sha512-xqYAhQb4NhCJSRym03dwxpP1bYXpK3y7UN83Bo2WFi3x1Zmzn0SL/6xGoPr+gpt4WmNrgCCX3HPysvOwFOW36w==", + "requires": { + "node-fetch": "2.6.1", + "whatwg-fetch": "2.0.4" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/request": { - "version": "2.88.2", - "dev": true, - "license": "Apache-2.0", "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + } } }, - "node_modules/ganache-core/node_modules/resolve-url": { - "version": "0.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/responselike": { - "version": "1.0.2", + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, - "license": "MIT", - "optional": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, "dependencies": { - "lowercase-keys": "^1.0.0" + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, - "node_modules/ganache-core/node_modules/resumer": { - "version": "0.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "through": "~2.3.4" + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" } }, - "node_modules/ganache-core/node_modules/ret": { - "version": "0.1.15", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12" + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" } }, - "node_modules/ganache-core/node_modules/rimraf": { - "version": "2.6.3", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true }, - "node_modules/ganache-core/node_modules/ripemd160": { - "version": "2.0.2", + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "dev": true, - "license": "MIT", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "requires": { + "type-detect": "^4.0.0" } }, - "node_modules/ganache-core/node_modules/rlp": { - "version": "2.2.6", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "bn.js": "^4.11.1" - }, - "bin": { - "rlp": "bin/rlp" - } + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true }, - "node_modules/ganache-core/node_modules/rustbn.js": { - "version": "0.2.0", - "dev": true, - "license": "(MIT OR Apache-2.0)" + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, - "node_modules/ganache-core/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/safe-event-emitter": { - "version": "1.0.1", + "deferred-leveldown": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", "dev": true, - "license": "ISC", + "requires": { + "abstract-leveldown": "~6.2.1", + "inherits": "^2.0.3" + }, "dependencies": { - "events": "^3.0.0" + "abstract-leveldown": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + } + } } }, - "node_modules/ganache-core/node_modules/safe-regex": { - "version": "1.1.0", + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, - "license": "MIT", - "dependencies": { - "ret": "~0.1.10" + "requires": { + "object-keys": "^1.0.12" } }, - "node_modules/ganache-core/node_modules/safer-buffer": { - "version": "2.1.2", - "dev": true, - "license": "MIT" + "delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "dev": true }, - "node_modules/ganache-core/node_modules/scrypt-js": { - "version": "3.0.1", - "dev": true, - "license": "MIT" + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, - "node_modules/ganache-core/node_modules/scryptsy": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "pbkdf2": "^3.0.3" - } + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true }, - "node_modules/ganache-core/node_modules/secp256k1": { - "version": "4.0.2", + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "elliptic": "^6.5.2", - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" - }, - "engines": { - "node": ">=10.0.0" + "requires": { + "esutils": "^2.0.2" } }, - "node_modules/ganache-core/node_modules/seedrandom": { - "version": "3.0.1", - "dev": true, - "license": "MIT" + "dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" }, - "node_modules/ganache-core/node_modules/semaphore": { - "version": "1.1.0", - "dev": true, - "engines": { - "node": ">=0.8.0" + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, - "node_modules/ganache-core/node_modules/send": { - "version": "0.17.1", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "engines": { - "node": ">= 0.8.0" - } + "electron-to-chromium": { + "version": "1.4.48", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.48.tgz", + "integrity": "sha512-RT3SEmpv7XUA+tKXrZGudAWLDpa7f8qmhjcLaM6OD/ERxjQ/zAojT8/Vvo0BSzbArkElFZ1WyZ9FuwAYbkdBNA==" }, - "node_modules/ganache-core/node_modules/send/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "2.0.0" + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "optional": true + "emittery": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.0.tgz", + "integrity": "sha512-AGvFfs+d0JKCJQ4o01ASQLGPmSCxgfU9RFXvzPvZdjKK8oscynksuJhWrSTSw7j7Ep/sZct5b5ZhYCi8S/t0HQ==", + "dev": true }, - "node_modules/ganache-core/node_modules/send/node_modules/ms": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "optional": true + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true }, - "node_modules/ganache-core/node_modules/serve-static": { - "version": "1.14.1", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "requires": { + "iconv-lite": "^0.6.2" }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ganache-core/node_modules/servify": { - "version": "0.1.12", - "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "body-parser": "^1.16.0", - "cors": "^2.8.1", - "express": "^4.14.0", - "request": "^2.79.0", - "xhr": "^2.3.3" - }, - "engines": { - "node": ">=6" + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } } }, - "node_modules/ganache-core/node_modules/set-immediate-shim": { - "version": "1.0.1", + "encoding-down": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", + "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "requires": { + "abstract-leveldown": "^6.2.1", + "inherits": "^2.0.3", + "level-codec": "^9.0.0", + "level-errors": "^2.0.0" } }, - "node_modules/ganache-core/node_modules/set-value": { - "version": "2.0.1", + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "ansi-colors": "^4.1.1" } }, - "node_modules/ganache-core/node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true }, - "node_modules/ganache-core/node_modules/set-value/node_modules/is-extendable": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "requires": { + "prr": "~1.0.1" } }, - "node_modules/ganache-core/node_modules/setimmediate": { - "version": "1.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/setprototypeof": { - "version": "1.1.1", + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, - "license": "ISC", - "optional": true + "requires": { + "is-arrayish": "^0.2.1" + } }, - "node_modules/ganache-core/node_modules/sha.js": { - "version": "2.4.11", + "es-abstract": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", "dev": true, - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/ganache-core/node_modules/simple-concat": { - "version": "1.0.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/simple-get": { - "version": "2.8.1", - "dev": true, - "license": "MIT", - "optional": true, "dependencies": { - "decompress-response": "^3.3.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/snapdragon": { - "version": "0.8.2", + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, - "license": "MIT", - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" } }, - "node_modules/ganache-core/node_modules/snapdragon-node": { - "version": "2.1.1", + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, - "node_modules/ganache-core/node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" } }, - "node_modules/ganache-core/node_modules/snapdragon-util": { - "version": "3.0.1", + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "eslint-visitor-keys": "^1.1.0" } }, - "node_modules/ganache-core/node_modules/snapdragon-util/node_modules/is-buffer": { - "version": "1.1.6", - "dev": true, - "license": "MIT" + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true }, - "node_modules/ganache-core/node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" } }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" + "requires": { + "estraverse": "^5.1.0" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/extend-shallow": { - "version": "2.0.1", - "dev": true, - "license": "MIT", "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } } }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/is-accessor-descriptor": { - "version": "0.1.6", + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" + "requires": { + "estraverse": "^5.2.0" }, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } } }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/is-buffer": { - "version": "1.1.6", - "dev": true, - "license": "MIT" + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/is-data-descriptor": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "eth-block-tracker": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/eth-block-tracker/-/eth-block-tracker-4.4.3.tgz", + "integrity": "sha512-A8tG4Z4iNg4mw5tP1Vung9N9IjgMNqpiMoJ/FouSFwNCGHv2X0mmOYwtQOJzki6XN7r7Tyo01S29p7b224I4jw==", + "requires": { + "@babel/plugin-transform-runtime": "^7.5.5", + "@babel/runtime": "^7.5.5", + "eth-query": "^2.1.0", + "json-rpc-random-id": "^1.0.1", + "pify": "^3.0.0", + "safe-event-emitter": "^1.0.1" } }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", + "eth-ens-namehash": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz", + "integrity": "sha512-VWEI1+KJfz4Km//dadyvBBoBeSQ0MHTXPvr8UIXiLW6IanxvAV+DmlZAijZwAyggqGUfwQBeHf7tc9wzc1piSw==", "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" + "peer": true, + "requires": { + "idna-uts46-hx": "^2.3.1", + "js-sha3": "^0.5.7" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.6", - "dev": true, - "license": "MIT", "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/is-extendable": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "js-sha3": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", + "integrity": "sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g==", + "dev": true, + "peer": true + } } }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/kind-of": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "eth-json-rpc-errors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eth-json-rpc-errors/-/eth-json-rpc-errors-2.0.2.tgz", + "integrity": "sha512-uBCRM2w2ewusRHGxN8JhcuOb2RN3ueAOYH/0BhqdFmQkZx5lj5+fLKTz0mIVOzd4FG5/kUksCzCD7eTEim6gaA==", + "requires": { + "fast-safe-stringify": "^2.0.6" } }, - "node_modules/ganache-core/node_modules/snapdragon/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/source-map": { - "version": "0.5.7", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" + "eth-query": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/eth-query/-/eth-query-2.1.2.tgz", + "integrity": "sha1-1nQdkAAQa1FRDHLbktY2VFam2l4=", + "requires": { + "json-rpc-random-id": "^1.0.0", + "xtend": "^4.0.1" } }, - "node_modules/ganache-core/node_modules/source-map-resolve": { - "version": "0.5.3", - "dev": true, - "license": "MIT", - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" + "eth-rpc-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eth-rpc-errors/-/eth-rpc-errors-3.0.0.tgz", + "integrity": "sha512-iPPNHPrLwUlR9xCSYm7HHQjWBasor3+KZfRvwEWxMz3ca0yqnlBeJrnyphkGIXZ4J7AMAaOLmwy4AWhnxOiLxg==", + "requires": { + "fast-safe-stringify": "^2.0.6" } }, - "node_modules/ganache-core/node_modules/source-map-support": { - "version": "0.5.12", + "eth-sig-util": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.5.4.tgz", + "integrity": "sha512-aCMBwp8q/4wrW4QLsF/HYBOSA7TpLKmkVwP3pYQNkEEseW2Rr8Z5Uxc9/h6HX+OG3tuHo+2bINVSihIeBfym6A==", "dev": true, - "license": "MIT", + "requires": { + "ethereumjs-abi": "0.6.8", + "ethereumjs-util": "^5.1.1", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.0" + }, "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "ethereumjs-util": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", + "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", + "dev": true, + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "ethjs-util": "^0.1.3", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", + "ethereum-bloom-filters": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.10.tgz", + "integrity": "sha512-rxJ5OFN3RwjQxDcFP2Z5+Q9ho4eIdEmSc2ht0fCu8Se9nbXjZ7/031uXoUYJ87KHCOdVeiUuwSnoS7hmYAGVHA==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" + "peer": true, + "requires": { + "js-sha3": "^0.8.0" } }, - "node_modules/ganache-core/node_modules/source-map-url": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/split-string": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } + "ethereum-common": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.2.0.tgz", + "integrity": "sha512-XOnAR/3rntJgbCdGhqdaLIxDLWKLmsZOGhHdBKadEr6gEnJLH52k93Ou+TUdFaPN3hJc3isBZBal3U/XZ15abA==" }, - "node_modules/ganache-core/node_modules/sshpk": { - "version": "1.16.1", - "dev": true, - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "engines": { - "node": ">=0.10.0" + "ethereum-cryptography": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz", + "integrity": "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==", + "requires": { + "@types/pbkdf2": "^3.0.0", + "@types/secp256k1": "^4.0.1", + "blakejs": "^1.1.0", + "browserify-aes": "^1.2.0", + "bs58check": "^2.1.2", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "hash.js": "^1.1.7", + "keccak": "^3.0.0", + "pbkdf2": "^3.0.17", + "randombytes": "^2.1.0", + "safe-buffer": "^5.1.2", + "scrypt-js": "^3.0.0", + "secp256k1": "^4.0.1", + "setimmediate": "^1.0.5" } }, - "node_modules/ganache-core/node_modules/sshpk/node_modules/tweetnacl": { - "version": "0.14.5", - "dev": true, - "license": "Unlicense" + "ethereum-protocol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ethereum-protocol/-/ethereum-protocol-1.0.1.tgz", + "integrity": "sha512-3KLX1mHuEsBW0dKG+c6EOJS1NBNqdCICvZW9sInmZTt5aY0oxmHVggYRE0lJu1tcnMD1K+AKHdLi6U43Awm1Vg==" }, - "node_modules/ganache-core/node_modules/static-extend": { - "version": "0.1.2", + "ethereum-waffle": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/ethereum-waffle/-/ethereum-waffle-4.0.10.tgz", + "integrity": "sha512-iw9z1otq7qNkGDNcMoeNeLIATF9yKl1M8AIeu42ElfNBplq0e+5PeasQmm8ybY/elkZ1XyRO0JBQxQdVRb8bqQ==", "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "requires": { + "@ethereum-waffle/chai": "4.0.10", + "@ethereum-waffle/compiler": "4.0.3", + "@ethereum-waffle/mock-contract": "4.0.4", + "@ethereum-waffle/provider": "4.0.5", + "solc": "0.8.15", + "typechain": "^8.0.0" } }, - "node_modules/ganache-core/node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "ethereumjs-abi": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz", + "integrity": "sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA==", + "requires": { + "bn.js": "^4.11.8", + "ethereumjs-util": "^6.0.0" } }, - "node_modules/ganache-core/node_modules/static-extend/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" + "ethereumjs-account": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/ethereumjs-account/-/ethereumjs-account-2.0.5.tgz", + "integrity": "sha512-bgDojnXGjhMwo6eXQC0bY6UK2liSFUSMwwylOmQvZbSl/D7NXQ3+vrGO46ZeOgjGfxXmgIeVNDIiHw7fNZM4VA==", + "requires": { + "ethereumjs-util": "^5.0.0", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "ethereumjs-util": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", + "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "ethjs-util": "^0.1.3", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/static-extend/node_modules/is-buffer": { - "version": "1.1.6", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/static-extend/node_modules/is-data-descriptor": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" + "ethereumjs-block": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-1.7.1.tgz", + "integrity": "sha512-B+sSdtqm78fmKkBq78/QLKJbu/4Ts4P2KFISdgcuZUPDm9x+N7qgBPIIFUGbaakQh8bzuquiRVbdmvPKqbILRg==", + "requires": { + "async": "^2.0.1", + "ethereum-common": "0.2.0", + "ethereumjs-tx": "^1.2.2", + "ethereumjs-util": "^5.0.0", + "merkle-patricia-tree": "^2.1.2" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "ethereumjs-tx": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", + "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==", + "requires": { + "ethereum-common": "^0.0.18", + "ethereumjs-util": "^5.0.0" + }, + "dependencies": { + "ethereum-common": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", + "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" + } + } + }, + "ethereumjs-util": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", + "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "ethjs-util": "^0.1.3", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1" + } + } } }, - "node_modules/ganache-core/node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } + "ethereumjs-common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ethereumjs-common/-/ethereumjs-common-1.5.2.tgz", + "integrity": "sha512-hTfZjwGX52GS2jcVO6E2sx4YuFnf0Fhp5ylo4pEPhEffNln7vS59Hr5sLnp3/QCazFLluuBZ+FZ6J5HTp0EqCA==" }, - "node_modules/ganache-core/node_modules/static-extend/node_modules/kind-of": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "ethereumjs-tx": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-2.1.2.tgz", + "integrity": "sha512-zZEK1onCeiORb0wyCXUvg94Ve5It/K6GD1K+26KfFKodiBiS6d9lfCXlUKGBBdQ+bv7Day+JK0tj1K+BeNFRAw==", + "requires": { + "ethereumjs-common": "^1.5.0", + "ethereumjs-util": "^6.0.0" } }, - "node_modules/ganache-core/node_modules/statuses": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" + "ethereumjs-util": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz", + "integrity": "sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==", + "requires": { + "@types/bn.js": "^4.11.3", + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "ethjs-util": "0.1.6", + "rlp": "^2.2.3" + }, + "dependencies": { + "@types/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "requires": { + "@types/node": "*" + } + } } }, - "node_modules/ganache-core/node_modules/stream-to-pull-stream": { - "version": "1.7.3", - "dev": true, - "license": "MIT", + "ethereumjs-vm": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.6.0.tgz", + "integrity": "sha512-r/XIUik/ynGbxS3y+mvGnbOKnuLo40V5Mj1J25+HEO63aWYREIqvWeRO/hnROlMBE5WoniQmPmhiaN0ctiHaXw==", + "requires": { + "async": "^2.1.2", + "async-eventemitter": "^0.2.2", + "ethereumjs-account": "^2.0.3", + "ethereumjs-block": "~2.2.0", + "ethereumjs-common": "^1.1.0", + "ethereumjs-util": "^6.0.0", + "fake-merkle-patricia-tree": "^1.0.1", + "functional-red-black-tree": "^1.0.1", + "merkle-patricia-tree": "^2.3.2", + "rustbn.js": "~0.2.0", + "safe-buffer": "^5.1.1" + }, "dependencies": { - "looper": "^3.0.0", - "pull-stream": "^3.2.3" + "ethereumjs-block": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-2.2.2.tgz", + "integrity": "sha512-2p49ifhek3h2zeg/+da6XpdFR3GlqY3BIEiqxGF8j9aSRIgkb7M1Ky+yULBKJOu8PAZxfhsYA+HxUk2aCQp3vg==", + "requires": { + "async": "^2.0.1", + "ethereumjs-common": "^1.5.0", + "ethereumjs-tx": "^2.1.1", + "ethereumjs-util": "^5.0.0", + "merkle-patricia-tree": "^2.1.2" + }, + "dependencies": { + "ethereumjs-util": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", + "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "ethjs-util": "^0.1.3", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1" + } + } + } + } } }, - "node_modules/ganache-core/node_modules/stream-to-pull-stream/node_modules/looper": { - "version": "3.0.0", - "dev": true, - "license": "MIT" + "ethereumjs-wallet": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-1.0.2.tgz", + "integrity": "sha512-CCWV4RESJgRdHIvFciVQFnCHfqyhXWchTPlkfp28Qc53ufs+doi5I/cV2+xeK9+qEo25XCWfP9MiL+WEPAZfdA==", + "requires": { + "aes-js": "^3.1.2", + "bs58check": "^2.1.2", + "ethereum-cryptography": "^0.1.3", + "ethereumjs-util": "^7.1.2", + "randombytes": "^2.1.0", + "scrypt-js": "^3.0.1", + "utf8": "^3.0.0", + "uuid": "^8.3.2" + }, + "dependencies": { + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" + }, + "ethereumjs-util": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", + "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", + "requires": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + } + } + } }, - "node_modules/ganache-core/node_modules/strict-uri-encode": { - "version": "1.1.0", + "ethers": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.5.3.tgz", + "integrity": "sha512-fTT4WT8/hTe/BLwRUtl7I5zlpF3XC3P/Xwqxc5AIP2HGlH15qpmjs0Ou78az93b1rLITzXLFxoNX63B8ZbUd7g==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" + "requires": { + "@ethersproject/abi": "5.5.0", + "@ethersproject/abstract-provider": "5.5.1", + "@ethersproject/abstract-signer": "5.5.0", + "@ethersproject/address": "5.5.0", + "@ethersproject/base64": "5.5.0", + "@ethersproject/basex": "5.5.0", + "@ethersproject/bignumber": "5.5.0", + "@ethersproject/bytes": "5.5.0", + "@ethersproject/constants": "5.5.0", + "@ethersproject/contracts": "5.5.0", + "@ethersproject/hash": "5.5.0", + "@ethersproject/hdnode": "5.5.0", + "@ethersproject/json-wallets": "5.5.0", + "@ethersproject/keccak256": "5.5.0", + "@ethersproject/logger": "5.5.0", + "@ethersproject/networks": "5.5.2", + "@ethersproject/pbkdf2": "5.5.0", + "@ethersproject/properties": "5.5.0", + "@ethersproject/providers": "5.5.2", + "@ethersproject/random": "5.5.1", + "@ethersproject/rlp": "5.5.0", + "@ethersproject/sha2": "5.5.0", + "@ethersproject/signing-key": "5.5.0", + "@ethersproject/solidity": "5.5.0", + "@ethersproject/strings": "5.5.0", + "@ethersproject/transactions": "5.5.0", + "@ethersproject/units": "5.5.0", + "@ethersproject/wallet": "5.5.0", + "@ethersproject/web": "5.5.1", + "@ethersproject/wordlists": "5.5.0" } }, - "node_modules/ganache-core/node_modules/string_decoder": { - "version": "1.1.1", + "ethjs-unit": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", + "integrity": "sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==", "dev": true, - "license": "MIT", + "peer": true, + "requires": { + "bn.js": "4.11.6", + "number-to-bn": "1.7.0" + }, "dependencies": { - "safe-buffer": "~5.1.0" + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "dev": true, + "peer": true + } } }, - "node_modules/ganache-core/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/string.prototype.trim": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "ethjs-util": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", + "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "requires": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" } }, - "node_modules/ganache-core/node_modules/string.prototype.trimend": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, - "node_modules/ganache-core/node_modules/string.prototype.trimstart": { + "evp_bytestokey": { "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" } }, - "node_modules/ganache-core/node_modules/strip-hex-prefix": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-hex-prefixed": "1.0.0" - }, - "engines": { - "node": ">=6.5.0", - "npm": ">=3" - } + "exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "dev": true }, - "node_modules/ganache-core/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, - "node_modules/ganache-core/node_modules/swarm-js": { - "version": "0.1.40", + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bluebird": "^3.5.0", - "buffer": "^5.0.5", - "eth-lib": "^0.1.26", - "fs-extra": "^4.0.2", - "got": "^7.1.0", - "mime-types": "^2.1.16", - "mkdirp-promise": "^5.0.1", - "mock-fs": "^4.1.0", - "setimmediate": "^1.0.5", - "tar": "^4.0.2", - "xhr-request": "^1.0.1" + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" } }, - "node_modules/ganache-core/node_modules/swarm-js/node_modules/fs-extra": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, - "node_modules/ganache-core/node_modules/swarm-js/node_modules/get-stream": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" + "fake-merkle-patricia-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz", + "integrity": "sha1-S4w6z7Ugr635hgsfFM2M40As3dM=", + "requires": { + "checkpoint-store": "^1.1.0" } }, - "node_modules/ganache-core/node_modules/swarm-js/node_modules/got": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^3.2.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-plain-obj": "^1.1.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "p-cancelable": "^0.3.0", - "p-timeout": "^1.1.1", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "url-parse-lax": "^1.0.0", - "url-to-options": "^1.0.1" - }, - "engines": { - "node": ">=4" - } + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/ganache-core/node_modules/swarm-js/node_modules/is-stream": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true }, - "node_modules/ganache-core/node_modules/swarm-js/node_modules/p-cancelable": { - "version": "0.3.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, - "node_modules/ganache-core/node_modules/swarm-js/node_modules/prepend-http": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true }, - "node_modules/ganache-core/node_modules/swarm-js/node_modules/url-parse-lax": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "prepend-http": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, - "node_modules/ganache-core/node_modules/tape": { - "version": "4.13.3", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-equal": "~1.1.1", - "defined": "~1.0.0", - "dotignore": "~0.1.2", - "for-each": "~0.3.3", - "function-bind": "~1.1.1", - "glob": "~7.1.6", - "has": "~1.0.3", - "inherits": "~2.0.4", - "is-regex": "~1.0.5", - "minimist": "~1.2.5", - "object-inspect": "~1.7.0", - "resolve": "~1.17.0", - "resumer": "~0.0.0", - "string.prototype.trim": "~1.2.1", - "through": "~2.3.8" + "fetch-ponyfill": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz", + "integrity": "sha1-rjzl9zLGReq4fkroeTQUcJsjmJM=", + "requires": { + "node-fetch": "~1.7.1" }, - "bin": { - "tape": "bin/tape" - } - }, - "node_modules/ganache-core/node_modules/tape/node_modules/glob": { - "version": "7.1.6", - "dev": true, - "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + } } }, - "node_modules/ganache-core/node_modules/tape/node_modules/is-regex": { - "version": "1.0.5", + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "escape-string-regexp": "^1.0.5" } }, - "node_modules/ganache-core/node_modules/tape/node_modules/object-inspect": { - "version": "1.7.0", + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "flat-cache": "^2.0.1" } }, - "node_modules/ganache-core/node_modules/tape/node_modules/resolve": { - "version": "1.17.0", + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, - "license": "MIT", - "dependencies": { - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "requires": { + "to-regex-range": "^5.0.1" } }, - "node_modules/ganache-core/node_modules/tar": { - "version": "4.4.13", + "find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "engines": { - "node": ">=4.5" + "requires": { + "array-back": "^3.0.1" } }, - "node_modules/ganache-core/node_modules/tar/node_modules/fs-minipass": { - "version": "1.2.7", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^2.6.0" - } - }, - "node_modules/ganache-core/node_modules/tar/node_modules/minipass": { - "version": "2.9.0", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "node_modules/ganache-core/node_modules/through": { - "version": "2.3.8", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/through2": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/ganache-core/node_modules/timed-out": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/tmp": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "rimraf": "^2.6.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/ganache-core/node_modules/to-object-path": { - "version": "0.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/to-object-path/node_modules/is-buffer": { - "version": "1.1.6", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/to-readable-stream": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ganache-core/node_modules/to-regex": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/toidentifier": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/ganache-core/node_modules/tough-cookie": { - "version": "2.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/ganache-core/node_modules/trim-right": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/tunnel-agent": { - "version": "0.6.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ganache-core/node_modules/tweetnacl": { - "version": "1.0.3", - "dev": true, - "license": "Unlicense" - }, - "node_modules/ganache-core/node_modules/tweetnacl-util": { - "version": "0.15.1", - "dev": true, - "license": "Unlicense" - }, - "node_modules/ganache-core/node_modules/type": { - "version": "1.2.0", - "dev": true, - "license": "ISC" - }, - "node_modules/ganache-core/node_modules/type-is": { - "version": "1.6.18", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ganache-core/node_modules/typedarray": { - "version": "0.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/ganache-core/node_modules/typewise": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "typewise-core": "^1.2.0" - } - }, - "node_modules/ganache-core/node_modules/typewise-core": { - "version": "1.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/typewiselite": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/ultron": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/underscore": { - "version": "1.9.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/union-value": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/union-value/node_modules/is-extendable": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/universalify": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/ganache-core/node_modules/unorm": { - "version": "1.6.0", - "dev": true, - "license": "MIT or GPL-2.0", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/ganache-core/node_modules/unpipe": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ganache-core/node_modules/unset-value": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "find-up": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, - "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/ganache-core/node_modules/urix": { - "version": "0.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/url-parse-lax": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "prepend-http": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ganache-core/node_modules/url-set-query": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/url-to-options": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/ganache-core/node_modules/use": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ganache-core/node_modules/utf-8-validate": { - "version": "5.0.4", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.2.0" - } - }, - "node_modules/ganache-core/node_modules/utf8": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/util.promisify": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "for-each": "^0.3.3", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ganache-core/node_modules/utils-merge": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/ganache-core/node_modules/uuid": { - "version": "3.4.0", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/ganache-core/node_modules/varint": { - "version": "5.0.2", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/vary": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ganache-core/node_modules/verror": { - "version": "1.10.0", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/ganache-core/node_modules/web3": { - "version": "1.2.11", - "dev": true, - "hasInstallScript": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "web3-bzz": "1.2.11", - "web3-core": "1.2.11", - "web3-eth": "1.2.11", - "web3-eth-personal": "1.2.11", - "web3-net": "1.2.11", - "web3-shh": "1.2.11", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-bzz": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "@types/node": "^12.12.6", - "got": "9.6.0", - "swarm-js": "^0.1.40", - "underscore": "1.9.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-bzz/node_modules/@types/node": { - "version": "12.19.12", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/web3-core": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "@types/bn.js": "^4.11.5", - "@types/node": "^12.12.6", - "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.2.11", - "web3-core-method": "1.2.11", - "web3-core-requestmanager": "1.2.11", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-core-helpers": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "underscore": "1.9.1", - "web3-eth-iban": "1.2.11", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-core-method": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "@ethersproject/transactions": "^5.0.0-beta.135", - "underscore": "1.9.1", - "web3-core-helpers": "1.2.11", - "web3-core-promievent": "1.2.11", - "web3-core-subscriptions": "1.2.11", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-core-promievent": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "eventemitter3": "4.0.4" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-core-requestmanager": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "underscore": "1.9.1", - "web3-core-helpers": "1.2.11", - "web3-providers-http": "1.2.11", - "web3-providers-ipc": "1.2.11", - "web3-providers-ws": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-core-subscriptions": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "eventemitter3": "4.0.4", - "underscore": "1.9.1", - "web3-core-helpers": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-core/node_modules/@types/node": { - "version": "12.19.12", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/web3-eth": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "underscore": "1.9.1", - "web3-core": "1.2.11", - "web3-core-helpers": "1.2.11", - "web3-core-method": "1.2.11", - "web3-core-subscriptions": "1.2.11", - "web3-eth-abi": "1.2.11", - "web3-eth-accounts": "1.2.11", - "web3-eth-contract": "1.2.11", - "web3-eth-ens": "1.2.11", - "web3-eth-iban": "1.2.11", - "web3-eth-personal": "1.2.11", - "web3-net": "1.2.11", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-eth-abi": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "@ethersproject/abi": "5.0.0-beta.153", - "underscore": "1.9.1", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-eth-accounts": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "crypto-browserify": "3.12.0", - "eth-lib": "0.2.8", - "ethereumjs-common": "^1.3.2", - "ethereumjs-tx": "^2.1.1", - "scrypt-js": "^3.0.1", - "underscore": "1.9.1", - "uuid": "3.3.2", - "web3-core": "1.2.11", - "web3-core-helpers": "1.2.11", - "web3-core-method": "1.2.11", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-eth-accounts/node_modules/eth-lib": { - "version": "0.2.8", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "xhr-request-promise": "^0.1.2" - } - }, - "node_modules/ganache-core/node_modules/web3-eth-accounts/node_modules/uuid": { - "version": "3.3.2", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/ganache-core/node_modules/web3-eth-contract": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "@types/bn.js": "^4.11.5", - "underscore": "1.9.1", - "web3-core": "1.2.11", - "web3-core-helpers": "1.2.11", - "web3-core-method": "1.2.11", - "web3-core-promievent": "1.2.11", - "web3-core-subscriptions": "1.2.11", - "web3-eth-abi": "1.2.11", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-eth-ens": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "content-hash": "^2.5.2", - "eth-ens-namehash": "2.0.8", - "underscore": "1.9.1", - "web3-core": "1.2.11", - "web3-core-helpers": "1.2.11", - "web3-core-promievent": "1.2.11", - "web3-eth-abi": "1.2.11", - "web3-eth-contract": "1.2.11", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-eth-iban": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "bn.js": "^4.11.9", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-eth-personal": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "@types/node": "^12.12.6", - "web3-core": "1.2.11", - "web3-core-helpers": "1.2.11", - "web3-core-method": "1.2.11", - "web3-net": "1.2.11", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-eth-personal/node_modules/@types/node": { - "version": "12.19.12", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/web3-net": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "web3-core": "1.2.11", - "web3-core-method": "1.2.11", - "web3-utils": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine": { - "version": "14.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^2.5.0", - "backoff": "^2.5.0", - "clone": "^2.0.0", - "cross-fetch": "^2.1.0", - "eth-block-tracker": "^3.0.0", - "eth-json-rpc-infura": "^3.1.0", - "eth-sig-util": "3.0.0", - "ethereumjs-block": "^1.2.2", - "ethereumjs-tx": "^1.2.0", - "ethereumjs-util": "^5.1.5", - "ethereumjs-vm": "^2.3.4", - "json-rpc-error": "^2.0.0", - "json-stable-stringify": "^1.0.1", - "promise-to-callback": "^1.0.0", - "readable-stream": "^2.2.9", - "request": "^2.85.0", - "semaphore": "^1.0.3", - "ws": "^5.1.1", - "xhr": "^2.2.0", - "xtend": "^4.0.1" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/abstract-leveldown": { - "version": "2.6.3", - "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "~4.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/deferred-leveldown": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~2.6.0" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/eth-sig-util": { - "version": "1.4.2", - "dev": true, - "license": "ISC", - "dependencies": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git", - "ethereumjs-util": "^5.1.1" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ethereumjs-account": { - "version": "2.0.5", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "ethereumjs-util": "^5.0.0", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ethereumjs-block": { - "version": "1.7.1", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "async": "^2.0.1", - "ethereum-common": "0.2.0", - "ethereumjs-tx": "^1.2.2", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ethereumjs-block/node_modules/ethereum-common": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ethereumjs-tx": { - "version": "1.3.7", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ethereumjs-vm": { - "version": "2.6.0", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "async": "^2.1.2", - "async-eventemitter": "^0.2.2", - "ethereumjs-account": "^2.0.3", - "ethereumjs-block": "~2.2.0", - "ethereumjs-common": "^1.1.0", - "ethereumjs-util": "^6.0.0", - "fake-merkle-patricia-tree": "^1.0.1", - "functional-red-black-tree": "^1.0.1", - "merkle-patricia-tree": "^2.3.2", - "rustbn.js": "~0.2.0", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ethereumjs-vm/node_modules/ethereumjs-block": { - "version": "2.2.2", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "async": "^2.0.1", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.1", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ethereumjs-vm/node_modules/ethereumjs-block/node_modules/ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ethereumjs-vm/node_modules/ethereumjs-tx": { - "version": "2.1.2", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ethereumjs-vm/node_modules/ethereumjs-util": { - "version": "6.2.1", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/isarray": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/level-codec": { - "version": "7.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/level-errors": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "errno": "~0.1.1" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/level-iterator-stream": { - "version": "1.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/level-iterator-stream/node_modules/readable-stream": { - "version": "1.1.14", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/level-ws": { - "version": "0.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/level-ws/node_modules/readable-stream": { - "version": "1.0.34", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/level-ws/node_modules/xtend": { - "version": "2.1.2", - "dev": true, - "dependencies": { - "object-keys": "~0.4.0" - }, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/levelup": { - "version": "1.3.9", - "dev": true, - "license": "MIT", - "dependencies": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ltgt": { - "version": "2.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/memdown": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/memdown/node_modules/abstract-leveldown": { - "version": "2.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "~4.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/merkle-patricia-tree": { - "version": "2.3.2", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/merkle-patricia-tree/node_modules/async": { - "version": "1.5.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/object-keys": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/semver": { - "version": "5.4.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/string_decoder": { - "version": "0.10.31", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/web3-provider-engine/node_modules/ws": { - "version": "5.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "async-limiter": "~1.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-providers-http": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "web3-core-helpers": "1.2.11", - "xhr2-cookies": "1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-providers-ipc": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "oboe": "2.1.4", - "underscore": "1.9.1", - "web3-core-helpers": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-providers-ws": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "eventemitter3": "4.0.4", - "underscore": "1.9.1", - "web3-core-helpers": "1.2.11", - "websocket": "^1.0.31" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-shh": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "web3-core": "1.2.11", - "web3-core-method": "1.2.11", - "web3-core-subscriptions": "1.2.11", - "web3-net": "1.2.11" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-utils": { - "version": "1.2.11", - "dev": true, - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "bn.js": "^4.11.9", - "eth-lib": "0.2.8", - "ethereum-bloom-filters": "^1.0.6", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "underscore": "1.9.1", - "utf8": "3.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ganache-core/node_modules/web3-utils/node_modules/eth-lib": { - "version": "0.2.8", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "xhr-request-promise": "^0.1.2" - } - }, - "node_modules/ganache-core/node_modules/websocket": { - "version": "1.0.32", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.50", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/ganache-core/node_modules/websocket/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/ganache-core/node_modules/websocket/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/whatwg-fetch": { - "version": "2.0.4", - "dev": true, - "license": "MIT" - }, - "node_modules/ganache-core/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "node_modules/ganache-core/node_modules/ws": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } - }, - "node_modules/ganache-core/node_modules/ws/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/ganache-core/node_modules/xhr": { - "version": "2.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "global": "~4.4.0", - "is-function": "^1.0.1", - "parse-headers": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "node_modules/ganache-core/node_modules/xhr-request": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer-to-arraybuffer": "^0.0.5", - "object-assign": "^4.1.1", - "query-string": "^5.0.1", - "simple-get": "^2.7.0", - "timed-out": "^4.0.1", - "url-set-query": "^1.0.0", - "xhr": "^2.0.4" - } - }, - "node_modules/ganache-core/node_modules/xhr-request-promise": { - "version": "0.1.3", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "xhr-request": "^1.1.0" - } - }, - "node_modules/ganache-core/node_modules/xhr2-cookies": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "cookiejar": "^2.1.1" - } - }, - "node_modules/ganache-core/node_modules/xtend": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/ganache-core/node_modules/yaeti": { - "version": "0.0.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.32" - } - }, - "node_modules/ganache-core/node_modules/yallist": { - "version": "3.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dependencies": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true, - "engines": { - "node": ">=4.x" - } - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/hardhat": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.8.2.tgz", - "integrity": "sha512-cBUqzZGOi+lwKHArWl5Be7zeFIwlu1IUXOna6k5XhORZ8hAWDVbAJBVfxgmjkcX5GffIf0C5g841zRxo36sQ5g==", - "dev": true, - "dependencies": { - "@ethereumjs/block": "^3.6.0", - "@ethereumjs/blockchain": "^5.5.0", - "@ethereumjs/common": "^2.6.0", - "@ethereumjs/tx": "^3.4.0", - "@ethereumjs/vm": "^5.6.0", - "@ethersproject/abi": "^5.1.2", - "@sentry/node": "^5.18.1", - "@solidity-parser/parser": "^0.14.0", - "@types/bn.js": "^5.1.0", - "@types/lru-cache": "^5.1.0", - "abort-controller": "^3.0.0", - "adm-zip": "^0.4.16", - "ansi-escapes": "^4.3.0", - "chalk": "^2.4.2", - "chokidar": "^3.4.0", - "ci-info": "^2.0.0", - "debug": "^4.1.1", - "enquirer": "^2.3.0", - "env-paths": "^2.2.0", - "eth-sig-util": "^2.5.2", - "ethereum-cryptography": "^0.1.2", - "ethereumjs-abi": "^0.6.8", - "ethereumjs-util": "^7.1.3", - "find-up": "^2.1.0", - "fp-ts": "1.19.3", - "fs-extra": "^7.0.1", - "glob": "^7.1.3", - "https-proxy-agent": "^5.0.0", - "immutable": "^4.0.0-rc.12", - "io-ts": "1.10.4", - "lodash": "^4.17.11", - "merkle-patricia-tree": "^4.2.2", - "mnemonist": "^0.38.0", - "mocha": "^7.2.0", - "node-fetch": "^2.6.0", - "qs": "^6.7.0", - "raw-body": "^2.4.1", - "resolve": "1.17.0", - "semver": "^6.3.0", - "slash": "^3.0.0", - "solc": "0.7.3", - "source-map-support": "^0.5.13", - "stacktrace-parser": "^0.1.10", - "true-case-path": "^2.2.1", - "tsort": "0.0.1", - "uuid": "^8.3.2", - "ws": "^7.4.6" - }, - "bin": { - "hardhat": "internal/cli/cli.js" - }, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/hardhat/node_modules/bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "node_modules/hardhat/node_modules/ethereumjs-util": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", - "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", - "dev": true, - "dependencies": { - "@types/bn.js": "^5.1.0", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/hardhat/node_modules/jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/hardhat/node_modules/level-ws": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/level-ws/-/level-ws-2.0.0.tgz", - "integrity": "sha512-1iv7VXx0G9ec1isqQZ7y5LmoZo/ewAsyDHNA8EFDW5hqH2Kqovm33nSFkSdnLLAK+I5FlT+lo5Cw9itGe+CpQA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "readable-stream": "^3.1.0", - "xtend": "^4.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/hardhat/node_modules/merkle-patricia-tree": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-4.2.2.tgz", - "integrity": "sha512-eqZYNTshcYx9aESkSPr71EqwsR/QmpnObDEV4iLxkt/x/IoLYZYjJvKY72voP/27Vy61iMOrfOG6jrn7ttXD+Q==", - "dev": true, - "dependencies": { - "@types/levelup": "^4.3.0", - "ethereumjs-util": "^7.1.2", - "level-mem": "^5.0.1", - "level-ws": "^2.0.0", - "readable-stream": "^3.6.0", - "rlp": "^2.2.4", - "semaphore-async-await": "^1.5.1" - } - }, - "node_modules/hardhat/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/hardhat/node_modules/resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, - "dependencies": { - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hardhat/node_modules/solc": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/solc/-/solc-0.7.3.tgz", - "integrity": "sha512-GAsWNAjGzIDg7VxzP6mPjdurby3IkGCjQcM8GFYZT6RyaoUZKmMU6Y7YwG+tFGhv7dwZ8rmR4iwFDrrD99JwqA==", - "dev": true, - "dependencies": { - "command-exists": "^1.2.8", - "commander": "3.0.2", - "follow-redirects": "^1.12.1", - "fs-extra": "^0.30.0", - "js-sha3": "0.8.0", - "memorystream": "^0.3.1", - "require-from-string": "^2.0.0", - "semver": "^5.5.0", - "tmp": "0.0.33" - }, - "bin": { - "solcjs": "solcjs" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/hardhat/node_modules/solc/node_modules/fs-extra": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", - "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" - } - }, - "node_modules/hardhat/node_modules/solc/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/hardhat/node_modules/ws": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", - "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", - "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/hash-base/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/idna-uts46-hx": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz", - "integrity": "sha512-PWoF9Keq6laYdIRwwCdhTPl60xRqAloYNMQLiyUnG42VjT53oW07BXIRM+NK7eQjzXjAk2gUvX9caRxlnF9TAA==", - "dev": true, - "dependencies": { - "punycode": "2.1.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", - "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==" - }, - "node_modules/immutable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", - "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", - "dev": true - }, - "node_modules/import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/inquirer/node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/inquirer/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/io-ts": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz", - "integrity": "sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==", - "dev": true, - "dependencies": { - "fp-ts": "^1.0.0" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "dependencies": { - "ci-info": "^2.0.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fn/-/is-fn-1.0.0.tgz", - "integrity": "sha1-lUPV3nvPWwiiLsiiC65uKG1RDYw=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/is-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", - "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hex-prefixed": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", - "integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=", - "engines": { - "node": ">=6.5.0", - "npm": ">=3" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "node_modules/is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", - "dev": true - }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "node_modules/js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "node_modules/json-rpc-engine": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-5.4.0.tgz", - "integrity": "sha512-rAffKbPoNDjuRnXkecTjnsE3xLLrb00rEkdgalINhaYVYIxDwWtvYBr9UFbhTvPB1B2qUOLoFd/cV6f4Q7mh7g==", - "dependencies": { - "eth-rpc-errors": "^3.0.0", - "safe-event-emitter": "^1.0.1" - } - }, - "node_modules/json-rpc-random-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz", - "integrity": "sha1-uknZat7RRE27jaPSA3SKy7zeyMg=" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dependencies": { - "jsonify": "~0.0.0" - } - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "peer": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "engines": { - "node": "*" - } - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/keccak": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", - "integrity": "sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==", - "hasInstallScript": true, - "dependencies": { - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/keccak/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.9" - } - }, - "node_modules/klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.11" - } - }, - "node_modules/lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "dependencies": { - "invert-kv": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/level-codec": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", - "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", - "dev": true, - "dependencies": { - "buffer": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-concat-iterator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", - "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", - "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", - "dev": true, - "dependencies": { - "errno": "~0.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-iterator-stream": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", - "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", - "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.4.0", - "xtend": "^4.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-iterator-stream/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/level-mem": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/level-mem/-/level-mem-5.0.1.tgz", - "integrity": "sha512-qd+qUJHXsGSFoHTziptAKXoLX87QjR7v2KMbqncDXPxQuCdsQlzmyX+gwrEHhlzn08vkf8TyipYyMmiC6Gobzg==", - "dev": true, - "dependencies": { - "level-packager": "^5.0.3", - "memdown": "^5.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-packager": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", - "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", - "dev": true, - "dependencies": { - "encoding-down": "^6.3.0", - "levelup": "^4.3.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-supports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", - "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", - "dev": true, - "dependencies": { - "xtend": "^4.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/level-ws": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/level-ws/-/level-ws-0.0.0.tgz", - "integrity": "sha1-Ny5RIXeSSgBCSwtDrvK7QkltIos=", - "dependencies": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" - } - }, - "node_modules/level-ws/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "node_modules/level-ws/node_modules/object-keys": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" - }, - "node_modules/level-ws/node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/level-ws/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "node_modules/level-ws/node_modules/xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "dependencies": { - "object-keys": "~0.4.0" - }, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/levelup": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", - "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", - "dev": true, - "dependencies": { - "deferred-leveldown": "~5.3.0", - "level-errors": "~2.0.0", - "level-iterator-stream": "~4.0.0", - "level-supports": "~1.0.0", - "xtend": "~4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/load-json-file/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", - "dev": true - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" - }, - "node_modules/lodash.flatmap": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", - "integrity": "sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=" - }, - "node_modules/log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", - "dev": true, - "dependencies": { - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lru_map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", - "integrity": "sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=", - "dev": true - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/ltgt": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", - "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" - }, - "node_modules/mcl-wasm": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/mcl-wasm/-/mcl-wasm-0.7.9.tgz", - "integrity": "sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ==", - "dev": true, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/memdown": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/memdown/-/memdown-5.1.0.tgz", - "integrity": "sha512-B3J+UizMRAlEArDjWHTMmadet+UKwHd3UjMgGBkZcKAxAYVPS9o0Yeiha4qvz7iGiL2Sb3igUft6p7nbFWctpw==", - "dev": true, - "dependencies": { - "abstract-leveldown": "~6.2.1", - "functional-red-black-tree": "~1.0.1", - "immediate": "~3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/memdown/node_modules/abstract-leveldown": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", - "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "immediate": "^3.2.3", - "level-concat-iterator": "~2.0.0", - "level-supports": "~1.0.0", - "xtend": "~4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/memdown/node_modules/immediate": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", - "integrity": "sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=", - "dev": true - }, - "node_modules/memdown/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/memorystream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/merkle-patricia-tree": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-2.3.2.tgz", - "integrity": "sha512-81PW5m8oz/pz3GvsAwbauj7Y00rqm81Tzad77tHBwU7pIAtN+TJnMSOJhxBKflSVYhptMMb9RskhqHqrSm1V+g==", - "dependencies": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" - } - }, - "node_modules/merkle-patricia-tree/node_modules/abstract-leveldown": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz", - "integrity": "sha512-2++wDf/DYqkPR3o5tbfdhF96EfMApo1GpPfzOsR/ZYXdkSmELlvOOEAl9iKkRsktMPHdGjO4rtkBpf2I7TiTeA==", - "dependencies": { - "xtend": "~4.0.0" - } - }, - "node_modules/merkle-patricia-tree/node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "node_modules/merkle-patricia-tree/node_modules/deferred-leveldown": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-1.2.2.tgz", - "integrity": "sha512-uukrWD2bguRtXilKt6cAWKyoXrTSMo5m7crUdLfWQmu8kIm88w3QZoUL+6nhpfKVmhHANER6Re3sKoNoZ3IKMA==", - "dependencies": { - "abstract-leveldown": "~2.6.0" - } - }, - "node_modules/merkle-patricia-tree/node_modules/ethereumjs-util": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", - "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", - "dependencies": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/merkle-patricia-tree/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "node_modules/merkle-patricia-tree/node_modules/level-codec": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-7.0.1.tgz", - "integrity": "sha512-Ua/R9B9r3RasXdRmOtd+t9TCOEIIlts+TN/7XTT2unhDaL6sJn83S3rUyljbr6lVtw49N3/yA0HHjpV6Kzb2aQ==" - }, - "node_modules/merkle-patricia-tree/node_modules/level-errors": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-1.0.5.tgz", - "integrity": "sha512-/cLUpQduF6bNrWuAC4pwtUKA5t669pCsCi2XbmojG2tFeOr9j6ShtdDCtFFQO1DRt+EVZhx9gPzP9G2bUaG4ig==", - "dependencies": { - "errno": "~0.1.1" - } - }, - "node_modules/merkle-patricia-tree/node_modules/level-iterator-stream": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-1.3.1.tgz", - "integrity": "sha1-5Dt4sagUPm+pek9IXrjqUwNS8u0=", - "dependencies": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" - } - }, - "node_modules/merkle-patricia-tree/node_modules/level-iterator-stream/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/merkle-patricia-tree/node_modules/levelup": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/levelup/-/levelup-1.3.9.tgz", - "integrity": "sha512-VVGHfKIlmw8w1XqpGOAGwq6sZm2WwWLmlDcULkKWQXEA5EopA8OBNJ2Ck2v6bdk8HeEZSbCSEgzXadyQFm76sQ==", - "dependencies": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" - } - }, - "node_modules/merkle-patricia-tree/node_modules/memdown": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/memdown/-/memdown-1.4.1.tgz", - "integrity": "sha1-tOThkhdGZP+65BNhqlAPMRnv4hU=", - "dependencies": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" - } - }, - "node_modules/merkle-patricia-tree/node_modules/memdown/node_modules/abstract-leveldown": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.7.2.tgz", - "integrity": "sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w==", - "dependencies": { - "xtend": "~4.0.0" - } - }, - "node_modules/merkle-patricia-tree/node_modules/semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/merkle-patricia-tree/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dependencies": { - "mime-db": "1.51.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", - "dependencies": { - "dom-walk": "^0.1.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" - }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mnemonist": { - "version": "0.38.5", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", - "integrity": "sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==", - "dev": true, - "dependencies": { - "obliterator": "^2.0.0" - } - }, - "node_modules/mocha": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", - "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", - "dev": true, - "dependencies": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.5", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.1.1" - } - }, - "node_modules/mocha/node_modules/debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/mocha/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "deprecated": "\"Please update to latest v2.3 or v2.2\"", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mocha/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "node_modules/mocha/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", - "dev": true, - "dependencies": { - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/node-addon-api": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", - "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" - }, - "node_modules/node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dev": true, - "dependencies": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "node_modules/node-environment-flags/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-gyp-build": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", - "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==" - }, - "node_modules/nofilter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", - "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/number-to-bn": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", - "integrity": "sha1-uzYjWS9+X54AMLGXe9QaDFP+HqA=", - "dev": true, - "dependencies": { - "bn.js": "4.11.6", - "strip-hex-prefix": "1.0.0" - }, - "engines": { - "node": ">=6.5.0", - "npm": ">=3" - } - }, - "node_modules/number-to-bn/node_modules/bn.js": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=", - "dev": true - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, - "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz", - "integrity": "sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obliterator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.1.tgz", - "integrity": "sha512-XnkiCrrBcIZQitJPAI36mrrpEUvatbte8hLcTcQwKA1v9NkCKasSi+UAguLsLDs/out7MoRzAlmz7VXvY6ph6w==", - "dev": true - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "dependencies": { - "lcid": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parent-module/node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-headers": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.4.tgz", - "integrity": "sha512-psZ9iZoCNFLrgRjZ1d8mn0h9WRqJwFxM9q3x7iUjN/YT2OksthDJ5TiPCu2F38kS4zutqfW+YdVVkBZZx3/1aw==" - }, - "node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/patch-package": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.4.7.tgz", - "integrity": "sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==", - "dev": true, - "dependencies": { - "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^2.4.2", - "cross-spawn": "^6.0.5", - "find-yarn-workspace-root": "^2.0.0", - "fs-extra": "^7.0.1", - "is-ci": "^2.0.0", - "klaw-sync": "^6.0.0", - "minimist": "^1.2.0", - "open": "^7.4.2", - "rimraf": "^2.6.3", - "semver": "^5.6.0", - "slash": "^2.0.0", - "tmp": "^0.0.33" - }, - "bin": { - "patch-package": "index.js" - }, - "engines": { - "npm": ">5" - } - }, - "node_modules/patch-package/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/patch-package/node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-type/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "engines": { - "node": ">=4" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postinstall-postinstall": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz", - "integrity": "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ==", - "dev": true, - "hasInstallScript": true - }, - "node_modules/precond": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", - "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/printj": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", - "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", - "dev": true, - "bin": { - "printj": "bin/printj.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-to-callback": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/promise-to-callback/-/promise-to-callback-1.0.0.tgz", - "integrity": "sha1-XSp0kBC/tn2WNZj805YHRqaP7vc=", - "dependencies": { - "is-fn": "^1.0.0", - "set-immediate-shim": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" - }, - "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, - "node_modules/punycode": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", - "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystring": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", - "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/raw-body": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", - "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", - "dev": true, - "dependencies": { - "bytes": "3.1.1", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" - }, - "node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "engines": { - "node": ">=6.5.0" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/request/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "node_modules/resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", - "dependencies": { - "is-core-module": "^2.8.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/rlp": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.7.tgz", - "integrity": "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==", - "dependencies": { - "bn.js": "^5.2.0" - }, - "bin": { - "rlp": "bin/rlp" - } - }, - "node_modules/rlp/node_modules/bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" - }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/rustbn.js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/rustbn.js/-/rustbn.js-0.2.0.tgz", - "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==" - }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safe-event-emitter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-event-emitter/-/safe-event-emitter-1.0.1.tgz", - "integrity": "sha512-e1wFe99A91XYYxoQbcq2ZJUWurxEyP8vfz7A7vuUe1s95q8r5ebraVaA1BukYJcpM6V16ugWoD9vngi8Ccu5fg==", - "deprecated": "Renamed to @metamask/safe-event-emitter", - "dependencies": { - "events": "^3.0.0" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/scrypt-js": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", - "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==" - }, - "node_modules/secp256k1": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", - "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", - "hasInstallScript": true, - "dependencies": { - "elliptic": "^6.5.4", - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/semaphore": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", - "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/semaphore-async-await": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/semaphore-async-await/-/semaphore-async-await-1.5.1.tgz", - "integrity": "sha1-hXvvXjZEYBykuVcLh+nfXKEpdPo=", - "dev": true, - "engines": { - "node": ">=4.1" - } - }, - "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "node_modules/set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", - "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/solc": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/solc/-/solc-0.6.12.tgz", - "integrity": "sha512-Lm0Ql2G9Qc7yPP2Ba+WNmzw2jwsrd3u4PobHYlSOxaut3TtUbj9+5ZrT6f4DUpNPEoBaFUOEg9Op9C0mk7ge9g==", - "dev": true, - "dependencies": { - "command-exists": "^1.2.8", - "commander": "3.0.2", - "fs-extra": "^0.30.0", - "js-sha3": "0.8.0", - "memorystream": "^0.3.1", - "require-from-string": "^2.0.0", - "semver": "^5.5.0", - "tmp": "0.0.33" - }, - "bin": { - "solcjs": "solcjs" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/solc/node_modules/fs-extra": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", - "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" - } - }, - "node_modules/solc/node_modules/jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/solc/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/solhint": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.3.6.tgz", - "integrity": "sha512-HWUxTAv2h7hx3s3hAab3ifnlwb02ZWhwFU/wSudUHqteMS3ll9c+m1FlGn9V8ztE2rf3Z82fQZA005Wv7KpcFA==", - "dev": true, - "dependencies": { - "@solidity-parser/parser": "^0.13.2", - "ajv": "^6.6.1", - "antlr4": "4.7.1", - "ast-parents": "0.0.1", - "chalk": "^2.4.2", - "commander": "2.18.0", - "cosmiconfig": "^5.0.7", - "eslint": "^5.6.0", - "fast-diff": "^1.1.2", - "glob": "^7.1.3", - "ignore": "^4.0.6", - "js-yaml": "^3.12.0", - "lodash": "^4.17.11", - "semver": "^6.3.0" - }, - "bin": { - "solhint": "solhint.js" - }, - "optionalDependencies": { - "prettier": "^1.14.3" - } - }, - "node_modules/solhint/node_modules/@solidity-parser/parser": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.13.2.tgz", - "integrity": "sha512-RwHnpRnfrnD2MSPveYoPh8nhofEvX7fgjHk1Oq+NNvCcLx4r1js91CO9o+F/F3fBzOCyvm8kKRTriFICX/odWw==", - "dev": true, - "dependencies": { - "antlr4ts": "^0.5.0-alpha.4" - } - }, - "node_modules/solhint/node_modules/commander": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", - "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", - "dev": true - }, - "node_modules/solhint/node_modules/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true, - "optional": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", - "dev": true - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sshpk/node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "node_modules/stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "dev": true, - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "dependencies": { - "is-utf8": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-hex-prefix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", - "integrity": "sha1-DF8VX+8RUTczd96du1iNoFUA428=", - "dependencies": { - "is-hex-prefixed": "1.0.0" - }, - "engines": { - "node": ">=6.5.0", - "npm": ">=3" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/table/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/table/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/table/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/test-value": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", - "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=", - "dev": true, - "dependencies": { - "array-back": "^1.0.3", - "typical": "^2.6.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/test-value/node_modules/array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", - "dev": true, - "dependencies": { - "typical": "^2.6.0" - }, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/testrpc": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/testrpc/-/testrpc-0.0.1.tgz", - "integrity": "sha512-afH1hO+SQ/VPlmaLUFj2636QMeDvPCeQMc/9RBMW0IfjNe9gFD9Ra3ShqYkB7py0do1ZcCna/9acHyzTJ+GcNA==", - "deprecated": "testrpc has been renamed to ganache-cli, please use this package from now on.", - "dev": true - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tough-cookie/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true - }, - "node_modules/true-case-path": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-2.2.1.tgz", - "integrity": "sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q==", - "dev": true - }, - "node_modules/truffle-plugin-verify": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/truffle-plugin-verify/-/truffle-plugin-verify-0.5.20.tgz", - "integrity": "sha512-s6zG7QbVK5tWPAhRz1oKi/M8SXdRgcWR4PRuHM/BB0qZBcE/82WmnqyC2D/qfqEY+BCgUUWXfc/hyzsgH4dyNw==", - "dev": true, - "dependencies": { - "axios": "^0.21.1", - "cli-logger": "^0.5.40", - "delay": "^5.0.0", - "querystring": "^0.2.1" - } - }, - "node_modules/ts-essentials": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-1.0.4.tgz", - "integrity": "sha512-q3N1xS4vZpRouhYHDPwO0bDW3EZ6SK9CrrDHxi/D6BPReSjpVgWIOpLS2o0gSBZm+7q/wyKp6RVM1AeeW7uyfQ==", - "dev": true - }, - "node_modules/ts-generator": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ts-generator/-/ts-generator-0.1.1.tgz", - "integrity": "sha512-N+ahhZxTLYu1HNTQetwWcx3so8hcYbkKBHTr4b4/YgObFTIKkOSSsaa+nal12w8mfrJAyzJfETXawbNjSfP2gQ==", - "dev": true, - "dependencies": { - "@types/mkdirp": "^0.5.2", - "@types/prettier": "^2.1.1", - "@types/resolve": "^0.0.8", - "chalk": "^2.4.1", - "glob": "^7.1.2", - "mkdirp": "^0.5.1", - "prettier": "^2.1.2", - "resolve": "^1.8.1", - "ts-essentials": "^1.0.0" - }, - "bin": { - "ts-generator": "dist/cli/run.js" - } - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tsort": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz", - "integrity": "sha1-4igPXoF/i/QnVlf9D5rr1E9aJ4Y=", - "dev": true - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "dev": true - }, - "node_modules/tweetnacl-util": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", - "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", - "dev": true - }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typechain": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/typechain/-/typechain-3.0.0.tgz", - "integrity": "sha512-ft4KVmiN3zH4JUFu2WJBrwfHeDf772Tt2d8bssDTo/YcckKW2D+OwFrHXRC6hJvO3mHjFQTihoMV6fJOi0Hngg==", - "dev": true, - "dependencies": { - "command-line-args": "^4.0.7", - "debug": "^4.1.1", - "fs-extra": "^7.0.0", - "js-sha3": "^0.8.0", - "lodash": "^4.17.15", - "ts-essentials": "^6.0.3", - "ts-generator": "^0.1.1" - }, - "bin": { - "typechain": "dist/cli/cli.js" - } - }, - "node_modules/typechain/node_modules/ts-essentials": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-6.0.7.tgz", - "integrity": "sha512-2E4HIIj4tQJlIHuATRHayv0EfMGK3ris/GRk1E3CFnsZzeNV+hUmelbaTZHLtXaZppM5oLhHRtO04gINC4Jusw==", - "dev": true, - "peerDependencies": { - "typescript": ">=3.7.0" - } - }, - "node_modules/typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", - "dev": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=", - "dev": true - }, - "node_modules/unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - }, - "node_modules/url/node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/utf8": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", - "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "node_modules/web3-utils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.0.tgz", - "integrity": "sha512-O8Tl4Ky40Sp6pe89Olk2FsaUkgHyb5QAXuaKo38ms3CxZZ4d3rPGfjP9DNKGm5+IUgAZBNpF1VmlSmNCqfDI1w==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/web3-utils/node_modules/ethereumjs-util": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", - "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", - "dev": true, - "dependencies": { - "@types/bn.js": "^5.1.0", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/web3-utils/node_modules/ethereumjs-util/node_modules/bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true - }, - "node_modules/whatwg-fetch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, - "node_modules/window-size": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", - "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=", - "dev": true, - "bin": { - "window-size": "cli.js" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ws": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", - "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", - "dependencies": { - "async-limiter": "~1.0.0" - } - }, - "node_modules/xhr": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", - "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", - "dependencies": { - "global": "~4.4.0", - "is-function": "^1.0.1", - "parse-headers": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "dependencies": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "node_modules/yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", - "dev": true, - "dependencies": { - "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - } - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/compat-data": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.8.tgz", - "integrity": "sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q==" - }, - "@babel/core": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.7.tgz", - "integrity": "sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==", - "peer": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.7", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" - } - }, - "@babel/generator": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz", - "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==", - "requires": { - "@babel/types": "^7.16.8", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "requires": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz", - "integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==", - "requires": { - "@babel/helper-compilation-targets": "^7.13.0", - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/traverse": "^7.13.0", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "peer": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==" - }, - "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "peer": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==" - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==" - }, - "@babel/helpers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.7.tgz", - "integrity": "sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==", - "peer": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/highlight": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz", - "integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==", - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", - "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==" - }, - "@babel/plugin-transform-runtime": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.8.tgz", - "integrity": "sha512-6Kg2XHPFnIarNweZxmzbgYnnWsXxkx9WQUVk2sksBRL80lBC1RAQV3wQagWxdCHiYHqPN+oenwNIuttlYgIbQQ==", - "requires": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-plugin-utils": "^7.16.7", - "babel-plugin-polyfill-corejs2": "^0.3.0", - "babel-plugin-polyfill-corejs3": "^0.5.0", - "babel-plugin-polyfill-regenerator": "^0.3.0", - "semver": "^6.3.0" - } - }, - "@babel/runtime": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.8.tgz", - "integrity": "sha512-xe+H7JlvKsDQwXRsBhSnq1/+9c+LlQcCK3Tn/l5sbx02HYns/cn7ibp9+RV1sIUqu7hKg91NWsgHurO9dowITQ==", - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.8", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.16.8", - "@babel/types": "^7.16.8", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz", - "integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==", - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "@ensdomains/ens": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@ensdomains/ens/-/ens-0.4.5.tgz", - "integrity": "sha512-JSvpj1iNMFjK6K+uVl4unqMoa9rf5jopb8cya5UGBWz23Nw8hSNT7efgUx4BTlAPAgpNlEioUfeTyQ6J9ZvTVw==", - "dev": true, - "requires": { - "bluebird": "^3.5.2", - "eth-ens-namehash": "^2.0.8", - "solc": "^0.4.20", - "testrpc": "0.0.1", - "web3-utils": "^1.0.0-beta.31" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "fs-extra": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", - "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "require-from-string": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", - "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "solc": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/solc/-/solc-0.4.26.tgz", - "integrity": "sha512-o+c6FpkiHd+HPjmjEVpQgH7fqZ14tJpXhho+/bQXlXbliLIS/xjXb42Vxh+qQY1WCSTMQ0+a5vR9vi0MfhU6mA==", - "dev": true, - "requires": { - "fs-extra": "^0.30.0", - "memorystream": "^0.3.1", - "require-from-string": "^1.1.0", - "semver": "^5.3.0", - "yargs": "^4.7.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", - "dev": true - }, - "yargs": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", - "integrity": "sha1-wMQpJMpKqmsObaFznfshZDn53cA=", - "dev": true, - "requires": { - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "lodash.assign": "^4.0.3", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.1", - "which-module": "^1.0.0", - "window-size": "^0.2.0", - "y18n": "^3.2.1", - "yargs-parser": "^2.4.1" - } - }, - "yargs-parser": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", - "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "lodash.assign": "^4.0.6" - } - } - } - }, - "@ensdomains/resolver": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@ensdomains/resolver/-/resolver-0.2.4.tgz", - "integrity": "sha512-bvaTH34PMCbv6anRa9I/0zjLJgY4EuznbEMgbV77JBCQ9KNC46rzi0avuxpOfu+xDjPEtSFGqVEOr5GlUSGudA==", - "dev": true - }, - "@ethereum-waffle/chai": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@ethereum-waffle/chai/-/chai-3.4.1.tgz", - "integrity": "sha512-8mjgjWCe8XSCWuyJgVtJY8sm00VTczGBTDxBejgEBWN/J9x7QD8jdmWW8bfxdnqZbxiDCTvRFL58Wmd254BEqQ==", - "dev": true, - "requires": { - "@ethereum-waffle/provider": "^3.4.0", - "ethers": "^5.4.7" - } - }, - "@ethereum-waffle/compiler": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@ethereum-waffle/compiler/-/compiler-3.4.0.tgz", - "integrity": "sha512-a2wxGOoB9F1QFRE+Om7Cz2wn+pxM/o7a0a6cbwhaS2lECJgFzeN9xEkVrKahRkF4gEfXGcuORg4msP0Asxezlw==", - "dev": true, - "requires": { - "@resolver-engine/imports": "^0.3.3", - "@resolver-engine/imports-fs": "^0.3.3", - "@typechain/ethers-v5": "^2.0.0", - "@types/mkdirp": "^0.5.2", - "@types/node-fetch": "^2.5.5", - "ethers": "^5.0.1", - "mkdirp": "^0.5.1", - "node-fetch": "^2.6.1", - "solc": "^0.6.3", - "ts-generator": "^0.1.1", - "typechain": "^3.0.0" - } - }, - "@ethereum-waffle/ens": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@ethereum-waffle/ens/-/ens-3.3.1.tgz", - "integrity": "sha512-xSjNWnT2Iwii3J3XGqD+F5yLEOzQzLHNLGfI5KIXdtQ4FHgReW/AMGRgPPLi+n+SP08oEQWJ3sEKrvbFlwJuaA==", - "dev": true, - "requires": { - "@ensdomains/ens": "^0.4.4", - "@ensdomains/resolver": "^0.2.4", - "ethers": "^5.5.2" - } - }, - "@ethereum-waffle/mock-contract": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@ethereum-waffle/mock-contract/-/mock-contract-3.3.1.tgz", - "integrity": "sha512-h9yChF7IkpJLODg/o9/jlwKwTcXJLSEIq3gewgwUJuBHnhPkJGekcZvsTbximYc+e42QUZrDUATSuTCIryeCEA==", - "dev": true, - "requires": { - "@ethersproject/abi": "^5.5.0", - "ethers": "^5.5.2" - } - }, - "@ethereum-waffle/provider": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@ethereum-waffle/provider/-/provider-3.4.1.tgz", - "integrity": "sha512-5iDte7c9g9N1rTRE/P4npwk1Hus/wA2yH850X6sP30mr1IrwSG9NKn6/2SOQkAVJnh9jqyLVg2X9xCODWL8G4A==", - "dev": true, - "requires": { - "@ethereum-waffle/ens": "^3.3.1", - "ethers": "^5.5.2", - "ganache-core": "^2.13.2", - "patch-package": "^6.2.2", - "postinstall-postinstall": "^2.1.0" - } - }, - "@ethereumjs/block": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/block/-/block-3.6.0.tgz", - "integrity": "sha512-dqLo1LtsLG+Oelu5S5tWUDG0pah3QUwV5TJZy2cm19BXDr4ka/S9XBSgao0i09gTcuPlovlHgcs6d7EZ37urjQ==", - "dev": true, - "requires": { - "@ethereumjs/common": "^2.6.0", - "@ethereumjs/tx": "^3.4.0", - "ethereumjs-util": "^7.1.3", - "merkle-patricia-tree": "^4.2.2" - }, - "dependencies": { - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "ethereumjs-util": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", - "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", - "dev": true, - "requires": { - "@types/bn.js": "^5.1.0", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.2.4" - } - }, - "level-ws": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/level-ws/-/level-ws-2.0.0.tgz", - "integrity": "sha512-1iv7VXx0G9ec1isqQZ7y5LmoZo/ewAsyDHNA8EFDW5hqH2Kqovm33nSFkSdnLLAK+I5FlT+lo5Cw9itGe+CpQA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^3.1.0", - "xtend": "^4.0.1" - } - }, - "merkle-patricia-tree": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-4.2.2.tgz", - "integrity": "sha512-eqZYNTshcYx9aESkSPr71EqwsR/QmpnObDEV4iLxkt/x/IoLYZYjJvKY72voP/27Vy61iMOrfOG6jrn7ttXD+Q==", - "dev": true, - "requires": { - "@types/levelup": "^4.3.0", - "ethereumjs-util": "^7.1.2", - "level-mem": "^5.0.1", - "level-ws": "^2.0.0", - "readable-stream": "^3.6.0", - "rlp": "^2.2.4", - "semaphore-async-await": "^1.5.1" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "@ethereumjs/blockchain": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@ethereumjs/blockchain/-/blockchain-5.5.1.tgz", - "integrity": "sha512-JS2jeKxl3tlaa5oXrZ8mGoVBCz6YqsGG350XVNtHAtNZXKk7pU3rH4xzF2ru42fksMMqzFLzKh9l4EQzmNWDqA==", - "dev": true, - "requires": { - "@ethereumjs/block": "^3.6.0", - "@ethereumjs/common": "^2.6.0", - "@ethereumjs/ethash": "^1.1.0", - "debug": "^2.2.0", - "ethereumjs-util": "^7.1.3", - "level-mem": "^5.0.1", - "lru-cache": "^5.1.1", - "semaphore-async-await": "^1.5.1" - }, - "dependencies": { - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ethereumjs-util": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", - "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", - "dev": true, - "requires": { - "@types/bn.js": "^5.1.0", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.2.4" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "@ethereumjs/common": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.6.0.tgz", - "integrity": "sha512-Cq2qS0FTu6O2VU1sgg+WyU9Ps0M6j/BEMHN+hRaECXCV/r0aI78u4N6p52QW/BDVhwWZpCdrvG8X7NJdzlpNUA==", - "dev": true, - "requires": { - "crc-32": "^1.2.0", - "ethereumjs-util": "^7.1.3" - }, - "dependencies": { - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "ethereumjs-util": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", - "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", - "dev": true, - "requires": { - "@types/bn.js": "^5.1.0", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.2.4" - } - } - } - }, - "@ethereumjs/ethash": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/ethash/-/ethash-1.1.0.tgz", - "integrity": "sha512-/U7UOKW6BzpA+Vt+kISAoeDie1vAvY4Zy2KF5JJb+So7+1yKmJeJEHOGSnQIj330e9Zyl3L5Nae6VZyh2TJnAA==", - "dev": true, - "requires": { - "@ethereumjs/block": "^3.5.0", - "@types/levelup": "^4.3.0", - "buffer-xor": "^2.0.1", - "ethereumjs-util": "^7.1.1", - "miller-rabin": "^4.0.0" - }, - "dependencies": { - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "buffer-xor": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-2.0.2.tgz", - "integrity": "sha512-eHslX0bin3GB+Lx2p7lEYRShRewuNZL3fUl4qlVJGGiwoPGftmt8JQgk2Y9Ji5/01TnVDo33E5b5O3vUB1HdqQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.1" - } - }, - "ethereumjs-util": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", - "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", - "dev": true, - "requires": { - "@types/bn.js": "^5.1.0", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.2.4" - } - } - } - }, - "@ethereumjs/tx": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.4.0.tgz", - "integrity": "sha512-WWUwg1PdjHKZZxPPo274ZuPsJCWV3SqATrEKQP1n2DrVYVP1aZIYpo/mFaA0BDoE0tIQmBeimRCEA0Lgil+yYw==", - "dev": true, - "requires": { - "@ethereumjs/common": "^2.6.0", - "ethereumjs-util": "^7.1.3" - }, - "dependencies": { - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "ethereumjs-util": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", - "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", - "dev": true, - "requires": { - "@types/bn.js": "^5.1.0", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.2.4" - } - } - } - }, - "@ethereumjs/vm": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/vm/-/vm-5.6.0.tgz", - "integrity": "sha512-J2m/OgjjiGdWF2P9bj/4LnZQ1zRoZhY8mRNVw/N3tXliGI8ai1sI1mlDPkLpeUUM4vq54gH6n0ZlSpz8U/qlYQ==", - "dev": true, - "requires": { - "@ethereumjs/block": "^3.6.0", - "@ethereumjs/blockchain": "^5.5.0", - "@ethereumjs/common": "^2.6.0", - "@ethereumjs/tx": "^3.4.0", - "async-eventemitter": "^0.2.4", - "core-js-pure": "^3.0.1", - "debug": "^2.2.0", - "ethereumjs-util": "^7.1.3", - "functional-red-black-tree": "^1.0.1", - "mcl-wasm": "^0.7.1", - "merkle-patricia-tree": "^4.2.2", - "rustbn.js": "~0.2.0" - }, - "dependencies": { - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ethereumjs-util": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", - "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", - "dev": true, - "requires": { - "@types/bn.js": "^5.1.0", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.2.4" - } - }, - "level-ws": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/level-ws/-/level-ws-2.0.0.tgz", - "integrity": "sha512-1iv7VXx0G9ec1isqQZ7y5LmoZo/ewAsyDHNA8EFDW5hqH2Kqovm33nSFkSdnLLAK+I5FlT+lo5Cw9itGe+CpQA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^3.1.0", - "xtend": "^4.0.1" - } - }, - "merkle-patricia-tree": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-4.2.2.tgz", - "integrity": "sha512-eqZYNTshcYx9aESkSPr71EqwsR/QmpnObDEV4iLxkt/x/IoLYZYjJvKY72voP/27Vy61iMOrfOG6jrn7ttXD+Q==", - "dev": true, - "requires": { - "@types/levelup": "^4.3.0", - "ethereumjs-util": "^7.1.2", - "level-mem": "^5.0.1", - "level-ws": "^2.0.0", - "readable-stream": "^3.6.0", - "rlp": "^2.2.4", - "semaphore-async-await": "^1.5.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "@ethersproject/abi": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.5.0.tgz", - "integrity": "sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w==", - "dev": true, - "requires": { - "@ethersproject/address": "^5.5.0", - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/constants": "^5.5.0", - "@ethersproject/hash": "^5.5.0", - "@ethersproject/keccak256": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/strings": "^5.5.0" - } - }, - "@ethersproject/abstract-provider": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz", - "integrity": "sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg==", - "dev": true, - "requires": { - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/networks": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/transactions": "^5.5.0", - "@ethersproject/web": "^5.5.0" - } - }, - "@ethersproject/abstract-signer": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz", - "integrity": "sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA==", - "dev": true, - "requires": { - "@ethersproject/abstract-provider": "^5.5.0", - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/properties": "^5.5.0" - } - }, - "@ethersproject/address": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.5.0.tgz", - "integrity": "sha512-l4Nj0eWlTUh6ro5IbPTgbpT4wRbdH5l8CQf7icF7sb/SI3Nhd9Y9HzhonTSTi6CefI0necIw7LJqQPopPLZyWw==", - "dev": true, - "requires": { - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/keccak256": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/rlp": "^5.5.0" - } - }, - "@ethersproject/base64": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.5.0.tgz", - "integrity": "sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0" - } - }, - "@ethersproject/basex": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.5.0.tgz", - "integrity": "sha512-ZIodwhHpVJ0Y3hUCfUucmxKsWQA5TMnavp5j/UOuDdzZWzJlRmuOjcTMIGgHCYuZmHt36BfiSyQPSRskPxbfaQ==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/properties": "^5.5.0" - } - }, - "@ethersproject/bignumber": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.5.0.tgz", - "integrity": "sha512-6Xytlwvy6Rn3U3gKEc1vP7nR92frHkv6wtVr95LFR3jREXiCPzdWxKQ1cx4JGQBXxcguAwjA8murlYN2TSiEbg==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "bn.js": "^4.11.9" - } - }, - "@ethersproject/bytes": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.5.0.tgz", - "integrity": "sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog==", - "dev": true, - "requires": { - "@ethersproject/logger": "^5.5.0" - } - }, - "@ethersproject/constants": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.5.0.tgz", - "integrity": "sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ==", - "dev": true, - "requires": { - "@ethersproject/bignumber": "^5.5.0" - } - }, - "@ethersproject/contracts": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.5.0.tgz", - "integrity": "sha512-2viY7NzyvJkh+Ug17v7g3/IJC8HqZBDcOjYARZLdzRxrfGlRgmYgl6xPRKVbEzy1dWKw/iv7chDcS83pg6cLxg==", - "dev": true, - "requires": { - "@ethersproject/abi": "^5.5.0", - "@ethersproject/abstract-provider": "^5.5.0", - "@ethersproject/abstract-signer": "^5.5.0", - "@ethersproject/address": "^5.5.0", - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/constants": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/transactions": "^5.5.0" - } - }, - "@ethersproject/hash": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.5.0.tgz", - "integrity": "sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg==", - "dev": true, - "requires": { - "@ethersproject/abstract-signer": "^5.5.0", - "@ethersproject/address": "^5.5.0", - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/keccak256": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/strings": "^5.5.0" - } - }, - "@ethersproject/hdnode": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.5.0.tgz", - "integrity": "sha512-mcSOo9zeUg1L0CoJH7zmxwUG5ggQHU1UrRf8jyTYy6HxdZV+r0PBoL1bxr+JHIPXRzS6u/UW4mEn43y0tmyF8Q==", - "dev": true, - "requires": { - "@ethersproject/abstract-signer": "^5.5.0", - "@ethersproject/basex": "^5.5.0", - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/pbkdf2": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/sha2": "^5.5.0", - "@ethersproject/signing-key": "^5.5.0", - "@ethersproject/strings": "^5.5.0", - "@ethersproject/transactions": "^5.5.0", - "@ethersproject/wordlists": "^5.5.0" - } - }, - "@ethersproject/json-wallets": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz", - "integrity": "sha512-9lA21XQnCdcS72xlBn1jfQdj2A1VUxZzOzi9UkNdnokNKke/9Ya2xA9aIK1SC3PQyBDLt4C+dfps7ULpkvKikQ==", - "dev": true, - "requires": { - "@ethersproject/abstract-signer": "^5.5.0", - "@ethersproject/address": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/hdnode": "^5.5.0", - "@ethersproject/keccak256": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/pbkdf2": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/random": "^5.5.0", - "@ethersproject/strings": "^5.5.0", - "@ethersproject/transactions": "^5.5.0", - "aes-js": "3.0.0", - "scrypt-js": "3.0.1" - }, - "dependencies": { - "aes-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", - "integrity": "sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0=", - "dev": true - } - } - }, - "@ethersproject/keccak256": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.5.0.tgz", - "integrity": "sha512-5VoFCTjo2rYbBe1l2f4mccaRFN/4VQEYFwwn04aJV2h7qf4ZvI2wFxUE1XOX+snbwCLRzIeikOqtAoPwMza9kg==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0", - "js-sha3": "0.8.0" - } - }, - "@ethersproject/logger": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.5.0.tgz", - "integrity": "sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg==", - "dev": true - }, - "@ethersproject/networks": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.5.2.tgz", - "integrity": "sha512-NEqPxbGBfy6O3x4ZTISb90SjEDkWYDUbEeIFhJly0F7sZjoQMnj5KYzMSkMkLKZ+1fGpx00EDpHQCy6PrDupkQ==", - "dev": true, - "requires": { - "@ethersproject/logger": "^5.5.0" - } - }, - "@ethersproject/pbkdf2": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.5.0.tgz", - "integrity": "sha512-SaDvQFvXPnz1QGpzr6/HToLifftSXGoXrbpZ6BvoZhmx4bNLHrxDe8MZisuecyOziP1aVEwzC2Hasj+86TgWVg==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/sha2": "^5.5.0" - } - }, - "@ethersproject/properties": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.5.0.tgz", - "integrity": "sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA==", - "dev": true, - "requires": { - "@ethersproject/logger": "^5.5.0" - } - }, - "@ethersproject/providers": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.5.2.tgz", - "integrity": "sha512-hkbx7x/MKcRjyrO4StKXCzCpWer6s97xnm34xkfPiarhtEUVAN4TBBpamM+z66WcTt7H5B53YwbRj1n7i8pZoQ==", - "dev": true, - "requires": { - "@ethersproject/abstract-provider": "^5.5.0", - "@ethersproject/abstract-signer": "^5.5.0", - "@ethersproject/address": "^5.5.0", - "@ethersproject/basex": "^5.5.0", - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/constants": "^5.5.0", - "@ethersproject/hash": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/networks": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/random": "^5.5.0", - "@ethersproject/rlp": "^5.5.0", - "@ethersproject/sha2": "^5.5.0", - "@ethersproject/strings": "^5.5.0", - "@ethersproject/transactions": "^5.5.0", - "@ethersproject/web": "^5.5.0", - "bech32": "1.1.4", - "ws": "7.4.6" - }, - "dependencies": { - "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "dev": true, - "requires": {} - } - } - }, - "@ethersproject/random": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.5.1.tgz", - "integrity": "sha512-YaU2dQ7DuhL5Au7KbcQLHxcRHfgyNgvFV4sQOo0HrtW3Zkrc9ctWNz8wXQ4uCSfSDsqX2vcjhroxU5RQRV0nqA==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/logger": "^5.5.0" - } - }, - "@ethersproject/rlp": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.5.0.tgz", - "integrity": "sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/logger": "^5.5.0" - } - }, - "@ethersproject/sha2": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.5.0.tgz", - "integrity": "sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "hash.js": "1.1.7" - } - }, - "@ethersproject/signing-key": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.5.0.tgz", - "integrity": "sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "bn.js": "^4.11.9", - "elliptic": "6.5.4", - "hash.js": "1.1.7" - } - }, - "@ethersproject/solidity": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.5.0.tgz", - "integrity": "sha512-9NgZs9LhGMj6aCtHXhtmFQ4AN4sth5HuFXVvAQtzmm0jpSCNOTGtrHZJAeYTh7MBjRR8brylWZxBZR9zDStXbw==", - "dev": true, - "requires": { - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/keccak256": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/sha2": "^5.5.0", - "@ethersproject/strings": "^5.5.0" - } - }, - "@ethersproject/strings": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.5.0.tgz", - "integrity": "sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/constants": "^5.5.0", - "@ethersproject/logger": "^5.5.0" - } - }, - "@ethersproject/transactions": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.5.0.tgz", - "integrity": "sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA==", - "dev": true, - "requires": { - "@ethersproject/address": "^5.5.0", - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/constants": "^5.5.0", - "@ethersproject/keccak256": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/rlp": "^5.5.0", - "@ethersproject/signing-key": "^5.5.0" - } - }, - "@ethersproject/units": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.5.0.tgz", - "integrity": "sha512-7+DpjiZk4v6wrikj+TCyWWa9dXLNU73tSTa7n0TSJDxkYbV3Yf1eRh9ToMLlZtuctNYu9RDNNy2USq3AdqSbag==", - "dev": true, - "requires": { - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/constants": "^5.5.0", - "@ethersproject/logger": "^5.5.0" - } - }, - "@ethersproject/wallet": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.5.0.tgz", - "integrity": "sha512-Mlu13hIctSYaZmUOo7r2PhNSd8eaMPVXe1wxrz4w4FCE4tDYBywDH+bAR1Xz2ADyXGwqYMwstzTrtUVIsKDO0Q==", - "dev": true, - "requires": { - "@ethersproject/abstract-provider": "^5.5.0", - "@ethersproject/abstract-signer": "^5.5.0", - "@ethersproject/address": "^5.5.0", - "@ethersproject/bignumber": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/hash": "^5.5.0", - "@ethersproject/hdnode": "^5.5.0", - "@ethersproject/json-wallets": "^5.5.0", - "@ethersproject/keccak256": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/random": "^5.5.0", - "@ethersproject/signing-key": "^5.5.0", - "@ethersproject/transactions": "^5.5.0", - "@ethersproject/wordlists": "^5.5.0" - } - }, - "@ethersproject/web": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.5.1.tgz", - "integrity": "sha512-olvLvc1CB12sREc1ROPSHTdFCdvMh0J5GSJYiQg2D0hdD4QmJDy8QYDb1CvoqD/bF1c++aeKv2sR5uduuG9dQg==", - "dev": true, - "requires": { - "@ethersproject/base64": "^5.5.0", - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/strings": "^5.5.0" - } - }, - "@ethersproject/wordlists": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.5.0.tgz", - "integrity": "sha512-bL0UTReWDiaQJJYOC9sh/XcRu/9i2jMrzf8VLRmPKx58ckSlOJiohODkECCO50dtLZHcGU6MLXQ4OOrgBwP77Q==", - "dev": true, - "requires": { - "@ethersproject/bytes": "^5.5.0", - "@ethersproject/hash": "^5.5.0", - "@ethersproject/logger": "^5.5.0", - "@ethersproject/properties": "^5.5.0", - "@ethersproject/strings": "^5.5.0" - } - }, - "@nomiclabs/hardhat-ethers": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.4.tgz", - "integrity": "sha512-7LMR344TkdCYkMVF9LuC9VU2NBIi84akQiwqm7OufpWaDgHbWhuanY53rk3SVAW0E4HBk5xn5wl5+bN5f+Mq5w==", - "dev": true, - "requires": {} - }, - "@nomiclabs/hardhat-etherscan": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-2.1.8.tgz", - "integrity": "sha512-0+rj0SsZotVOcTLyDOxnOc3Gulo8upo0rsw/h+gBPcmtj91YqYJNhdARHoBxOhhE8z+5IUQPx+Dii04lXT14PA==", - "dev": true, - "requires": { - "@ethersproject/abi": "^5.1.2", - "@ethersproject/address": "^5.0.2", - "cbor": "^5.0.2", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "node-fetch": "^2.6.0", - "semver": "^6.3.0" - } - }, - "@nomiclabs/hardhat-waffle": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.1.tgz", - "integrity": "sha512-2YR2V5zTiztSH9n8BYWgtv3Q+EL0N5Ltm1PAr5z20uAY4SkkfylJ98CIqt18XFvxTD5x4K2wKBzddjV9ViDAZQ==", - "dev": true, - "requires": { - "@types/sinon-chai": "^3.2.3", - "@types/web3": "1.0.19" - } - }, - "@openzeppelin/contracts": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-3.4.2.tgz", - "integrity": "sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA==", - "dev": true - }, - "@resolver-engine/core": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@resolver-engine/core/-/core-0.3.3.tgz", - "integrity": "sha512-eB8nEbKDJJBi5p5SrvrvILn4a0h42bKtbCTri3ZxCGt6UvoQyp7HnGOfki944bUjBSHKK3RvgfViHn+kqdXtnQ==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "is-url": "^1.2.4", - "request": "^2.85.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@resolver-engine/fs": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@resolver-engine/fs/-/fs-0.3.3.tgz", - "integrity": "sha512-wQ9RhPUcny02Wm0IuJwYMyAG8fXVeKdmhm8xizNByD4ryZlx6PP6kRen+t/haF43cMfmaV7T3Cx6ChOdHEhFUQ==", - "dev": true, - "requires": { - "@resolver-engine/core": "^0.3.3", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@resolver-engine/imports": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@resolver-engine/imports/-/imports-0.3.3.tgz", - "integrity": "sha512-anHpS4wN4sRMwsAbMXhMfOD/y4a4Oo0Cw/5+rue7hSwGWsDOQaAU1ClK1OxjUC35/peazxEl8JaSRRS+Xb8t3Q==", - "dev": true, - "requires": { - "@resolver-engine/core": "^0.3.3", - "debug": "^3.1.0", - "hosted-git-info": "^2.6.0", - "path-browserify": "^1.0.0", - "url": "^0.11.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@resolver-engine/imports-fs": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@resolver-engine/imports-fs/-/imports-fs-0.3.3.tgz", - "integrity": "sha512-7Pjg/ZAZtxpeyCFlZR5zqYkz+Wdo84ugB5LApwriT8XFeQoLwGUj4tZFFvvCuxaNCcqZzCYbonJgmGObYBzyCA==", - "dev": true, - "requires": { - "@resolver-engine/fs": "^0.3.3", - "@resolver-engine/imports": "^0.3.3", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@sentry/core": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz", - "integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==", - "dev": true, - "requires": { - "@sentry/hub": "5.30.0", - "@sentry/minimal": "5.30.0", - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "tslib": "^1.9.3" - } - }, - "@sentry/hub": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz", - "integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==", - "dev": true, - "requires": { - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "tslib": "^1.9.3" - } - }, - "@sentry/minimal": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz", - "integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==", - "dev": true, - "requires": { - "@sentry/hub": "5.30.0", - "@sentry/types": "5.30.0", - "tslib": "^1.9.3" - } - }, - "@sentry/node": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.30.0.tgz", - "integrity": "sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==", - "dev": true, - "requires": { - "@sentry/core": "5.30.0", - "@sentry/hub": "5.30.0", - "@sentry/tracing": "5.30.0", - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "cookie": "^0.4.1", - "https-proxy-agent": "^5.0.0", - "lru_map": "^0.3.3", - "tslib": "^1.9.3" - } - }, - "@sentry/tracing": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz", - "integrity": "sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==", - "dev": true, - "requires": { - "@sentry/hub": "5.30.0", - "@sentry/minimal": "5.30.0", - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "tslib": "^1.9.3" - } - }, - "@sentry/types": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", - "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==", - "dev": true - }, - "@sentry/utils": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", - "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", - "dev": true, - "requires": { - "@sentry/types": "5.30.0", - "tslib": "^1.9.3" - } - }, - "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", - "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@solidity-parser/parser": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.0.tgz", - "integrity": "sha512-cX0JJRcmPtNUJpzD2K7FdA7qQsTOk1UZnFx2k7qAg9ZRvuaH5NBe5IEdBMXGlmf2+FmjhqbygJ26H8l2SV7aKQ==", - "dev": true, - "requires": { - "antlr4ts": "^0.5.0-alpha.4" - } - }, - "@truffle/hdwallet-provider": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@truffle/hdwallet-provider/-/hdwallet-provider-1.4.3.tgz", - "integrity": "sha512-Oo8ORAQLfcbLYp6HwG1mpOx6IpVkHv8IkKy25LZUN5Q5bCCqxdlMF0F7CnSXPBdQ+UqZY9+RthC0VrXv9gXiPQ==", - "requires": { - "@trufflesuite/web3-provider-engine": "15.0.13-1", - "ethereum-cryptography": "^0.1.3", - "ethereum-protocol": "^1.0.1", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.2", - "ethereumjs-util": "^6.1.0", - "ethereumjs-wallet": "^1.0.1" - } - }, - "@trufflesuite/eth-json-rpc-filters": { - "version": "4.1.2-1", - "resolved": "https://registry.npmjs.org/@trufflesuite/eth-json-rpc-filters/-/eth-json-rpc-filters-4.1.2-1.tgz", - "integrity": "sha512-/MChvC5dw2ck9NU1cZmdovCz2VKbOeIyR4tcxDvA5sT+NaL0rA2/R5U0yI7zsbo1zD+pgqav77rQHTzpUdDNJQ==", - "requires": { - "@trufflesuite/eth-json-rpc-middleware": "^4.4.2-0", - "await-semaphore": "^0.1.3", - "eth-query": "^2.1.2", - "json-rpc-engine": "^5.1.3", - "lodash.flatmap": "^4.5.0", - "safe-event-emitter": "^1.0.1" - } - }, - "@trufflesuite/eth-json-rpc-infura": { - "version": "4.0.3-0", - "resolved": "https://registry.npmjs.org/@trufflesuite/eth-json-rpc-infura/-/eth-json-rpc-infura-4.0.3-0.tgz", - "integrity": "sha512-xaUanOmo0YLqRsL0SfXpFienhdw5bpQ1WEXxMTRi57az4lwpZBv4tFUDvcerdwJrxX9wQqNmgUgd1BrR01dumw==", - "requires": { - "@trufflesuite/eth-json-rpc-middleware": "^4.4.2-1", - "cross-fetch": "^2.1.1", - "eth-json-rpc-errors": "^1.0.1", - "json-rpc-engine": "^5.1.3" - }, - "dependencies": { - "eth-json-rpc-errors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/eth-json-rpc-errors/-/eth-json-rpc-errors-1.1.1.tgz", - "integrity": "sha512-WT5shJ5KfNqHi9jOZD+ID8I1kuYWNrigtZat7GOQkvwo99f8SzAVaEcWhJUv656WiZOAg3P1RiJQANtUmDmbIg==", - "requires": { - "fast-safe-stringify": "^2.0.6" - } - } - } - }, - "@trufflesuite/eth-json-rpc-middleware": { - "version": "4.4.2-1", - "resolved": "https://registry.npmjs.org/@trufflesuite/eth-json-rpc-middleware/-/eth-json-rpc-middleware-4.4.2-1.tgz", - "integrity": "sha512-iEy9H8ja7/8aYES5HfrepGBKU9n/Y4OabBJEklVd/zIBlhCCBAWBqkIZgXt11nBXO/rYAeKwYuE3puH3ByYnLA==", - "requires": { - "@trufflesuite/eth-sig-util": "^1.4.2", - "btoa": "^1.2.1", - "clone": "^2.1.1", - "eth-json-rpc-errors": "^1.0.1", - "eth-query": "^2.1.2", - "ethereumjs-block": "^1.6.0", - "ethereumjs-tx": "^1.3.7", - "ethereumjs-util": "^5.1.2", - "ethereumjs-vm": "^2.6.0", - "fetch-ponyfill": "^4.0.0", - "json-rpc-engine": "^5.1.3", - "json-stable-stringify": "^1.0.1", - "pify": "^3.0.0", - "safe-event-emitter": "^1.0.1" - }, - "dependencies": { - "eth-json-rpc-errors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/eth-json-rpc-errors/-/eth-json-rpc-errors-1.1.1.tgz", - "integrity": "sha512-WT5shJ5KfNqHi9jOZD+ID8I1kuYWNrigtZat7GOQkvwo99f8SzAVaEcWhJUv656WiZOAg3P1RiJQANtUmDmbIg==", - "requires": { - "fast-safe-stringify": "^2.0.6" - } - }, - "ethereum-common": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", - "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" - }, - "ethereumjs-tx": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", - "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==", - "requires": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" - } - }, - "ethereumjs-util": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", - "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "@trufflesuite/eth-sig-util": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@trufflesuite/eth-sig-util/-/eth-sig-util-1.4.2.tgz", - "integrity": "sha512-+GyfN6b0LNW77hbQlH3ufZ/1eCON7mMrGym6tdYf7xiNw9Vv3jBO72bmmos1EId2NgBvPMhmYYm6DSLQFTmzrA==", - "requires": { - "ethereumjs-abi": "^0.6.8", - "ethereumjs-util": "^5.1.1" - }, - "dependencies": { - "ethereumjs-util": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", - "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "@trufflesuite/web3-provider-engine": { - "version": "15.0.13-1", - "resolved": "https://registry.npmjs.org/@trufflesuite/web3-provider-engine/-/web3-provider-engine-15.0.13-1.tgz", - "integrity": "sha512-6u3x/iIN5fyj8pib5QTUDmIOUiwAGhaqdSTXdqCu6v9zo2BEwdCqgEJd1uXDh3DBmPRDfiZ/ge8oUPy7LerpHg==", - "requires": { - "@trufflesuite/eth-json-rpc-filters": "^4.1.2-1", - "@trufflesuite/eth-json-rpc-infura": "^4.0.3-0", - "@trufflesuite/eth-json-rpc-middleware": "^4.4.2-1", - "@trufflesuite/eth-sig-util": "^1.4.2", - "async": "^2.5.0", - "backoff": "^2.5.0", - "clone": "^2.0.0", - "cross-fetch": "^2.1.0", - "eth-block-tracker": "^4.4.2", - "eth-json-rpc-errors": "^2.0.2", - "ethereumjs-block": "^1.2.2", - "ethereumjs-tx": "^1.2.0", - "ethereumjs-util": "^5.1.5", - "ethereumjs-vm": "^2.3.4", - "json-stable-stringify": "^1.0.1", - "promise-to-callback": "^1.0.0", - "readable-stream": "^2.2.9", - "request": "^2.85.0", - "semaphore": "^1.0.3", - "ws": "^5.1.1", - "xhr": "^2.2.0", - "xtend": "^4.0.1" - }, - "dependencies": { - "ethereum-common": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", - "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" - }, - "ethereumjs-tx": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", - "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==", - "requires": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" - } - }, - "ethereumjs-util": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", - "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "@typechain/ethers-v5": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-2.0.0.tgz", - "integrity": "sha512-0xdCkyGOzdqh4h5JSf+zoWx85IusEjDcPIwNEHP8mrWSnCae4rvrqB+/gtpdNfX7zjlFlZiMeePn2r63EI3Lrw==", - "dev": true, - "requires": { - "ethers": "^5.0.2" - } - }, - "@types/abstract-leveldown": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz", - "integrity": "sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ==", - "dev": true - }, - "@types/bn.js": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.0.tgz", - "integrity": "sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA==", - "requires": { - "@types/node": "*" - } - }, - "@types/chai": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", - "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", - "dev": true - }, - "@types/level-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/level-errors/-/level-errors-3.0.0.tgz", - "integrity": "sha512-/lMtoq/Cf/2DVOm6zE6ORyOM+3ZVm/BvzEZVxUhf6bgh8ZHglXlBqxbxSlJeVp8FCbD3IVvk/VbsaNmDjrQvqQ==", - "dev": true - }, - "@types/levelup": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/levelup/-/levelup-4.3.3.tgz", - "integrity": "sha512-K+OTIjJcZHVlZQN1HmU64VtrC0jC3dXWQozuEIR9zVvltIk90zaGPM2AgT+fIkChpzHhFE3YnvFLCbLtzAmexA==", - "dev": true, - "requires": { - "@types/abstract-leveldown": "*", - "@types/level-errors": "*", - "@types/node": "*" - } - }, - "@types/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==", - "dev": true - }, - "@types/mkdirp": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.5.2.tgz", - "integrity": "sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.10.tgz", - "integrity": "sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==" - }, - "@types/node-fetch": { - "version": "2.5.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", - "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", - "dev": true, - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "@types/pbkdf2": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz", - "integrity": "sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/prettier": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.3.tgz", - "integrity": "sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w==", - "dev": true - }, - "@types/resolve": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", - "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/secp256k1": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.3.tgz", - "integrity": "sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==", - "requires": { - "@types/node": "*" - } - }, - "@types/sinon": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.6.tgz", - "integrity": "sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==", - "dev": true, - "requires": { - "@sinonjs/fake-timers": "^7.1.0" - } - }, - "@types/sinon-chai": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.8.tgz", - "integrity": "sha512-d4ImIQbT/rKMG8+AXpmcan5T2/PNeSjrYhvkwet6z0p8kzYtfgA32xzOBlbU0yqJfq+/0Ml805iFoODO0LP5/g==", - "dev": true, - "requires": { - "@types/chai": "*", - "@types/sinon": "*" - } - }, - "@types/underscore": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.4.tgz", - "integrity": "sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg==", - "dev": true - }, - "@types/web3": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/@types/web3/-/web3-1.0.19.tgz", - "integrity": "sha512-fhZ9DyvDYDwHZUp5/STa9XW2re0E8GxoioYJ4pEUZ13YHpApSagixj7IAdoYH5uAK+UalGq6Ml8LYzmgRA/q+A==", - "dev": true, - "requires": { - "@types/bn.js": "*", - "@types/underscore": "*" - } - }, - "@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "abstract-leveldown": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz", - "integrity": "sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "immediate": "^3.2.3", - "level-concat-iterator": "~2.0.0", - "level-supports": "~1.0.0", - "xtend": "~4.0.0" - } - }, - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "adm-zip": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", - "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", - "dev": true - }, - "aes-js": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", - "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - } - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "antlr4": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.7.1.tgz", - "integrity": "sha512-haHyTW7Y9joE5MVs37P2lNYfU2RWBLfcRDD8OWldcdZm5TiCE91B5Xl1oWSwiDUSd4rlExpt2pu1fksYQjRBYQ==", - "dev": true - }, - "antlr4ts": { - "version": "0.5.0-alpha.4", - "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", - "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", - "dev": true - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "requires": { - "typical": "^2.6.1" - } - }, - "asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "ast-parents": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/ast-parents/-/ast-parents-0.0.1.tgz", - "integrity": "sha1-UI/Q8F0MSHddnszaLhdEIyYejdM=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "requires": { - "lodash": "^4.17.14" - } - }, - "async-eventemitter": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/async-eventemitter/-/async-eventemitter-0.2.4.tgz", - "integrity": "sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==", - "requires": { - "async": "^2.4.0" - } - }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "await-semaphore": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/await-semaphore/-/await-semaphore-0.1.3.tgz", - "integrity": "sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" - }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dev": true, - "requires": { - "follow-redirects": "^1.14.0" - } - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", - "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", - "requires": { - "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.3.1", - "semver": "^6.1.1" - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.1.tgz", - "integrity": "sha512-TihqEe4sQcb/QcPJvxe94/9RZuLQuF1+To4WqQcRvc+3J3gLCPIPgDKzGLG6zmQLfH3nn25heRuDNkS2KR4I8A==", - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1", - "core-js-compat": "^3.20.0" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", - "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1" - } - }, - "backoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", - "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", - "requires": { - "precond": "0.2" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - }, - "dependencies": { - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - } - } - }, - "bech32": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", - "dev": true - }, - "bignumber.js": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", - "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "blakejs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.1.tgz", - "integrity": "sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg==" - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", - "requires": { - "base-x": "^3.0.2" - } - }, - "bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", - "requires": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" - } - }, - "btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "bytes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", - "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==", - "dev": true - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001300", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001300.tgz", - "integrity": "sha512-cVjiJHWGcNlJi8TZVKNMnvMid3Z3TTdDHmLDzlOdIiZq138Exvo0G+G0wTdVYolxKb4AYwC+38pxodiInVtJSA==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "cbor": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", - "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", - "dev": true, - "requires": { - "bignumber.js": "^9.0.1", - "nofilter": "^1.0.4" - } - }, - "chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "checkpoint-store": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/checkpoint-store/-/checkpoint-store-1.1.0.tgz", - "integrity": "sha1-BOTLUWuRQziTWB5tRgGnjpVS6gY=", - "requires": { - "functional-red-black-tree": "^1.0.1" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "circular": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/circular/-/circular-1.0.5.tgz", - "integrity": "sha1-fad6+Yu96c5LWzWM1Va13e0tMUk=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-logger": { - "version": "0.5.40", - "resolved": "https://registry.npmjs.org/cli-logger/-/cli-logger-0.5.40.tgz", - "integrity": "sha1-CX8OEbByx8aYomxH9YiinCC0iws=", - "dev": true, - "requires": { - "circular": "^1.0.5", - "cli-util": "~1.1.27" - } - }, - "cli-regexp": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/cli-regexp/-/cli-regexp-0.1.2.tgz", - "integrity": "sha1-a82TsJ+y7RAl0woRVdWZeVSlNRI=", - "dev": true - }, - "cli-util": { - "version": "1.1.27", - "resolved": "https://registry.npmjs.org/cli-util/-/cli-util-1.1.27.tgz", - "integrity": "sha1-QtaeNqBAoyH8nPhRwVE8rcUJMFQ=", - "dev": true, - "requires": { - "cli-regexp": "~0.1.0" - } - }, - "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "command-exists": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", - "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "dev": true - }, - "command-line-args": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-4.0.7.tgz", - "integrity": "sha512-aUdPvQRAyBvQd2n7jXcsMDz68ckBJELXNzBybCHOibUWEg0mWTnaYCSRU8h9R+aNRSvDihJtssSRCiDRpLaezA==", - "dev": true, - "requires": { - "array-back": "^2.0.0", - "find-replace": "^1.0.3", - "typical": "^2.6.1" - } - }, - "commander": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", - "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "peer": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "dev": true - }, - "core-js-compat": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.20.3.tgz", - "integrity": "sha512-c8M5h0IkNZ+I92QhIpuSijOxGAcj3lgpsWdkCqmUTZNwidujF4r3pi6x1DCN+Vcs5qTS2XWWMfWSuCqyupX8gw==", - "requires": { - "browserslist": "^4.19.1", - "semver": "7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==" - } - } - }, - "core-js-pure": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.20.3.tgz", - "integrity": "sha512-Q2H6tQ5MtPtcC7f3HxJ48i4Q7T9ybPKgvWyuH7JXIoNa2pm0KuBnycsET/qw1SLLZYfbsbrZQNMeIOClb+6WIA==", - "dev": true - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "dependencies": { - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - } - } - }, - "crc-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", - "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", - "dev": true, - "requires": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-fetch": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.5.tgz", - "integrity": "sha512-xqYAhQb4NhCJSRym03dwxpP1bYXpK3y7UN83Bo2WFi3x1Zmzn0SL/6xGoPr+gpt4WmNrgCCX3HPysvOwFOW36w==", - "requires": { - "node-fetch": "2.6.1", - "whatwg-fetch": "2.0.4" - }, - "dependencies": { - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" - } - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "requires": { - "ms": "2.1.2" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "deferred-leveldown": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", - "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", - "dev": true, - "requires": { - "abstract-leveldown": "~6.2.1", - "inherits": "^2.0.3" - }, - "dependencies": { - "abstract-leveldown": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", - "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "immediate": "^3.2.3", - "level-concat-iterator": "~2.0.0", - "level-supports": "~1.0.0", - "xtend": "~4.0.0" - } - } - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "delay": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", - "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-walk": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "electron-to-chromium": { - "version": "1.4.48", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.48.tgz", - "integrity": "sha512-RT3SEmpv7XUA+tKXrZGudAWLDpa7f8qmhjcLaM6OD/ERxjQ/zAojT8/Vvo0BSzbArkElFZ1WyZ9FuwAYbkdBNA==" - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "requires": { - "iconv-lite": "^0.6.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "encoding-down": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", - "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", - "dev": true, - "requires": { - "abstract-leveldown": "^6.2.1", - "inherits": "^2.0.3", - "level-codec": "^9.0.0", - "level-errors": "^2.0.0" - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true - }, - "errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "requires": { - "prr": "~1.0.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - } - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.9.1", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.1", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.11", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0" - }, - "dependencies": { - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - }, - "espree": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", - "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", - "dev": true, - "requires": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "eth-block-tracker": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/eth-block-tracker/-/eth-block-tracker-4.4.3.tgz", - "integrity": "sha512-A8tG4Z4iNg4mw5tP1Vung9N9IjgMNqpiMoJ/FouSFwNCGHv2X0mmOYwtQOJzki6XN7r7Tyo01S29p7b224I4jw==", - "requires": { - "@babel/plugin-transform-runtime": "^7.5.5", - "@babel/runtime": "^7.5.5", - "eth-query": "^2.1.0", - "json-rpc-random-id": "^1.0.1", - "pify": "^3.0.0", - "safe-event-emitter": "^1.0.1" - } - }, - "eth-ens-namehash": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz", - "integrity": "sha1-IprEbsqG1S4MmR58sq74P/D2i88=", - "dev": true, - "requires": { - "idna-uts46-hx": "^2.3.1", - "js-sha3": "^0.5.7" - }, - "dependencies": { - "js-sha3": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", - "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=", - "dev": true - } - } - }, - "eth-json-rpc-errors": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eth-json-rpc-errors/-/eth-json-rpc-errors-2.0.2.tgz", - "integrity": "sha512-uBCRM2w2ewusRHGxN8JhcuOb2RN3ueAOYH/0BhqdFmQkZx5lj5+fLKTz0mIVOzd4FG5/kUksCzCD7eTEim6gaA==", - "requires": { - "fast-safe-stringify": "^2.0.6" - } - }, - "eth-query": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/eth-query/-/eth-query-2.1.2.tgz", - "integrity": "sha1-1nQdkAAQa1FRDHLbktY2VFam2l4=", - "requires": { - "json-rpc-random-id": "^1.0.0", - "xtend": "^4.0.1" - } - }, - "eth-rpc-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eth-rpc-errors/-/eth-rpc-errors-3.0.0.tgz", - "integrity": "sha512-iPPNHPrLwUlR9xCSYm7HHQjWBasor3+KZfRvwEWxMz3ca0yqnlBeJrnyphkGIXZ4J7AMAaOLmwy4AWhnxOiLxg==", - "requires": { - "fast-safe-stringify": "^2.0.6" - } - }, - "eth-sig-util": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.5.4.tgz", - "integrity": "sha512-aCMBwp8q/4wrW4QLsF/HYBOSA7TpLKmkVwP3pYQNkEEseW2Rr8Z5Uxc9/h6HX+OG3tuHo+2bINVSihIeBfym6A==", - "dev": true, - "requires": { - "ethereumjs-abi": "0.6.8", - "ethereumjs-util": "^5.1.1", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.0" - }, - "dependencies": { - "ethereumjs-util": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", - "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "ethereum-bloom-filters": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.10.tgz", - "integrity": "sha512-rxJ5OFN3RwjQxDcFP2Z5+Q9ho4eIdEmSc2ht0fCu8Se9nbXjZ7/031uXoUYJ87KHCOdVeiUuwSnoS7hmYAGVHA==", - "dev": true, - "requires": { - "js-sha3": "^0.8.0" - } - }, - "ethereum-common": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.2.0.tgz", - "integrity": "sha512-XOnAR/3rntJgbCdGhqdaLIxDLWKLmsZOGhHdBKadEr6gEnJLH52k93Ou+TUdFaPN3hJc3isBZBal3U/XZ15abA==" - }, - "ethereum-cryptography": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz", - "integrity": "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==", - "requires": { - "@types/pbkdf2": "^3.0.0", - "@types/secp256k1": "^4.0.1", - "blakejs": "^1.1.0", - "browserify-aes": "^1.2.0", - "bs58check": "^2.1.2", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "hash.js": "^1.1.7", - "keccak": "^3.0.0", - "pbkdf2": "^3.0.17", - "randombytes": "^2.1.0", - "safe-buffer": "^5.1.2", - "scrypt-js": "^3.0.0", - "secp256k1": "^4.0.1", - "setimmediate": "^1.0.5" - } - }, - "ethereum-protocol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ethereum-protocol/-/ethereum-protocol-1.0.1.tgz", - "integrity": "sha512-3KLX1mHuEsBW0dKG+c6EOJS1NBNqdCICvZW9sInmZTt5aY0oxmHVggYRE0lJu1tcnMD1K+AKHdLi6U43Awm1Vg==" - }, - "ethereum-waffle": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ethereum-waffle/-/ethereum-waffle-3.4.0.tgz", - "integrity": "sha512-ADBqZCkoSA5Isk486ntKJVjFEawIiC+3HxNqpJqONvh3YXBTNiRfXvJtGuAFLXPG91QaqkGqILEHANAo7j/olQ==", - "dev": true, - "requires": { - "@ethereum-waffle/chai": "^3.4.0", - "@ethereum-waffle/compiler": "^3.4.0", - "@ethereum-waffle/mock-contract": "^3.3.0", - "@ethereum-waffle/provider": "^3.4.0", - "ethers": "^5.0.1" - } - }, - "ethereumjs-abi": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz", - "integrity": "sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA==", - "requires": { - "bn.js": "^4.11.8", - "ethereumjs-util": "^6.0.0" - } - }, - "ethereumjs-account": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/ethereumjs-account/-/ethereumjs-account-2.0.5.tgz", - "integrity": "sha512-bgDojnXGjhMwo6eXQC0bY6UK2liSFUSMwwylOmQvZbSl/D7NXQ3+vrGO46ZeOgjGfxXmgIeVNDIiHw7fNZM4VA==", - "requires": { - "ethereumjs-util": "^5.0.0", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - }, - "dependencies": { - "ethereumjs-util": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", - "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "ethereumjs-block": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-1.7.1.tgz", - "integrity": "sha512-B+sSdtqm78fmKkBq78/QLKJbu/4Ts4P2KFISdgcuZUPDm9x+N7qgBPIIFUGbaakQh8bzuquiRVbdmvPKqbILRg==", - "requires": { - "async": "^2.0.1", - "ethereum-common": "0.2.0", - "ethereumjs-tx": "^1.2.2", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - }, - "dependencies": { - "ethereumjs-tx": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", - "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==", - "requires": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" - }, - "dependencies": { - "ethereum-common": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", - "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" - } - } - }, - "ethereumjs-util": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", - "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "ethereumjs-common": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/ethereumjs-common/-/ethereumjs-common-1.5.2.tgz", - "integrity": "sha512-hTfZjwGX52GS2jcVO6E2sx4YuFnf0Fhp5ylo4pEPhEffNln7vS59Hr5sLnp3/QCazFLluuBZ+FZ6J5HTp0EqCA==" - }, - "ethereumjs-tx": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-2.1.2.tgz", - "integrity": "sha512-zZEK1onCeiORb0wyCXUvg94Ve5It/K6GD1K+26KfFKodiBiS6d9lfCXlUKGBBdQ+bv7Day+JK0tj1K+BeNFRAw==", - "requires": { - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.0.0" - } - }, - "ethereumjs-util": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz", - "integrity": "sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==", - "requires": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" - }, - "dependencies": { - "@types/bn.js": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", - "requires": { - "@types/node": "*" - } - } - } - }, - "ethereumjs-vm": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.6.0.tgz", - "integrity": "sha512-r/XIUik/ynGbxS3y+mvGnbOKnuLo40V5Mj1J25+HEO63aWYREIqvWeRO/hnROlMBE5WoniQmPmhiaN0ctiHaXw==", - "requires": { - "async": "^2.1.2", - "async-eventemitter": "^0.2.2", - "ethereumjs-account": "^2.0.3", - "ethereumjs-block": "~2.2.0", - "ethereumjs-common": "^1.1.0", - "ethereumjs-util": "^6.0.0", - "fake-merkle-patricia-tree": "^1.0.1", - "functional-red-black-tree": "^1.0.1", - "merkle-patricia-tree": "^2.3.2", - "rustbn.js": "~0.2.0", - "safe-buffer": "^5.1.1" - }, - "dependencies": { - "ethereumjs-block": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-2.2.2.tgz", - "integrity": "sha512-2p49ifhek3h2zeg/+da6XpdFR3GlqY3BIEiqxGF8j9aSRIgkb7M1Ky+yULBKJOu8PAZxfhsYA+HxUk2aCQp3vg==", - "requires": { - "async": "^2.0.1", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.1", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - }, - "dependencies": { - "ethereumjs-util": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", - "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - } - } - }, - "ethereumjs-wallet": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-1.0.2.tgz", - "integrity": "sha512-CCWV4RESJgRdHIvFciVQFnCHfqyhXWchTPlkfp28Qc53ufs+doi5I/cV2+xeK9+qEo25XCWfP9MiL+WEPAZfdA==", - "requires": { - "aes-js": "^3.1.2", - "bs58check": "^2.1.2", - "ethereum-cryptography": "^0.1.3", - "ethereumjs-util": "^7.1.2", - "randombytes": "^2.1.0", - "scrypt-js": "^3.0.1", - "utf8": "^3.0.0", - "uuid": "^8.3.2" - }, - "dependencies": { - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" - }, - "ethereumjs-util": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", - "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", - "requires": { - "@types/bn.js": "^5.1.0", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.2.4" - } - } - } - }, - "ethers": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.5.3.tgz", - "integrity": "sha512-fTT4WT8/hTe/BLwRUtl7I5zlpF3XC3P/Xwqxc5AIP2HGlH15qpmjs0Ou78az93b1rLITzXLFxoNX63B8ZbUd7g==", - "dev": true, - "requires": { - "@ethersproject/abi": "5.5.0", - "@ethersproject/abstract-provider": "5.5.1", - "@ethersproject/abstract-signer": "5.5.0", - "@ethersproject/address": "5.5.0", - "@ethersproject/base64": "5.5.0", - "@ethersproject/basex": "5.5.0", - "@ethersproject/bignumber": "5.5.0", - "@ethersproject/bytes": "5.5.0", - "@ethersproject/constants": "5.5.0", - "@ethersproject/contracts": "5.5.0", - "@ethersproject/hash": "5.5.0", - "@ethersproject/hdnode": "5.5.0", - "@ethersproject/json-wallets": "5.5.0", - "@ethersproject/keccak256": "5.5.0", - "@ethersproject/logger": "5.5.0", - "@ethersproject/networks": "5.5.2", - "@ethersproject/pbkdf2": "5.5.0", - "@ethersproject/properties": "5.5.0", - "@ethersproject/providers": "5.5.2", - "@ethersproject/random": "5.5.1", - "@ethersproject/rlp": "5.5.0", - "@ethersproject/sha2": "5.5.0", - "@ethersproject/signing-key": "5.5.0", - "@ethersproject/solidity": "5.5.0", - "@ethersproject/strings": "5.5.0", - "@ethersproject/transactions": "5.5.0", - "@ethersproject/units": "5.5.0", - "@ethersproject/wallet": "5.5.0", - "@ethersproject/web": "5.5.1", - "@ethersproject/wordlists": "5.5.0" - } - }, - "ethjs-unit": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", - "integrity": "sha1-xmWSHkduh7ziqdWIpv4EBbLEFpk=", - "dev": true, - "requires": { - "bn.js": "4.11.6", - "number-to-bn": "1.7.0" - }, - "dependencies": { - "bn.js": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=", - "dev": true - } - } - }, - "ethjs-util": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", - "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", - "requires": { - "is-hex-prefixed": "1.0.0", - "strip-hex-prefix": "1.0.0" - } - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "exit-on-epipe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", - "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", - "dev": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fake-merkle-patricia-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz", - "integrity": "sha1-S4w6z7Ugr635hgsfFM2M40As3dM=", - "requires": { - "checkpoint-store": "^1.1.0" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - }, - "fetch-ponyfill": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz", - "integrity": "sha1-rjzl9zLGReq4fkroeTQUcJsjmJM=", - "requires": { - "node-fetch": "~1.7.1" - }, - "dependencies": { - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - } - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-replace": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz", - "integrity": "sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=", - "dev": true, - "requires": { - "array-back": "^1.0.4", - "test-value": "^2.1.0" - }, - "dependencies": { - "array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", - "dev": true, - "requires": { - "typical": "^2.6.0" - } - } - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "find-yarn-workspace-root": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "dev": true, - "requires": { - "micromatch": "^4.0.2" - } - }, - "flat": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", - "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fp-ts": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz", - "integrity": "sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==", - "dev": true - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - }, - "ganache-core": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/ganache-core/-/ganache-core-2.13.2.tgz", - "integrity": "sha512-tIF5cR+ANQz0+3pHWxHjIwHqFXcVo0Mb+kcsNhglNFALcYo49aQpnS9dqHartqPfMFjiHh/qFoD3mYK0d/qGgw==", - "dev": true, - "requires": { - "abstract-leveldown": "3.0.0", - "async": "2.6.2", - "bip39": "2.5.0", - "cachedown": "1.0.0", - "clone": "2.1.2", - "debug": "3.2.6", - "encoding-down": "5.0.4", - "eth-sig-util": "3.0.0", - "ethereumjs-abi": "0.6.8", - "ethereumjs-account": "3.0.0", - "ethereumjs-block": "2.2.2", - "ethereumjs-common": "1.5.0", - "ethereumjs-tx": "2.1.2", - "ethereumjs-util": "6.2.1", - "ethereumjs-vm": "4.2.0", - "ethereumjs-wallet": "0.6.5", - "heap": "0.2.6", - "keccak": "3.0.1", - "level-sublevel": "6.6.4", - "levelup": "3.1.1", - "lodash": "4.17.20", - "lru-cache": "5.1.1", - "merkle-patricia-tree": "3.0.0", - "patch-package": "6.2.2", - "seedrandom": "3.0.1", - "source-map-support": "0.5.12", - "tmp": "0.1.0", - "web3": "1.2.11", - "web3-provider-engine": "14.2.1", - "websocket": "1.0.32" - }, - "dependencies": { - "@ethersproject/abi": { - "version": "5.0.0-beta.153", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/address": ">=5.0.0-beta.128", - "@ethersproject/bignumber": ">=5.0.0-beta.130", - "@ethersproject/bytes": ">=5.0.0-beta.129", - "@ethersproject/constants": ">=5.0.0-beta.128", - "@ethersproject/hash": ">=5.0.0-beta.128", - "@ethersproject/keccak256": ">=5.0.0-beta.127", - "@ethersproject/logger": ">=5.0.0-beta.129", - "@ethersproject/properties": ">=5.0.0-beta.131", - "@ethersproject/strings": ">=5.0.0-beta.130" - } - }, - "@ethersproject/abstract-provider": { - "version": "5.0.8", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/bignumber": "^5.0.13", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/networks": "^5.0.7", - "@ethersproject/properties": "^5.0.7", - "@ethersproject/transactions": "^5.0.9", - "@ethersproject/web": "^5.0.12" - } - }, - "@ethersproject/abstract-signer": { - "version": "5.0.10", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/abstract-provider": "^5.0.8", - "@ethersproject/bignumber": "^5.0.13", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/properties": "^5.0.7" - } - }, - "@ethersproject/address": { - "version": "5.0.9", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/bignumber": "^5.0.13", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/keccak256": "^5.0.7", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/rlp": "^5.0.7" - } - }, - "@ethersproject/base64": { - "version": "5.0.7", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/bytes": "^5.0.9" - } - }, - "@ethersproject/bignumber": { - "version": "5.0.13", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8", - "bn.js": "^4.4.0" - } - }, - "@ethersproject/bytes": { - "version": "5.0.9", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/logger": "^5.0.8" - } - }, - "@ethersproject/constants": { - "version": "5.0.8", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/bignumber": "^5.0.13" - } - }, - "@ethersproject/hash": { - "version": "5.0.10", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/abstract-signer": "^5.0.10", - "@ethersproject/address": "^5.0.9", - "@ethersproject/bignumber": "^5.0.13", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/keccak256": "^5.0.7", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/properties": "^5.0.7", - "@ethersproject/strings": "^5.0.8" - } - }, - "@ethersproject/keccak256": { - "version": "5.0.7", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/bytes": "^5.0.9", - "js-sha3": "0.5.7" - } - }, - "@ethersproject/logger": { - "version": "5.0.8", - "dev": true, - "optional": true - }, - "@ethersproject/networks": { - "version": "5.0.7", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/logger": "^5.0.8" - } - }, - "@ethersproject/properties": { - "version": "5.0.7", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/logger": "^5.0.8" - } - }, - "@ethersproject/rlp": { - "version": "5.0.7", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8" - } - }, - "@ethersproject/signing-key": { - "version": "5.0.8", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/properties": "^5.0.7", - "elliptic": "6.5.3" - } - }, - "@ethersproject/strings": { - "version": "5.0.8", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/constants": "^5.0.8", - "@ethersproject/logger": "^5.0.8" - } - }, - "@ethersproject/transactions": { - "version": "5.0.9", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/address": "^5.0.9", - "@ethersproject/bignumber": "^5.0.13", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/constants": "^5.0.8", - "@ethersproject/keccak256": "^5.0.7", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/properties": "^5.0.7", - "@ethersproject/rlp": "^5.0.7", - "@ethersproject/signing-key": "^5.0.8" - } - }, - "@ethersproject/web": { - "version": "5.0.12", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/base64": "^5.0.7", - "@ethersproject/bytes": "^5.0.9", - "@ethersproject/logger": "^5.0.8", - "@ethersproject/properties": "^5.0.7", - "@ethersproject/strings": "^5.0.8" - } - }, - "@sindresorhus/is": { - "version": "0.14.0", - "dev": true, - "optional": true - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "dev": true, - "optional": true, - "requires": { - "defer-to-connect": "^1.0.1" - } - }, - "@types/bn.js": { - "version": "4.11.6", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "14.14.20", - "dev": true - }, - "@types/pbkdf2": { - "version": "3.1.0", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/secp256k1": { - "version": "4.0.1", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@yarnpkg/lockfile": { - "version": "1.1.0", - "dev": true - }, - "abstract-leveldown": { - "version": "3.0.0", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - }, - "accepts": { - "version": "1.3.7", - "dev": true, - "optional": true, - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "aes-js": { - "version": "3.1.2", - "dev": true, - "optional": true - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-styles": { - "version": "3.2.1", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "arr-diff": { - "version": "4.0.0", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "dev": true - }, - "array-flatten": { - "version": "1.1.1", - "dev": true, - "optional": true - }, - "array-unique": { - "version": "0.3.2", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "5.4.1", - "dev": true, - "optional": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "dev": true - }, - "async": { - "version": "2.6.2", - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - }, - "async-eventemitter": { - "version": "0.2.4", - "dev": true, - "requires": { - "async": "^2.4.0" - } - }, - "async-limiter": { - "version": "1.0.1", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "dev": true - }, - "atob": { - "version": "2.1.2", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "dev": true - }, - "aws4": { - "version": "1.11.0", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "js-tokens": { - "version": "3.0.2", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "dev": true - } - } - }, - "babel-core": { - "version": "6.26.3", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "json5": { - "version": "0.5.1", - "dev": true - }, - "ms": { - "version": "2.0.0", - "dev": true - }, - "slash": { - "version": "1.0.0", - "dev": true - } - } - }, - "babel-generator": { - "version": "6.26.1", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "1.3.0", - "dev": true - } - } - }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-explode-assignable-expression": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-call-delegate": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-define-map": { - "version": "6.26.0", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-function-name": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-get-function-arity": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-hoist-variables": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-optimise-call-expression": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-regex": { - "version": "6.26.0", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-helper-remap-async-to-generator": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-replace-supers": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helpers": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-messages": { - "version": "6.23.0", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-check-es2015-constants": { - "version": "6.22.0", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "dev": true - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "dev": true - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "dev": true - }, - "babel-plugin-transform-async-to-generator": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-functions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "6.26.0", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-plugin-transform-es2015-classes": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-define-map": "^6.24.1", - "babel-helper-function-name": "^6.24.1", - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-helper-replace-supers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-duplicate-keys": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-literals": { - "version": "6.22.0", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-modules-amd": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.26.2", - "dev": true, - "requires": { - "babel-plugin-transform-strict-mode": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-types": "^6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-systemjs": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-modules-umd": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-plugin-transform-es2015-modules-amd": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-replace-supers": "^6.24.1", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-call-delegate": "^6.24.1", - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-typeof-symbol": { - "version": "6.23.0", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "regexpu-core": "^2.0.0" - } - }, - "babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", - "babel-plugin-syntax-exponentiation-operator": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-regenerator": { - "version": "6.26.0", - "dev": true, - "requires": { - "regenerator-transform": "^0.10.0" - } - }, - "babel-plugin-transform-strict-mode": { - "version": "6.24.1", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-preset-env": { - "version": "1.7.0", - "dev": true, - "requires": { - "babel-plugin-check-es2015-constants": "^6.22.0", - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-to-generator": "^6.22.0", - "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoping": "^6.23.0", - "babel-plugin-transform-es2015-classes": "^6.23.0", - "babel-plugin-transform-es2015-computed-properties": "^6.22.0", - "babel-plugin-transform-es2015-destructuring": "^6.23.0", - "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0", - "babel-plugin-transform-es2015-for-of": "^6.23.0", - "babel-plugin-transform-es2015-function-name": "^6.22.0", - "babel-plugin-transform-es2015-literals": "^6.22.0", - "babel-plugin-transform-es2015-modules-amd": "^6.22.0", - "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", - "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0", - "babel-plugin-transform-es2015-modules-umd": "^6.23.0", - "babel-plugin-transform-es2015-object-super": "^6.22.0", - "babel-plugin-transform-es2015-parameters": "^6.23.0", - "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0", - "babel-plugin-transform-es2015-spread": "^6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "^6.22.0", - "babel-plugin-transform-es2015-template-literals": "^6.22.0", - "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0", - "babel-plugin-transform-es2015-unicode-regex": "^6.22.0", - "babel-plugin-transform-exponentiation-operator": "^6.22.0", - "babel-plugin-transform-regenerator": "^6.22.0", - "browserslist": "^3.2.6", - "invariant": "^2.2.2", - "semver": "^5.3.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "dev": true - } - } - }, - "babel-register": { - "version": "6.26.0", - "dev": true, - "requires": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" - }, - "dependencies": { - "source-map-support": { - "version": "0.4.18", - "dev": true, - "requires": { - "source-map": "^0.5.6" - } - } - } - }, - "babel-runtime": { - "version": "6.26.0", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "babel-template": { - "version": "6.26.0", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "globals": { - "version": "9.18.0", - "dev": true - }, - "ms": { - "version": "2.0.0", - "dev": true - } - } - }, - "babel-types": { - "version": "6.26.0", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - }, - "dependencies": { - "to-fast-properties": { - "version": "1.0.3", - "dev": true - } - } - }, - "babelify": { - "version": "7.3.0", - "dev": true, - "requires": { - "babel-core": "^6.0.14", - "object-assign": "^4.0.0" - } - }, - "babylon": { - "version": "6.18.0", - "dev": true - }, - "backoff": { - "version": "2.5.0", - "dev": true, - "requires": { - "precond": "0.2" - } - }, - "balanced-match": { - "version": "1.0.0", - "dev": true - }, - "base": { - "version": "0.11.2", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - } - } - }, - "base-x": { - "version": "3.0.8", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "base64-js": { - "version": "1.5.1", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - }, - "dependencies": { - "tweetnacl": { - "version": "0.14.5", - "dev": true - } - } - }, - "bignumber.js": { - "version": "9.0.1", - "dev": true, - "optional": true - }, - "bip39": { - "version": "2.5.0", - "dev": true, - "requires": { - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1", - "safe-buffer": "^5.0.1", - "unorm": "^1.3.3" - } - }, - "blakejs": { - "version": "1.1.0", - "dev": true - }, - "bluebird": { - "version": "3.7.2", - "dev": true, - "optional": true - }, - "bn.js": { - "version": "4.11.9", - "dev": true - }, - "body-parser": { - "version": "1.19.0", - "dev": true, - "optional": true, - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "dev": true, - "optional": true - }, - "qs": { - "version": "6.7.0", - "dev": true, - "optional": true - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "dev": true, - "optional": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "dev": true, - "optional": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.1.0", - "dev": true, - "optional": true, - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - }, - "dependencies": { - "bn.js": { - "version": "5.1.3", - "dev": true, - "optional": true - } - } - }, - "browserify-sign": { - "version": "4.2.1", - "dev": true, - "optional": true, - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "bn.js": { - "version": "5.1.3", - "dev": true, - "optional": true - }, - "readable-stream": { - "version": "3.6.0", - "dev": true, - "optional": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "browserslist": { - "version": "3.2.8", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30000844", - "electron-to-chromium": "^1.3.47" - } - }, - "bs58": { - "version": "4.0.1", - "dev": true, - "requires": { - "base-x": "^3.0.2" - } - }, - "bs58check": { - "version": "2.1.2", - "dev": true, - "requires": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" - } - }, - "buffer": { - "version": "5.7.1", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-from": { - "version": "1.1.1", - "dev": true - }, - "buffer-to-arraybuffer": { - "version": "0.0.5", - "dev": true, - "optional": true - }, - "buffer-xor": { - "version": "1.0.3", - "dev": true - }, - "bufferutil": { - "version": "4.0.3", - "dev": true, - "requires": { - "node-gyp-build": "^4.2.0" - } - }, - "bytes": { - "version": "3.1.0", - "dev": true, - "optional": true - }, - "bytewise": { - "version": "1.1.0", - "dev": true, - "requires": { - "bytewise-core": "^1.2.2", - "typewise": "^1.0.3" - } - }, - "bytewise-core": { - "version": "1.2.3", - "dev": true, - "requires": { - "typewise-core": "^1.2" - } - }, - "cache-base": { - "version": "1.0.1", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "cacheable-request": { - "version": "6.1.0", - "dev": true, - "optional": true, - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "lowercase-keys": { - "version": "2.0.0", - "dev": true, - "optional": true - } - } - }, - "cachedown": { - "version": "1.0.0", - "dev": true, - "requires": { - "abstract-leveldown": "^2.4.1", - "lru-cache": "^3.2.0" - }, - "dependencies": { - "abstract-leveldown": { - "version": "2.7.2", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - }, - "lru-cache": { - "version": "3.2.0", - "dev": true, - "requires": { - "pseudomap": "^1.0.1" - } - } - } - }, - "call-bind": { - "version": "1.0.2", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "caniuse-lite": { - "version": "1.0.30001174", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "checkpoint-store": { - "version": "1.1.0", - "dev": true, - "requires": { - "functional-red-black-tree": "^1.0.1" - } - }, - "chownr": { - "version": "1.1.4", - "dev": true, - "optional": true - }, - "ci-info": { - "version": "2.0.0", - "dev": true - }, - "cids": { - "version": "0.7.5", - "dev": true, - "optional": true, - "requires": { - "buffer": "^5.5.0", - "class-is": "^1.1.0", - "multibase": "~0.6.0", - "multicodec": "^1.0.0", - "multihashes": "~0.4.15" - }, - "dependencies": { - "multicodec": { - "version": "1.0.4", - "dev": true, - "optional": true, - "requires": { - "buffer": "^5.6.0", - "varint": "^5.0.0" - } - } - } - }, - "cipher-base": { - "version": "1.0.4", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-is": { - "version": "1.1.0", - "dev": true, - "optional": true - }, - "class-utils": { - "version": "0.3.6", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-buffer": { - "version": "1.1.6", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "dev": true - } - } - }, - "clone": { - "version": "2.1.2", - "dev": true - }, - "clone-response": { - "version": "1.0.2", - "dev": true, - "optional": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "collection-visit": { - "version": "1.0.0", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "component-emitter": { - "version": "1.3.0", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "content-disposition": { - "version": "0.5.3", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "dev": true, - "optional": true - } - } - }, - "content-hash": { - "version": "2.5.2", - "dev": true, - "optional": true, - "requires": { - "cids": "^0.7.1", - "multicodec": "^0.5.5", - "multihashes": "^0.4.15" - } - }, - "content-type": { - "version": "1.0.4", - "dev": true, - "optional": true - }, - "convert-source-map": { - "version": "1.7.0", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "dev": true - } - } - }, - "cookie": { - "version": "0.4.0", - "dev": true, - "optional": true - }, - "cookie-signature": { - "version": "1.0.6", - "dev": true, - "optional": true - }, - "cookiejar": { - "version": "2.1.2", - "dev": true, - "optional": true - }, - "copy-descriptor": { - "version": "0.1.1", - "dev": true - }, - "core-js": { - "version": "2.6.12", - "dev": true - }, - "core-js-pure": { - "version": "3.8.2", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "dev": true - }, - "cors": { - "version": "2.8.5", - "dev": true, - "optional": true, - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, - "create-ecdh": { - "version": "4.0.4", - "dev": true, - "optional": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "create-hash": { - "version": "1.2.0", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-fetch": { - "version": "2.2.3", - "dev": true, - "requires": { - "node-fetch": "2.1.2", - "whatwg-fetch": "2.0.4" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "dev": true, - "optional": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "d": { - "version": "1.0.1", - "dev": true, - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "dashdash": { - "version": "1.14.1", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "3.2.6", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "decode-uri-component": { - "version": "0.2.0", - "dev": true - }, - "decompress-response": { - "version": "3.3.0", - "dev": true, - "optional": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "deep-equal": { - "version": "1.1.1", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - } - }, - "defer-to-connect": { - "version": "1.1.3", - "dev": true, - "optional": true - }, - "deferred-leveldown": { - "version": "4.0.2", - "dev": true, - "requires": { - "abstract-leveldown": "~5.0.0", - "inherits": "^2.0.3" - }, - "dependencies": { - "abstract-leveldown": { - "version": "5.0.0", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - } - } - }, - "define-properties": { - "version": "1.1.3", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "defined": { - "version": "1.0.0", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "dev": true - }, - "depd": { - "version": "1.1.2", - "dev": true, - "optional": true - }, - "des.js": { - "version": "1.0.1", - "dev": true, - "optional": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "destroy": { - "version": "1.0.4", - "dev": true, - "optional": true - }, - "detect-indent": { - "version": "4.0.0", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "diffie-hellman": { - "version": "5.0.3", - "dev": true, - "optional": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "dom-walk": { - "version": "0.1.2", - "dev": true - }, - "dotignore": { - "version": "0.1.2", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "duplexer3": { - "version": "0.1.4", - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.2", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ee-first": { - "version": "1.1.1", - "dev": true, - "optional": true - }, - "electron-to-chromium": { - "version": "1.3.636", - "dev": true - }, - "elliptic": { - "version": "6.5.3", - "dev": true, - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, - "encodeurl": { - "version": "1.0.2", - "dev": true, - "optional": true - }, - "encoding": { - "version": "0.1.13", - "dev": true, - "requires": { - "iconv-lite": "^0.6.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.2", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "encoding-down": { - "version": "5.0.4", - "dev": true, - "requires": { - "abstract-leveldown": "^5.0.0", - "inherits": "^2.0.3", - "level-codec": "^9.0.0", - "level-errors": "^2.0.0", - "xtend": "^4.0.1" - }, - "dependencies": { - "abstract-leveldown": { - "version": "5.0.0", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - } - } - }, - "end-of-stream": { - "version": "1.4.4", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "errno": { - "version": "0.1.8", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "es-abstract": { - "version": "1.18.0-next.1", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es5-ext": { - "version": "0.10.53", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "es6-iterator": { - "version": "2.0.3", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-symbol": { - "version": "3.1.3", - "dev": true, - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "escape-html": { - "version": "1.0.3", - "dev": true, - "optional": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "etag": { - "version": "1.8.1", - "dev": true, - "optional": true - }, - "eth-block-tracker": { - "version": "3.0.1", - "dev": true, - "requires": { - "eth-query": "^2.1.0", - "ethereumjs-tx": "^1.3.3", - "ethereumjs-util": "^5.1.3", - "ethjs-util": "^0.1.3", - "json-rpc-engine": "^3.6.0", - "pify": "^2.3.0", - "tape": "^4.6.3" - }, - "dependencies": { - "ethereumjs-tx": { - "version": "1.3.7", - "dev": true, - "requires": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" - } - }, - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "pify": { - "version": "2.3.0", - "dev": true - } - } - }, - "eth-ens-namehash": { - "version": "2.0.8", - "dev": true, - "optional": true, - "requires": { - "idna-uts46-hx": "^2.3.1", - "js-sha3": "^0.5.7" - } - }, - "eth-json-rpc-infura": { - "version": "3.2.1", - "dev": true, - "requires": { - "cross-fetch": "^2.1.1", - "eth-json-rpc-middleware": "^1.5.0", - "json-rpc-engine": "^3.4.0", - "json-rpc-error": "^2.0.0" - } - }, - "eth-json-rpc-middleware": { - "version": "1.6.0", - "dev": true, - "requires": { - "async": "^2.5.0", - "eth-query": "^2.1.2", - "eth-tx-summary": "^3.1.2", - "ethereumjs-block": "^1.6.0", - "ethereumjs-tx": "^1.3.3", - "ethereumjs-util": "^5.1.2", - "ethereumjs-vm": "^2.1.0", - "fetch-ponyfill": "^4.0.0", - "json-rpc-engine": "^3.6.0", - "json-rpc-error": "^2.0.0", - "json-stable-stringify": "^1.0.1", - "promise-to-callback": "^1.0.0", - "tape": "^4.6.3" - }, - "dependencies": { - "abstract-leveldown": { - "version": "2.6.3", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - }, - "deferred-leveldown": { - "version": "1.2.2", - "dev": true, - "requires": { - "abstract-leveldown": "~2.6.0" - } - }, - "ethereumjs-account": { - "version": "2.0.5", - "dev": true, - "requires": { - "ethereumjs-util": "^5.0.0", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "ethereumjs-block": { - "version": "1.7.1", - "dev": true, - "requires": { - "async": "^2.0.1", - "ethereum-common": "0.2.0", - "ethereumjs-tx": "^1.2.2", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - }, - "dependencies": { - "ethereum-common": { - "version": "0.2.0", - "dev": true - } - } - }, - "ethereumjs-tx": { - "version": "1.3.7", - "dev": true, - "requires": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" - } - }, - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "ethereumjs-vm": { - "version": "2.6.0", - "dev": true, - "requires": { - "async": "^2.1.2", - "async-eventemitter": "^0.2.2", - "ethereumjs-account": "^2.0.3", - "ethereumjs-block": "~2.2.0", - "ethereumjs-common": "^1.1.0", - "ethereumjs-util": "^6.0.0", - "fake-merkle-patricia-tree": "^1.0.1", - "functional-red-black-tree": "^1.0.1", - "merkle-patricia-tree": "^2.3.2", - "rustbn.js": "~0.2.0", - "safe-buffer": "^5.1.1" - }, - "dependencies": { - "ethereumjs-block": { - "version": "2.2.2", - "dev": true, - "requires": { - "async": "^2.0.1", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.1", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - }, - "dependencies": { - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "ethereumjs-tx": { - "version": "2.1.2", - "dev": true, - "requires": { - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.0.0" - } - }, - "ethereumjs-util": { - "version": "6.2.1", - "dev": true, - "requires": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" - } - } - } - }, - "isarray": { - "version": "0.0.1", - "dev": true - }, - "level-codec": { - "version": "7.0.1", - "dev": true - }, - "level-errors": { - "version": "1.0.5", - "dev": true, - "requires": { - "errno": "~0.1.1" - } - }, - "level-iterator-stream": { - "version": "1.3.1", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - } - } - }, - "level-ws": { - "version": "0.0.0", - "dev": true, - "requires": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "xtend": { - "version": "2.1.2", - "dev": true, - "requires": { - "object-keys": "~0.4.0" - } - } - } - }, - "levelup": { - "version": "1.3.9", - "dev": true, - "requires": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" - } - }, - "ltgt": { - "version": "2.2.1", - "dev": true - }, - "memdown": { - "version": "1.4.1", - "dev": true, - "requires": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" - }, - "dependencies": { - "abstract-leveldown": { - "version": "2.7.2", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - } - } - }, - "merkle-patricia-tree": { - "version": "2.3.2", - "dev": true, - "requires": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "dev": true - } - } - }, - "object-keys": { - "version": "0.4.0", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "dev": true - }, - "semver": { - "version": "5.4.1", - "dev": true - }, - "string_decoder": { - "version": "0.10.31", - "dev": true - } - } - }, - "eth-lib": { - "version": "0.1.29", - "dev": true, - "optional": true, - "requires": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "nano-json-stream-parser": "^0.1.2", - "servify": "^0.1.12", - "ws": "^3.0.0", - "xhr-request-promise": "^0.1.2" - } - }, - "eth-query": { - "version": "2.1.2", - "dev": true, - "requires": { - "json-rpc-random-id": "^1.0.0", - "xtend": "^4.0.1" - } - }, - "eth-sig-util": { - "version": "3.0.0", - "dev": true, - "requires": { - "buffer": "^5.2.1", - "elliptic": "^6.4.0", - "ethereumjs-abi": "0.6.5", - "ethereumjs-util": "^5.1.1", - "tweetnacl": "^1.0.0", - "tweetnacl-util": "^0.15.0" - }, - "dependencies": { - "ethereumjs-abi": { - "version": "0.6.5", - "dev": true, - "requires": { - "bn.js": "^4.10.0", - "ethereumjs-util": "^4.3.0" - }, - "dependencies": { - "ethereumjs-util": { - "version": "4.5.1", - "dev": true, - "requires": { - "bn.js": "^4.8.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.0.0" - } - } - } - }, - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "eth-tx-summary": { - "version": "3.2.4", - "dev": true, - "requires": { - "async": "^2.1.2", - "clone": "^2.0.0", - "concat-stream": "^1.5.1", - "end-of-stream": "^1.1.0", - "eth-query": "^2.0.2", - "ethereumjs-block": "^1.4.1", - "ethereumjs-tx": "^1.1.1", - "ethereumjs-util": "^5.0.1", - "ethereumjs-vm": "^2.6.0", - "through2": "^2.0.3" - }, - "dependencies": { - "abstract-leveldown": { - "version": "2.6.3", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - }, - "deferred-leveldown": { - "version": "1.2.2", - "dev": true, - "requires": { - "abstract-leveldown": "~2.6.0" - } - }, - "ethereumjs-account": { - "version": "2.0.5", - "dev": true, - "requires": { - "ethereumjs-util": "^5.0.0", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "ethereumjs-block": { - "version": "1.7.1", - "dev": true, - "requires": { - "async": "^2.0.1", - "ethereum-common": "0.2.0", - "ethereumjs-tx": "^1.2.2", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - }, - "dependencies": { - "ethereum-common": { - "version": "0.2.0", - "dev": true - } - } - }, - "ethereumjs-tx": { - "version": "1.3.7", - "dev": true, - "requires": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" - } - }, - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "ethereumjs-vm": { - "version": "2.6.0", - "dev": true, - "requires": { - "async": "^2.1.2", - "async-eventemitter": "^0.2.2", - "ethereumjs-account": "^2.0.3", - "ethereumjs-block": "~2.2.0", - "ethereumjs-common": "^1.1.0", - "ethereumjs-util": "^6.0.0", - "fake-merkle-patricia-tree": "^1.0.1", - "functional-red-black-tree": "^1.0.1", - "merkle-patricia-tree": "^2.3.2", - "rustbn.js": "~0.2.0", - "safe-buffer": "^5.1.1" - }, - "dependencies": { - "ethereumjs-block": { - "version": "2.2.2", - "dev": true, - "requires": { - "async": "^2.0.1", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.1", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - }, - "dependencies": { - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "ethereumjs-tx": { - "version": "2.1.2", - "dev": true, - "requires": { - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.0.0" - } - }, - "ethereumjs-util": { - "version": "6.2.1", - "dev": true, - "requires": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" - } - } - } - }, - "isarray": { - "version": "0.0.1", - "dev": true - }, - "level-codec": { - "version": "7.0.1", - "dev": true - }, - "level-errors": { - "version": "1.0.5", - "dev": true, - "requires": { - "errno": "~0.1.1" - } - }, - "level-iterator-stream": { - "version": "1.3.1", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - } - } - }, - "level-ws": { - "version": "0.0.0", - "dev": true, - "requires": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "xtend": { - "version": "2.1.2", - "dev": true, - "requires": { - "object-keys": "~0.4.0" - } - } - } - }, - "levelup": { - "version": "1.3.9", - "dev": true, - "requires": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" - } - }, - "ltgt": { - "version": "2.2.1", - "dev": true - }, - "memdown": { - "version": "1.4.1", - "dev": true, - "requires": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" - }, - "dependencies": { - "abstract-leveldown": { - "version": "2.7.2", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - } - } - }, - "merkle-patricia-tree": { - "version": "2.3.2", - "dev": true, - "requires": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "dev": true - } - } - }, - "object-keys": { - "version": "0.4.0", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "dev": true - }, - "semver": { - "version": "5.4.1", - "dev": true - }, - "string_decoder": { - "version": "0.10.31", - "dev": true - } - } - }, - "ethashjs": { - "version": "0.0.8", - "dev": true, - "requires": { - "async": "^2.1.2", - "buffer-xor": "^2.0.1", - "ethereumjs-util": "^7.0.2", - "miller-rabin": "^4.0.0" - }, - "dependencies": { - "bn.js": { - "version": "5.1.3", - "dev": true - }, - "buffer-xor": { - "version": "2.0.2", - "dev": true, - "requires": { - "safe-buffer": "^5.1.1" - } - }, - "ethereumjs-util": { - "version": "7.0.7", - "dev": true, - "requires": { - "@types/bn.js": "^4.11.3", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.4" - } - } - } - }, - "ethereum-bloom-filters": { - "version": "1.0.7", - "dev": true, - "optional": true, - "requires": { - "js-sha3": "^0.8.0" - }, - "dependencies": { - "js-sha3": { - "version": "0.8.0", - "dev": true, - "optional": true - } - } - }, - "ethereum-common": { - "version": "0.0.18", - "dev": true - }, - "ethereum-cryptography": { - "version": "0.1.3", - "dev": true, - "requires": { - "@types/pbkdf2": "^3.0.0", - "@types/secp256k1": "^4.0.1", - "blakejs": "^1.1.0", - "browserify-aes": "^1.2.0", - "bs58check": "^2.1.2", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "hash.js": "^1.1.7", - "keccak": "^3.0.0", - "pbkdf2": "^3.0.17", - "randombytes": "^2.1.0", - "safe-buffer": "^5.1.2", - "scrypt-js": "^3.0.0", - "secp256k1": "^4.0.1", - "setimmediate": "^1.0.5" - } - }, - "ethereumjs-abi": { - "version": "0.6.8", - "dev": true, - "requires": { - "bn.js": "^4.11.8", - "ethereumjs-util": "^6.0.0" - } - }, - "ethereumjs-account": { - "version": "3.0.0", - "dev": true, - "requires": { - "ethereumjs-util": "^6.0.0", - "rlp": "^2.2.1", - "safe-buffer": "^5.1.1" - } - }, - "ethereumjs-block": { - "version": "2.2.2", - "dev": true, - "requires": { - "async": "^2.0.1", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.1", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - }, - "dependencies": { - "abstract-leveldown": { - "version": "2.6.3", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - }, - "deferred-leveldown": { - "version": "1.2.2", - "dev": true, - "requires": { - "abstract-leveldown": "~2.6.0" - } - }, - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "isarray": { - "version": "0.0.1", - "dev": true - }, - "level-codec": { - "version": "7.0.1", - "dev": true - }, - "level-errors": { - "version": "1.0.5", - "dev": true, - "requires": { - "errno": "~0.1.1" - } - }, - "level-iterator-stream": { - "version": "1.3.1", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - } - } - }, - "level-ws": { - "version": "0.0.0", - "dev": true, - "requires": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "xtend": { - "version": "2.1.2", - "dev": true, - "requires": { - "object-keys": "~0.4.0" - } - } - } - }, - "levelup": { - "version": "1.3.9", - "dev": true, - "requires": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" - } - }, - "ltgt": { - "version": "2.2.1", - "dev": true - }, - "memdown": { - "version": "1.4.1", - "dev": true, - "requires": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" - }, - "dependencies": { - "abstract-leveldown": { - "version": "2.7.2", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - } - } - }, - "merkle-patricia-tree": { - "version": "2.3.2", - "dev": true, - "requires": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "dev": true - } - } - }, - "object-keys": { - "version": "0.4.0", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "dev": true - }, - "semver": { - "version": "5.4.1", - "dev": true - }, - "string_decoder": { - "version": "0.10.31", - "dev": true - } - } - }, - "ethereumjs-blockchain": { - "version": "4.0.4", - "dev": true, - "requires": { - "async": "^2.6.1", - "ethashjs": "~0.0.7", - "ethereumjs-block": "~2.2.2", - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.1.0", - "flow-stoplight": "^1.0.0", - "level-mem": "^3.0.1", - "lru-cache": "^5.1.1", - "rlp": "^2.2.2", - "semaphore": "^1.1.0" - } - }, - "ethereumjs-common": { - "version": "1.5.0", - "dev": true - }, - "ethereumjs-tx": { - "version": "2.1.2", - "dev": true, - "requires": { - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.0.0" - } - }, - "ethereumjs-util": { - "version": "6.2.1", - "dev": true, - "requires": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" - } - }, - "ethereumjs-vm": { - "version": "4.2.0", - "dev": true, - "requires": { - "async": "^2.1.2", - "async-eventemitter": "^0.2.2", - "core-js-pure": "^3.0.1", - "ethereumjs-account": "^3.0.0", - "ethereumjs-block": "^2.2.2", - "ethereumjs-blockchain": "^4.0.3", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.2", - "ethereumjs-util": "^6.2.0", - "fake-merkle-patricia-tree": "^1.0.1", - "functional-red-black-tree": "^1.0.1", - "merkle-patricia-tree": "^2.3.2", - "rustbn.js": "~0.2.0", - "safe-buffer": "^5.1.1", - "util.promisify": "^1.0.0" - }, - "dependencies": { - "abstract-leveldown": { - "version": "2.6.3", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - }, - "deferred-leveldown": { - "version": "1.2.2", - "dev": true, - "requires": { - "abstract-leveldown": "~2.6.0" - } - }, - "isarray": { - "version": "0.0.1", - "dev": true - }, - "level-codec": { - "version": "7.0.1", - "dev": true - }, - "level-errors": { - "version": "1.0.5", - "dev": true, - "requires": { - "errno": "~0.1.1" - } - }, - "level-iterator-stream": { - "version": "1.3.1", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - } - } - }, - "level-ws": { - "version": "0.0.0", - "dev": true, - "requires": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "xtend": { - "version": "2.1.2", - "dev": true, - "requires": { - "object-keys": "~0.4.0" - } - } - } - }, - "levelup": { - "version": "1.3.9", - "dev": true, - "requires": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" - } - }, - "ltgt": { - "version": "2.2.1", - "dev": true - }, - "memdown": { - "version": "1.4.1", - "dev": true, - "requires": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" - }, - "dependencies": { - "abstract-leveldown": { - "version": "2.7.2", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - } - } - }, - "merkle-patricia-tree": { - "version": "2.3.2", - "dev": true, - "requires": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "dev": true - }, - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "object-keys": { - "version": "0.4.0", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "dev": true - }, - "semver": { - "version": "5.4.1", - "dev": true - }, - "string_decoder": { - "version": "0.10.31", - "dev": true - } - } - }, - "ethereumjs-wallet": { - "version": "0.6.5", - "dev": true, - "optional": true, - "requires": { - "aes-js": "^3.1.1", - "bs58check": "^2.1.2", - "ethereum-cryptography": "^0.1.3", - "ethereumjs-util": "^6.0.0", - "randombytes": "^2.0.6", - "safe-buffer": "^5.1.2", - "scryptsy": "^1.2.1", - "utf8": "^3.0.0", - "uuid": "^3.3.2" - } - }, - "ethjs-unit": { - "version": "0.1.6", - "dev": true, - "optional": true, - "requires": { - "bn.js": "4.11.6", - "number-to-bn": "1.7.0" - }, - "dependencies": { - "bn.js": { - "version": "4.11.6", - "dev": true, - "optional": true - } - } - }, - "ethjs-util": { - "version": "0.1.6", - "dev": true, - "requires": { - "is-hex-prefixed": "1.0.0", - "strip-hex-prefix": "1.0.0" - } - }, - "eventemitter3": { - "version": "4.0.4", - "dev": true, - "optional": true - }, - "events": { - "version": "3.2.0", - "dev": true - }, - "evp_bytestokey": { - "version": "1.0.3", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "expand-brackets": { - "version": "2.1.4", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-buffer": { - "version": "1.1.6", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "dev": true - }, - "kind-of": { - "version": "5.1.0", - "dev": true - }, - "ms": { - "version": "2.0.0", - "dev": true - } - } - }, - "express": { - "version": "4.17.1", - "dev": true, - "optional": true, - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "dev": true, - "optional": true - }, - "qs": { - "version": "6.7.0", - "dev": true, - "optional": true - }, - "safe-buffer": { - "version": "5.1.2", - "dev": true, - "optional": true - } - } - }, - "ext": { - "version": "1.4.0", - "dev": true, - "requires": { - "type": "^2.0.0" - }, - "dependencies": { - "type": { - "version": "2.1.0", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "extglob": { - "version": "2.0.4", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "dev": true - } - } - }, - "extsprintf": { - "version": "1.3.0", - "dev": true - }, - "fake-merkle-patricia-tree": { - "version": "1.0.1", - "dev": true, - "requires": { - "checkpoint-store": "^1.1.0" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fetch-ponyfill": { - "version": "4.1.0", - "dev": true, - "requires": { - "node-fetch": "~1.7.1" - }, - "dependencies": { - "is-stream": { - "version": "1.1.0", - "dev": true - }, - "node-fetch": { - "version": "1.7.3", - "dev": true, - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - } - } - }, - "finalhandler": { - "version": "1.1.2", - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "dev": true, - "optional": true - } - } - }, - "find-yarn-workspace-root": { - "version": "1.2.1", - "dev": true, - "requires": { - "fs-extra": "^4.0.3", - "micromatch": "^3.1.4" - }, - "dependencies": { - "braces": { - "version": "2.3.2", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fs-extra": { - "version": "4.0.3", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "dev": true - }, - "is-extendable": { - "version": "0.1.1", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "to-regex-range": { - "version": "2.1.1", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "flow-stoplight": { - "version": "1.0.0", - "dev": true - }, - "for-each": { - "version": "0.3.3", - "dev": true, - "requires": { - "is-callable": "^1.1.3" - } - }, - "for-in": { - "version": "1.0.2", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.1.2", - "dev": true, - "optional": true - }, - "fragment-cache": { - "version": "0.2.1", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "dev": true, - "optional": true - }, - "fs-extra": { - "version": "7.0.1", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "function-bind": { - "version": "1.1.1", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "dev": true - }, - "get-intrinsic": { - "version": "1.0.2", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "get-stream": { - "version": "5.2.0", - "dev": true, - "optional": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "global": { - "version": "4.4.0", - "dev": true, - "requires": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "got": { - "version": "9.6.0", - "dev": true, - "optional": true, - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "dependencies": { - "get-stream": { - "version": "4.1.0", - "dev": true, - "optional": true, - "requires": { - "pump": "^3.0.0" - } - } - } - }, - "graceful-fs": { - "version": "4.2.4", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "dev": true - } - } - }, - "has-flag": { - "version": "3.0.0", - "dev": true - }, - "has-symbol-support-x": { - "version": "1.4.2", - "dev": true, - "optional": true - }, - "has-symbols": { - "version": "1.0.1", - "dev": true - }, - "has-to-string-tag-x": { - "version": "1.4.1", - "dev": true, - "optional": true, - "requires": { - "has-symbol-support-x": "^1.4.1" - } - }, - "has-value": { - "version": "1.0.0", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.1.0", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "hash.js": { - "version": "1.1.7", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "heap": { - "version": "0.2.6", - "dev": true - }, - "hmac-drbg": { - "version": "1.0.1", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "home-or-tmp": { - "version": "2.0.0", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" - } - }, - "http-cache-semantics": { - "version": "4.1.0", - "dev": true, - "optional": true - }, - "http-errors": { - "version": "1.7.2", - "dev": true, - "optional": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "dev": true, - "optional": true - } - } - }, - "http-https": { - "version": "1.0.0", - "dev": true, - "optional": true - }, - "http-signature": { - "version": "1.2.0", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "idna-uts46-hx": { - "version": "2.3.1", - "dev": true, - "optional": true, - "requires": { - "punycode": "2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.0", - "dev": true, - "optional": true - } - } - }, - "ieee754": { - "version": "1.2.1", - "dev": true - }, - "immediate": { - "version": "3.2.3", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "invariant": { - "version": "2.2.4", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "ipaddr.js": { - "version": "1.9.1", - "dev": true, - "optional": true - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-arguments": { - "version": "1.1.0", - "dev": true, - "requires": { - "call-bind": "^1.0.0" - } - }, - "is-callable": { - "version": "1.2.2", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-date-object": { - "version": "1.0.2", - "dev": true - }, - "is-descriptor": { - "version": "1.0.2", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-finite": { - "version": "1.1.0", - "dev": true - }, - "is-fn": { - "version": "1.0.0", - "dev": true - }, - "is-function": { - "version": "1.0.2", - "dev": true - }, - "is-hex-prefixed": { - "version": "1.0.0", - "dev": true - }, - "is-negative-zero": { - "version": "2.0.1", - "dev": true - }, - "is-object": { - "version": "1.0.2", - "dev": true, - "optional": true - }, - "is-plain-obj": { - "version": "1.1.0", - "dev": true, - "optional": true - }, - "is-plain-object": { - "version": "2.0.4", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-regex": { - "version": "1.1.1", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-retry-allowed": { - "version": "1.2.0", - "dev": true, - "optional": true - }, - "is-symbol": { - "version": "1.0.3", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "dev": true - }, - "isurl": { - "version": "1.0.0", - "dev": true, - "optional": true, - "requires": { - "has-to-string-tag-x": "^1.2.0", - "is-object": "^1.0.1" - } - }, - "js-sha3": { - "version": "0.5.7", - "dev": true, - "optional": true - }, - "js-tokens": { - "version": "4.0.0", - "dev": true - }, - "jsbn": { - "version": "0.1.1", - "dev": true - }, - "json-buffer": { - "version": "3.0.0", - "dev": true, - "optional": true - }, - "json-rpc-engine": { - "version": "3.8.0", - "dev": true, - "requires": { - "async": "^2.0.1", - "babel-preset-env": "^1.7.0", - "babelify": "^7.3.0", - "json-rpc-error": "^2.0.0", - "promise-to-callback": "^1.0.0", - "safe-event-emitter": "^1.0.1" - } - }, - "json-rpc-error": { - "version": "2.0.0", - "dev": true, - "requires": { - "inherits": "^2.0.1" - } - }, - "json-rpc-random-id": { - "version": "1.0.1", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "dev": true - }, - "jsonfile": { - "version": "4.0.0", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonify": { - "version": "0.0.0", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "keccak": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" - } - }, - "keyv": { - "version": "3.1.0", - "dev": true, - "optional": true, - "requires": { - "json-buffer": "3.0.0" - } - }, - "kind-of": { - "version": "6.0.3", - "dev": true - }, - "klaw-sync": { - "version": "6.0.0", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11" - } - }, - "level-codec": { - "version": "9.0.2", - "dev": true, - "requires": { - "buffer": "^5.6.0" - } - }, - "level-errors": { - "version": "2.0.1", - "dev": true, - "requires": { - "errno": "~0.1.1" - } - }, - "level-iterator-stream": { - "version": "2.0.3", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.5", - "xtend": "^4.0.0" - } - }, - "level-mem": { - "version": "3.0.1", - "dev": true, - "requires": { - "level-packager": "~4.0.0", - "memdown": "~3.0.0" - }, - "dependencies": { - "abstract-leveldown": { - "version": "5.0.0", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - }, - "ltgt": { - "version": "2.2.1", - "dev": true - }, - "memdown": { - "version": "3.0.0", - "dev": true, - "requires": { - "abstract-leveldown": "~5.0.0", - "functional-red-black-tree": "~1.0.1", - "immediate": "~3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "dev": true - } - } - }, - "level-packager": { - "version": "4.0.1", - "dev": true, - "requires": { - "encoding-down": "~5.0.0", - "levelup": "^3.0.0" - } - }, - "level-post": { - "version": "1.0.7", - "dev": true, - "requires": { - "ltgt": "^2.1.2" - } - }, - "level-sublevel": { - "version": "6.6.4", - "dev": true, - "requires": { - "bytewise": "~1.1.0", - "level-codec": "^9.0.0", - "level-errors": "^2.0.0", - "level-iterator-stream": "^2.0.3", - "ltgt": "~2.1.1", - "pull-defer": "^0.2.2", - "pull-level": "^2.0.3", - "pull-stream": "^3.6.8", - "typewiselite": "~1.0.0", - "xtend": "~4.0.0" - } - }, - "level-ws": { - "version": "1.0.0", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.2.8", - "xtend": "^4.0.1" - } - }, - "levelup": { - "version": "3.1.1", - "dev": true, - "requires": { - "deferred-leveldown": "~4.0.0", - "level-errors": "~2.0.0", - "level-iterator-stream": "~3.0.0", - "xtend": "~4.0.0" - }, - "dependencies": { - "level-iterator-stream": { - "version": "3.0.1", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "xtend": "^4.0.0" - } - } - } - }, - "lodash": { - "version": "4.17.20", - "dev": true - }, - "looper": { - "version": "2.0.0", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lowercase-keys": { - "version": "1.0.1", - "dev": true, - "optional": true - }, - "lru-cache": { - "version": "5.1.1", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "ltgt": { - "version": "2.1.3", - "dev": true - }, - "map-cache": { - "version": "0.2.2", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "md5.js": { - "version": "1.3.5", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "media-typer": { - "version": "0.3.0", - "dev": true, - "optional": true - }, - "merge-descriptors": { - "version": "1.0.1", - "dev": true, - "optional": true - }, - "merkle-patricia-tree": { - "version": "3.0.0", - "dev": true, - "requires": { - "async": "^2.6.1", - "ethereumjs-util": "^5.2.0", - "level-mem": "^3.0.1", - "level-ws": "^1.0.0", - "readable-stream": "^3.0.6", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" - }, - "dependencies": { - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "readable-stream": { - "version": "3.6.0", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "methods": { - "version": "1.1.2", - "dev": true, - "optional": true - }, - "miller-rabin": { - "version": "4.0.1", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, - "mime": { - "version": "1.6.0", - "dev": true, - "optional": true - }, - "mime-db": { - "version": "1.45.0", - "dev": true - }, - "mime-types": { - "version": "2.1.28", - "dev": true, - "requires": { - "mime-db": "1.45.0" - } - }, - "mimic-response": { - "version": "1.0.1", - "dev": true, - "optional": true - }, - "min-document": { - "version": "2.19.0", - "dev": true, - "requires": { - "dom-walk": "^0.1.0" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "dev": true - }, - "minizlib": { - "version": "1.3.3", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "mixin-deep": { - "version": "1.3.2", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - } - }, - "mkdirp": { - "version": "0.5.5", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "mkdirp-promise": { - "version": "5.0.1", - "dev": true, - "optional": true, - "requires": { - "mkdirp": "*" - } - }, - "mock-fs": { - "version": "4.13.0", - "dev": true, - "optional": true - }, - "ms": { - "version": "2.1.3", - "dev": true - }, - "multibase": { - "version": "0.6.1", - "dev": true, - "optional": true, - "requires": { - "base-x": "^3.0.8", - "buffer": "^5.5.0" - } - }, - "multicodec": { - "version": "0.5.7", - "dev": true, - "optional": true, - "requires": { - "varint": "^5.0.0" - } - }, - "multihashes": { - "version": "0.4.21", - "dev": true, - "optional": true, - "requires": { - "buffer": "^5.5.0", - "multibase": "^0.7.0", - "varint": "^5.0.0" - }, - "dependencies": { - "multibase": { - "version": "0.7.0", - "dev": true, - "optional": true, - "requires": { - "base-x": "^3.0.8", - "buffer": "^5.5.0" - } - } - } - }, - "nano-json-stream-parser": { - "version": "0.1.2", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "negotiator": { - "version": "0.6.2", - "dev": true, - "optional": true - }, - "next-tick": { - "version": "1.0.0", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "dev": true - }, - "node-addon-api": { - "version": "2.0.2", - "bundled": true, - "dev": true - }, - "node-fetch": { - "version": "2.1.2", - "dev": true - }, - "node-gyp-build": { - "version": "4.2.3", - "bundled": true, - "dev": true - }, - "normalize-url": { - "version": "4.5.0", - "dev": true, - "optional": true - }, - "number-to-bn": { - "version": "1.7.0", - "dev": true, - "optional": true, - "requires": { - "bn.js": "4.11.6", - "strip-hex-prefix": "1.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.11.6", - "dev": true, - "optional": true - } - } - }, - "oauth-sign": { - "version": "0.9.0", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-buffer": { - "version": "1.1.6", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-descriptor": { - "version": "0.1.6", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "dev": true - } - } - }, - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-inspect": { - "version": "1.9.0", - "dev": true - }, - "object-is": { - "version": "1.1.4", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - } - }, - "object-keys": { - "version": "1.1.1", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.2", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.1", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - } - }, - "object.pick": { - "version": "1.3.0", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "oboe": { - "version": "2.1.4", - "dev": true, - "optional": true, - "requires": { - "http-https": "^1.0.0" - } - }, - "on-finished": { - "version": "2.3.0", - "dev": true, - "optional": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "dev": true - }, - "p-cancelable": { - "version": "1.1.0", - "dev": true, - "optional": true - }, - "p-timeout": { - "version": "1.2.1", - "dev": true, - "optional": true, - "requires": { - "p-finally": "^1.0.0" - }, - "dependencies": { - "p-finally": { - "version": "1.0.0", - "dev": true, - "optional": true - } - } - }, - "parse-asn1": { - "version": "5.1.6", - "dev": true, - "optional": true, - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parse-headers": { - "version": "2.0.3", - "dev": true - }, - "parseurl": { - "version": "1.3.3", - "dev": true, - "optional": true - }, - "pascalcase": { - "version": "0.1.1", - "dev": true - }, - "patch-package": { - "version": "6.2.2", - "dev": true, - "requires": { - "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^2.4.2", - "cross-spawn": "^6.0.5", - "find-yarn-workspace-root": "^1.2.1", - "fs-extra": "^7.0.1", - "is-ci": "^2.0.0", - "klaw-sync": "^6.0.0", - "minimist": "^1.2.0", - "rimraf": "^2.6.3", - "semver": "^5.6.0", - "slash": "^2.0.0", - "tmp": "^0.0.33" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "path-key": { - "version": "2.0.1", - "dev": true - }, - "semver": { - "version": "5.7.1", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "dev": true - }, - "slash": { - "version": "2.0.0", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "which": { - "version": "1.3.1", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "dev": true, - "optional": true - }, - "pbkdf2": { - "version": "3.1.1", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "performance-now": { - "version": "2.1.0", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "dev": true - }, - "precond": { - "version": "0.2.3", - "dev": true - }, - "prepend-http": { - "version": "2.0.0", - "dev": true, - "optional": true - }, - "private": { - "version": "0.1.8", - "dev": true - }, - "process": { - "version": "0.11.10", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "dev": true - }, - "promise-to-callback": { - "version": "1.0.0", - "dev": true, - "requires": { - "is-fn": "^1.0.0", - "set-immediate-shim": "^1.0.1" - } - }, - "proxy-addr": { - "version": "2.0.6", - "dev": true, - "optional": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - } - }, - "prr": { - "version": "1.0.1", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "dev": true - }, - "psl": { - "version": "1.8.0", - "dev": true - }, - "public-encrypt": { - "version": "4.0.3", - "dev": true, - "optional": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "pull-cat": { - "version": "1.1.11", - "dev": true - }, - "pull-defer": { - "version": "0.2.3", - "dev": true - }, - "pull-level": { - "version": "2.0.4", - "dev": true, - "requires": { - "level-post": "^1.0.7", - "pull-cat": "^1.1.9", - "pull-live": "^1.0.1", - "pull-pushable": "^2.0.0", - "pull-stream": "^3.4.0", - "pull-window": "^2.1.4", - "stream-to-pull-stream": "^1.7.1" - } - }, - "pull-live": { - "version": "1.0.1", - "dev": true, - "requires": { - "pull-cat": "^1.1.9", - "pull-stream": "^3.4.0" - } - }, - "pull-pushable": { - "version": "2.2.0", - "dev": true - }, - "pull-stream": { - "version": "3.6.14", - "dev": true - }, - "pull-window": { - "version": "2.1.4", - "dev": true, - "requires": { - "looper": "^2.0.0" - } - }, - "pump": { - "version": "3.0.0", - "dev": true, - "optional": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "dev": true - }, - "qs": { - "version": "6.5.2", - "dev": true - }, - "query-string": { - "version": "5.1.1", - "dev": true, - "optional": true, - "requires": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "randombytes": { - "version": "2.1.0", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "dev": true, - "optional": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "dev": true, - "optional": true - }, - "raw-body": { - "version": "2.4.0", - "dev": true, - "optional": true, - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "readable-stream": { - "version": "2.3.7", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "dev": true - } - } - }, - "regenerate": { - "version": "1.4.2", - "dev": true - }, - "regenerator-runtime": { - "version": "0.11.1", - "dev": true - }, - "regenerator-transform": { - "version": "0.10.1", - "dev": true, - "requires": { - "babel-runtime": "^6.18.0", - "babel-types": "^6.19.0", - "private": "^0.1.6" - } - }, - "regex-not": { - "version": "1.0.2", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexp.prototype.flags": { - "version": "1.3.0", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "regexpu-core": { - "version": "2.0.0", - "dev": true, - "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" - } - }, - "regjsgen": { - "version": "0.2.0", - "dev": true - }, - "regjsparser": { - "version": "0.1.5", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "dev": true - } - } - }, - "repeat-element": { - "version": "1.1.3", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "request": { - "version": "2.88.2", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "resolve-url": { - "version": "0.2.1", - "dev": true - }, - "responselike": { - "version": "1.0.2", - "dev": true, - "optional": true, - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "resumer": { - "version": "0.0.0", - "dev": true, - "requires": { - "through": "~2.3.4" - } - }, - "ret": { - "version": "0.1.15", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "rlp": { - "version": "2.2.6", - "dev": true, - "requires": { - "bn.js": "^4.11.1" - } - }, - "rustbn.js": { - "version": "0.2.0", - "dev": true - }, - "safe-buffer": { - "version": "5.2.1", - "dev": true - }, - "safe-event-emitter": { - "version": "1.0.1", - "dev": true, - "requires": { - "events": "^3.0.0" - } - }, - "safe-regex": { - "version": "1.1.0", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "dev": true - }, - "scrypt-js": { - "version": "3.0.1", - "dev": true - }, - "scryptsy": { - "version": "1.2.1", - "dev": true, - "optional": true, - "requires": { - "pbkdf2": "^3.0.3" - } - }, - "secp256k1": { - "version": "4.0.2", - "dev": true, - "requires": { - "elliptic": "^6.5.2", - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" - } - }, - "seedrandom": { - "version": "3.0.1", - "dev": true - }, - "semaphore": { - "version": "1.1.0", - "dev": true - }, - "send": { - "version": "0.17.1", - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "dev": true, - "optional": true - } - } - }, - "ms": { - "version": "2.1.1", - "dev": true, - "optional": true - } - } - }, - "serve-static": { - "version": "1.14.1", - "dev": true, - "optional": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "servify": { - "version": "0.1.12", - "dev": true, - "optional": true, - "requires": { - "body-parser": "^1.16.0", - "cors": "^2.8.1", - "express": "^4.14.0", - "request": "^2.79.0", - "xhr": "^2.3.3" - } - }, - "set-immediate-shim": { - "version": "1.0.1", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "dev": true - } - } - }, - "setimmediate": { - "version": "1.0.5", - "dev": true - }, - "setprototypeof": { - "version": "1.1.1", - "dev": true, - "optional": true - }, - "sha.js": { - "version": "2.4.11", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "simple-concat": { - "version": "1.0.1", - "dev": true, - "optional": true - }, - "simple-get": { - "version": "2.8.1", - "dev": true, - "optional": true, - "requires": { - "decompress-response": "^3.3.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "snapdragon": { - "version": "0.8.2", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-buffer": { - "version": "1.1.6", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "dev": true - }, - "kind-of": { - "version": "5.1.0", - "dev": true - }, - "ms": { - "version": "2.0.0", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.3", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.12", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "dev": true - } - } - }, - "source-map-url": { - "version": "0.4.0", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sshpk": { - "version": "1.16.1", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "dependencies": { - "tweetnacl": { - "version": "0.14.5", - "dev": true - } - } - }, - "static-extend": { - "version": "0.1.2", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-buffer": { - "version": "1.1.6", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "dev": true - } - } - }, - "statuses": { - "version": "1.5.0", - "dev": true, - "optional": true - }, - "stream-to-pull-stream": { - "version": "1.7.3", - "dev": true, - "requires": { - "looper": "^3.0.0", - "pull-stream": "^3.2.3" - }, - "dependencies": { - "looper": { - "version": "3.0.0", - "dev": true - } - } - }, - "strict-uri-encode": { - "version": "1.1.0", - "dev": true, - "optional": true - }, - "string_decoder": { - "version": "1.1.1", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "dev": true - } - } - }, - "string.prototype.trim": { - "version": "1.2.3", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - } - }, - "string.prototype.trimend": { - "version": "1.0.3", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.3", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - } - }, - "strip-hex-prefix": { - "version": "1.0.0", - "dev": true, - "requires": { - "is-hex-prefixed": "1.0.0" - } - }, - "supports-color": { - "version": "5.5.0", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "swarm-js": { - "version": "0.1.40", - "dev": true, - "optional": true, - "requires": { - "bluebird": "^3.5.0", - "buffer": "^5.0.5", - "eth-lib": "^0.1.26", - "fs-extra": "^4.0.2", - "got": "^7.1.0", - "mime-types": "^2.1.16", - "mkdirp-promise": "^5.0.1", - "mock-fs": "^4.1.0", - "setimmediate": "^1.0.5", - "tar": "^4.0.2", - "xhr-request": "^1.0.1" - }, - "dependencies": { - "fs-extra": { - "version": "4.0.3", - "dev": true, - "optional": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "get-stream": { - "version": "3.0.0", - "dev": true, - "optional": true - }, - "got": { - "version": "7.1.0", - "dev": true, - "optional": true, - "requires": { - "decompress-response": "^3.2.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-plain-obj": "^1.1.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "p-cancelable": "^0.3.0", - "p-timeout": "^1.1.1", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "url-parse-lax": "^1.0.0", - "url-to-options": "^1.0.1" - } - }, - "is-stream": { - "version": "1.1.0", - "dev": true, - "optional": true - }, - "p-cancelable": { - "version": "0.3.0", - "dev": true, - "optional": true - }, - "prepend-http": { - "version": "1.0.4", - "dev": true, - "optional": true - }, - "url-parse-lax": { - "version": "1.0.0", - "dev": true, - "optional": true, - "requires": { - "prepend-http": "^1.0.1" - } - } - } - }, - "tape": { - "version": "4.13.3", - "dev": true, - "requires": { - "deep-equal": "~1.1.1", - "defined": "~1.0.0", - "dotignore": "~0.1.2", - "for-each": "~0.3.3", - "function-bind": "~1.1.1", - "glob": "~7.1.6", - "has": "~1.0.3", - "inherits": "~2.0.4", - "is-regex": "~1.0.5", - "minimist": "~1.2.5", - "object-inspect": "~1.7.0", - "resolve": "~1.17.0", - "resumer": "~0.0.0", - "string.prototype.trim": "~1.2.1", - "through": "~2.3.8" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "is-regex": { - "version": "1.0.5", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "object-inspect": { - "version": "1.7.0", - "dev": true - }, - "resolve": { - "version": "1.17.0", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - } - } - }, - "tar": { - "version": "4.4.13", - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "dependencies": { - "fs-minipass": { - "version": "1.2.7", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "minipass": { - "version": "2.9.0", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "through": { - "version": "2.3.8", - "dev": true - }, - "through2": { - "version": "2.0.5", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "timed-out": { - "version": "4.0.1", - "dev": true, - "optional": true - }, - "tmp": { - "version": "0.1.0", - "dev": true, - "requires": { - "rimraf": "^2.6.3" - } - }, - "to-object-path": { - "version": "0.3.0", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-readable-stream": { - "version": "1.0.0", - "dev": true, - "optional": true - }, - "to-regex": { - "version": "3.0.2", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "toidentifier": { - "version": "1.0.0", - "dev": true, - "optional": true - }, - "tough-cookie": { - "version": "2.5.0", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "trim-right": { - "version": "1.0.1", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "1.0.3", - "dev": true - }, - "tweetnacl-util": { - "version": "0.15.1", - "dev": true - }, - "type": { - "version": "1.2.0", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "dev": true, - "optional": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typewise": { - "version": "1.0.3", - "dev": true, - "requires": { - "typewise-core": "^1.2.0" - } - }, - "typewise-core": { - "version": "1.2.0", - "dev": true - }, - "typewiselite": { - "version": "1.0.0", - "dev": true - }, - "ultron": { - "version": "1.1.1", - "dev": true, - "optional": true - }, - "underscore": { - "version": "1.9.1", - "dev": true, - "optional": true - }, - "union-value": { - "version": "1.0.1", + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", + "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "0.1.1", - "dev": true - } + "glob": "^7.1.3" } - }, - "universalify": { - "version": "0.1.2", - "dev": true - }, - "unorm": { - "version": "1.6.0", - "dev": true - }, - "unpipe": { - "version": "1.0.0", - "dev": true, - "optional": true - }, - "unset-value": { - "version": "1.0.0", + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "follow-redirects": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fp-ts": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz", + "integrity": "sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==", + "dev": true + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + }, + "ganache": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/ganache/-/ganache-7.4.3.tgz", + "integrity": "sha512-RpEDUiCkqbouyE7+NMXG26ynZ+7sGiODU84Kz+FVoXUnQ4qQM4M8wif3Y4qUCt+D/eM1RVeGq0my62FPD6Y1KA==", + "dev": true, + "requires": { + "@trufflesuite/bigint-buffer": "1.1.10", + "@types/bn.js": "^5.1.0", + "@types/lru-cache": "5.1.1", + "@types/seedrandom": "3.0.1", + "bufferutil": "4.0.5", + "emittery": "0.10.0", + "keccak": "3.0.2", + "leveldown": "6.1.0", + "secp256k1": "4.0.3", + "utf-8-validate": "5.0.7" + }, + "dependencies": { + "@trufflesuite/bigint-buffer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@trufflesuite/bigint-buffer/-/bigint-buffer-1.1.10.tgz", + "integrity": "sha512-pYIQC5EcMmID74t26GCC67946mgTJFiLXOT/BYozgrd4UEY2JHEGLhWi9cMiQCt5BSqFEvKkCHNnoj82SRjiEw==", + "bundled": true, "dev": true, "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" + "node-gyp-build": "4.4.0" }, "dependencies": { - "has-value": { - "version": "0.3.1", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", + "node-gyp-build": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz", + "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==", + "bundled": true, "dev": true } } }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "dev": true - }, - "url-parse-lax": { - "version": "3.0.0", - "dev": true, - "optional": true, - "requires": { - "prepend-http": "^2.0.0" - } - }, - "url-set-query": { - "version": "1.0.0", - "dev": true, - "optional": true - }, - "url-to-options": { - "version": "1.0.1", - "dev": true, - "optional": true - }, - "use": { - "version": "3.1.1", - "dev": true - }, - "utf-8-validate": { - "version": "5.0.4", + "@types/bn.js": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.0.tgz", + "integrity": "sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA==", + "bundled": true, "dev": true, "requires": { - "node-gyp-build": "^4.2.0" + "@types/node": "*" } }, - "utf8": { - "version": "3.0.0", - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", + "@types/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==", + "bundled": true, "dev": true }, - "util.promisify": { - "version": "1.1.1", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "for-each": "^0.3.3", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.1" - } - }, - "utils-merge": { - "version": "1.0.1", - "dev": true, - "optional": true - }, - "uuid": { - "version": "3.4.0", + "@types/node": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.0.tgz", + "integrity": "sha512-eMhwJXc931Ihh4tkU+Y7GiLzT/y/DBNpNtr4yU9O2w3SYBsr9NaOPhQlLKRmoWtI54uNwuo0IOUFQjVOTZYRvw==", + "bundled": true, "dev": true }, - "varint": { - "version": "5.0.2", - "dev": true, - "optional": true - }, - "vary": { - "version": "1.1.2", - "dev": true, - "optional": true - }, - "verror": { - "version": "1.10.0", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "web3": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "web3-bzz": "1.2.11", - "web3-core": "1.2.11", - "web3-eth": "1.2.11", - "web3-eth-personal": "1.2.11", - "web3-net": "1.2.11", - "web3-shh": "1.2.11", - "web3-utils": "1.2.11" - } - }, - "web3-bzz": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "@types/node": "^12.12.6", - "got": "9.6.0", - "swarm-js": "^0.1.40", - "underscore": "1.9.1" - }, - "dependencies": { - "@types/node": { - "version": "12.19.12", - "dev": true, - "optional": true - } - } - }, - "web3-core": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "@types/bn.js": "^4.11.5", - "@types/node": "^12.12.6", - "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.2.11", - "web3-core-method": "1.2.11", - "web3-core-requestmanager": "1.2.11", - "web3-utils": "1.2.11" - }, - "dependencies": { - "@types/node": { - "version": "12.19.12", - "dev": true, - "optional": true - } - } - }, - "web3-core-helpers": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "underscore": "1.9.1", - "web3-eth-iban": "1.2.11", - "web3-utils": "1.2.11" - } - }, - "web3-core-method": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/transactions": "^5.0.0-beta.135", - "underscore": "1.9.1", - "web3-core-helpers": "1.2.11", - "web3-core-promievent": "1.2.11", - "web3-core-subscriptions": "1.2.11", - "web3-utils": "1.2.11" - } - }, - "web3-core-promievent": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "eventemitter3": "4.0.4" - } - }, - "web3-core-requestmanager": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "underscore": "1.9.1", - "web3-core-helpers": "1.2.11", - "web3-providers-http": "1.2.11", - "web3-providers-ipc": "1.2.11", - "web3-providers-ws": "1.2.11" - } - }, - "web3-core-subscriptions": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "eventemitter3": "4.0.4", - "underscore": "1.9.1", - "web3-core-helpers": "1.2.11" - } - }, - "web3-eth": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "underscore": "1.9.1", - "web3-core": "1.2.11", - "web3-core-helpers": "1.2.11", - "web3-core-method": "1.2.11", - "web3-core-subscriptions": "1.2.11", - "web3-eth-abi": "1.2.11", - "web3-eth-accounts": "1.2.11", - "web3-eth-contract": "1.2.11", - "web3-eth-ens": "1.2.11", - "web3-eth-iban": "1.2.11", - "web3-eth-personal": "1.2.11", - "web3-net": "1.2.11", - "web3-utils": "1.2.11" - } - }, - "web3-eth-abi": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "@ethersproject/abi": "5.0.0-beta.153", - "underscore": "1.9.1", - "web3-utils": "1.2.11" - } - }, - "web3-eth-accounts": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "crypto-browserify": "3.12.0", - "eth-lib": "0.2.8", - "ethereumjs-common": "^1.3.2", - "ethereumjs-tx": "^2.1.1", - "scrypt-js": "^3.0.1", - "underscore": "1.9.1", - "uuid": "3.3.2", - "web3-core": "1.2.11", - "web3-core-helpers": "1.2.11", - "web3-core-method": "1.2.11", - "web3-utils": "1.2.11" - }, - "dependencies": { - "eth-lib": { - "version": "0.2.8", - "dev": true, - "optional": true, - "requires": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "xhr-request-promise": "^0.1.2" - } - }, - "uuid": { - "version": "3.3.2", - "dev": true, - "optional": true - } - } + "@types/seedrandom": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.1.tgz", + "integrity": "sha512-giB9gzDeiCeloIXDgzFBCgjj1k4WxcDrZtGl6h1IqmUPlxF+Nx8Ve+96QCyDZ/HseB/uvDsKbpib9hU5cU53pw==", + "bundled": true, + "dev": true }, - "web3-eth-contract": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "@types/bn.js": "^4.11.5", - "underscore": "1.9.1", - "web3-core": "1.2.11", - "web3-core-helpers": "1.2.11", - "web3-core-method": "1.2.11", - "web3-core-promievent": "1.2.11", - "web3-core-subscriptions": "1.2.11", - "web3-eth-abi": "1.2.11", - "web3-utils": "1.2.11" - } + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "bundled": true, + "dev": true }, - "web3-eth-ens": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "content-hash": "^2.5.2", - "eth-ens-namehash": "2.0.8", - "underscore": "1.9.1", - "web3-core": "1.2.11", - "web3-core-helpers": "1.2.11", - "web3-core-promievent": "1.2.11", - "web3-eth-abi": "1.2.11", - "web3-eth-contract": "1.2.11", - "web3-utils": "1.2.11" - } + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "bundled": true, + "dev": true }, - "web3-eth-iban": { - "version": "1.2.11", + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "bundled": true, "dev": true, - "optional": true, "requires": { - "bn.js": "^4.11.9", - "web3-utils": "1.2.11" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "web3-eth-personal": { - "version": "1.2.11", + "bufferutil": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.5.tgz", + "integrity": "sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A==", "dev": true, "optional": true, "requires": { - "@types/node": "^12.12.6", - "web3-core": "1.2.11", - "web3-core-helpers": "1.2.11", - "web3-core-method": "1.2.11", - "web3-net": "1.2.11", - "web3-utils": "1.2.11" - }, - "dependencies": { - "@types/node": { - "version": "12.19.12", - "dev": true, - "optional": true - } + "node-gyp-build": "^4.3.0" } }, - "web3-net": { - "version": "1.2.11", + "catering": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.0.tgz", + "integrity": "sha512-M5imwzQn6y+ODBfgi+cfgZv2hIUI6oYU/0f35Mdb1ujGeqeoI5tOnl9Q13DTH7LW+7er+NYq8stNOKZD/Z3U/A==", + "bundled": true, "dev": true, - "optional": true, "requires": { - "web3-core": "1.2.11", - "web3-core-method": "1.2.11", - "web3-utils": "1.2.11" + "queue-tick": "^1.0.0" } }, - "web3-provider-engine": { - "version": "14.2.1", + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "bundled": true, "dev": true, "requires": { - "async": "^2.5.0", - "backoff": "^2.5.0", - "clone": "^2.0.0", - "cross-fetch": "^2.1.0", - "eth-block-tracker": "^3.0.0", - "eth-json-rpc-infura": "^3.1.0", - "eth-sig-util": "3.0.0", - "ethereumjs-block": "^1.2.2", - "ethereumjs-tx": "^1.2.0", - "ethereumjs-util": "^5.1.5", - "ethereumjs-vm": "^2.3.4", - "json-rpc-error": "^2.0.0", - "json-stable-stringify": "^1.0.1", - "promise-to-callback": "^1.0.0", - "readable-stream": "^2.2.9", - "request": "^2.85.0", - "semaphore": "^1.0.3", - "ws": "^5.1.1", - "xhr": "^2.2.0", - "xtend": "^4.0.1" + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { - "abstract-leveldown": { - "version": "2.6.3", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - }, - "deferred-leveldown": { - "version": "1.2.2", - "dev": true, - "requires": { - "abstract-leveldown": "~2.6.0" - } - }, - "eth-sig-util": { - "version": "1.4.2", - "dev": true, - "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git", - "ethereumjs-util": "^5.1.1" - } - }, - "ethereumjs-account": { - "version": "2.0.5", - "dev": true, - "requires": { - "ethereumjs-util": "^5.0.0", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "ethereumjs-block": { - "version": "1.7.1", - "dev": true, - "requires": { - "async": "^2.0.1", - "ethereum-common": "0.2.0", - "ethereumjs-tx": "^1.2.2", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - }, - "dependencies": { - "ethereum-common": { - "version": "0.2.0", - "dev": true - } - } - }, - "ethereumjs-tx": { - "version": "1.3.7", - "dev": true, - "requires": { - "ethereum-common": "^0.0.18", - "ethereumjs-util": "^5.0.0" - } - }, - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - }, - "ethereumjs-vm": { - "version": "2.6.0", - "dev": true, - "requires": { - "async": "^2.1.2", - "async-eventemitter": "^0.2.2", - "ethereumjs-account": "^2.0.3", - "ethereumjs-block": "~2.2.0", - "ethereumjs-common": "^1.1.0", - "ethereumjs-util": "^6.0.0", - "fake-merkle-patricia-tree": "^1.0.1", - "functional-red-black-tree": "^1.0.1", - "merkle-patricia-tree": "^2.3.2", - "rustbn.js": "~0.2.0", - "safe-buffer": "^5.1.1" - }, - "dependencies": { - "ethereumjs-block": { - "version": "2.2.2", - "dev": true, - "requires": { - "async": "^2.0.1", - "ethereumjs-common": "^1.5.0", - "ethereumjs-tx": "^2.1.1", - "ethereumjs-util": "^5.0.0", - "merkle-patricia-tree": "^2.1.2" - }, - "dependencies": { - "ethereumjs-util": { - "version": "5.2.1", - "dev": true, - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "^0.1.3", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1" - } - } - } - }, - "ethereumjs-tx": { - "version": "2.1.2", - "dev": true, - "requires": { - "ethereumjs-common": "^1.5.0", - "ethereumjs-util": "^6.0.0" - } - }, - "ethereumjs-util": { - "version": "6.2.1", - "dev": true, - "requires": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" - } - } - } - }, - "isarray": { - "version": "0.0.1", - "dev": true - }, - "level-codec": { - "version": "7.0.1", - "dev": true - }, - "level-errors": { - "version": "1.0.5", - "dev": true, - "requires": { - "errno": "~0.1.1" - } - }, - "level-iterator-stream": { - "version": "1.3.1", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "level-errors": "^1.0.3", - "readable-stream": "^1.0.33", - "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - } - } - }, - "level-ws": { - "version": "0.0.0", - "dev": true, - "requires": { - "readable-stream": "~1.0.15", - "xtend": "~2.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "xtend": { - "version": "2.1.2", - "dev": true, - "requires": { - "object-keys": "~0.4.0" - } - } - } - }, - "levelup": { - "version": "1.3.9", - "dev": true, - "requires": { - "deferred-leveldown": "~1.2.1", - "level-codec": "~7.0.0", - "level-errors": "~1.0.3", - "level-iterator-stream": "~1.3.0", - "prr": "~1.0.1", - "semver": "~5.4.1", - "xtend": "~4.0.0" - } - }, - "ltgt": { - "version": "2.2.1", - "dev": true - }, - "memdown": { - "version": "1.4.1", - "dev": true, - "requires": { - "abstract-leveldown": "~2.7.1", - "functional-red-black-tree": "^1.0.1", - "immediate": "^3.2.3", - "inherits": "~2.0.1", - "ltgt": "~2.2.0", - "safe-buffer": "~5.1.1" - }, - "dependencies": { - "abstract-leveldown": { - "version": "2.7.2", - "dev": true, - "requires": { - "xtend": "~4.0.0" - } - } - } - }, - "merkle-patricia-tree": { - "version": "2.3.2", - "dev": true, - "requires": { - "async": "^1.4.2", - "ethereumjs-util": "^5.0.0", - "level-ws": "0.0.0", - "levelup": "^1.2.1", - "memdown": "^1.0.0", - "readable-stream": "^2.0.0", - "rlp": "^2.0.0", - "semaphore": ">=1.0.1" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "dev": true - } - } - }, - "object-keys": { - "version": "0.4.0", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "dev": true - }, - "semver": { - "version": "5.4.1", - "dev": true - }, - "string_decoder": { - "version": "0.10.31", + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "bundled": true, "dev": true - }, - "ws": { - "version": "5.2.2", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } } } }, - "web3-providers-http": { - "version": "1.2.11", - "dev": true, - "optional": true, - "requires": { - "web3-core-helpers": "1.2.11", - "xhr2-cookies": "1.1.0" - } + "emittery": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.0.tgz", + "integrity": "sha512-AGvFfs+d0JKCJQ4o01ASQLGPmSCxgfU9RFXvzPvZdjKK8oscynksuJhWrSTSw7j7Ep/sZct5b5ZhYCi8S/t0HQ==", + "bundled": true, + "dev": true }, - "web3-providers-ipc": { - "version": "1.2.11", + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "bundled": true, "dev": true, - "optional": true, "requires": { - "oboe": "2.1.4", - "underscore": "1.9.1", - "web3-core-helpers": "1.2.11" + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" } }, - "web3-providers-ws": { - "version": "1.2.11", + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "bundled": true, "dev": true, - "optional": true, "requires": { - "eventemitter3": "4.0.4", - "underscore": "1.9.1", - "web3-core-helpers": "1.2.11", - "websocket": "^1.0.31" + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, - "web3-shh": { - "version": "1.2.11", + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "bundled": true, + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "bundled": true, + "dev": true + }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "bundled": true, + "dev": true + }, + "keccak": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", + "integrity": "sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==", + "bundled": true, "dev": true, - "optional": true, "requires": { - "web3-core": "1.2.11", - "web3-core-method": "1.2.11", - "web3-core-subscriptions": "1.2.11", - "web3-net": "1.2.11" + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" } }, - "web3-utils": { - "version": "1.2.11", + "leveldown": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-6.1.0.tgz", + "integrity": "sha512-8C7oJDT44JXxh04aSSsfcMI8YiaGRhOFI9/pMEL7nWJLVsWajDPTRxsSHTM2WcTVY5nXM+SuRHzPPi0GbnDX+w==", + "bundled": true, "dev": true, - "optional": true, "requires": { - "bn.js": "^4.11.9", - "eth-lib": "0.2.8", - "ethereum-bloom-filters": "^1.0.6", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "underscore": "1.9.1", - "utf8": "3.0.0" + "abstract-leveldown": "^7.2.0", + "napi-macros": "~2.0.0", + "node-gyp-build": "^4.3.0" }, "dependencies": { - "eth-lib": { - "version": "0.2.8", + "abstract-leveldown": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz", + "integrity": "sha512-DnhQwcFEaYsvYDnACLZhMmCWd3rkOeEvglpa4q5i/5Jlm3UIsWaxVzuXvDLFCSCWRO3yy2/+V/G7FusFgejnfQ==", + "bundled": true, "dev": true, - "optional": true, "requires": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "xhr-request-promise": "^0.1.2" + "buffer": "^6.0.3", + "catering": "^2.0.0", + "is-buffer": "^2.0.5", + "level-concat-iterator": "^3.0.0", + "level-supports": "^2.0.1", + "queue-microtask": "^1.2.3" } - } - } - }, - "websocket": { - "version": "1.0.32", - "dev": true, - "requires": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.50", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" - }, - "dependencies": { - "debug": { - "version": "2.6.9", + }, + "level-concat-iterator": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-3.1.0.tgz", + "integrity": "sha512-BWRCMHBxbIqPxJ8vHOvKUsaO0v1sLYZtjN3K2iZJsRBYtp+ONsY6Jfi6hy9K3+zolgQRryhIn2NRZjZnWJ9NmQ==", + "bundled": true, "dev": true, "requires": { - "ms": "2.0.0" + "catering": "^2.1.0" } }, - "ms": { - "version": "2.0.0", + "level-supports": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-2.1.0.tgz", + "integrity": "sha512-E486g1NCjW5cF78KGPrMDRBYzPuueMZ6VBXHT6gC7A8UYWGiM14fGgp+s/L1oFfDWSPV/+SFkYCmZ0SiESkRKA==", + "bundled": true, "dev": true } } }, - "whatwg-fetch": { - "version": "2.0.4", + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "bundled": true, "dev": true }, - "wrappy": { - "version": "1.0.2", + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "bundled": true, "dev": true }, - "ws": { - "version": "3.3.3", - "dev": true, - "optional": true, - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "dev": true, - "optional": true - } - } + "napi-macros": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "bundled": true, + "dev": true + }, + "node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "bundled": true, + "dev": true + }, + "node-gyp-build": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", + "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", + "bundled": true, + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "bundled": true, + "dev": true + }, + "queue-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.0.tgz", + "integrity": "sha512-ULWhjjE8BmiICGn3G8+1L9wFpERNxkf8ysxkAer4+TFdRefDaXOCV5m92aMB9FtBVmn/8sETXLXY6BfW7hyaWQ==", + "bundled": true, + "dev": true }, - "xhr": { - "version": "2.6.0", + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "bundled": true, "dev": true, "requires": { - "global": "~4.4.0", - "is-function": "^1.0.1", - "parse-headers": "^2.0.0", - "xtend": "^4.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, - "xhr-request": { - "version": "1.1.0", + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "bundled": true, + "dev": true + }, + "secp256k1": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", + "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", + "bundled": true, "dev": true, - "optional": true, "requires": { - "buffer-to-arraybuffer": "^0.0.5", - "object-assign": "^4.1.1", - "query-string": "^5.0.1", - "simple-get": "^2.7.0", - "timed-out": "^4.0.1", - "url-set-query": "^1.0.0", - "xhr": "^2.0.4" + "elliptic": "^6.5.4", + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" } }, - "xhr-request-promise": { - "version": "0.1.3", + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "bundled": true, "dev": true, - "optional": true, "requires": { - "xhr-request": "^1.1.0" + "safe-buffer": "~5.2.0" } }, - "xhr2-cookies": { - "version": "1.1.0", + "utf-8-validate": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz", + "integrity": "sha512-vLt1O5Pp+flcArHGIyKEQq883nBt8nN8tVBcoL0qUXj2XT1n7p70yGIq2VK98I5FdZ1YHc0wk/koOnHjnXWk1Q==", "dev": true, "optional": true, "requires": { - "cookiejar": "^2.1.1" + "node-gyp-build": "^4.3.0" } }, - "xtend": { - "version": "4.0.2", - "dev": true - }, - "yaeti": { - "version": "0.0.6", - "dev": true - }, - "yallist": { - "version": "3.1.1", + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "bundled": true, "dev": true } } @@ -29427,6 +14483,7 @@ "resolved": "https://registry.npmjs.org/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz", "integrity": "sha512-PWoF9Keq6laYdIRwwCdhTPl60xRqAloYNMQLiyUnG42VjT53oW07BXIRM+NK7eQjzXjAk2gUvX9caRxlnF9TAA==", "dev": true, + "peer": true, "requires": { "punycode": "2.1.0" } @@ -29543,8 +14600,9 @@ "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", + "dev": true, + "peer": true }, "io-ts": { "version": "1.10.4", @@ -29601,15 +14659,6 @@ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", "dev": true }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, "is-core-module": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", @@ -29633,12 +14682,6 @@ "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", "dev": true }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -29749,8 +14792,9 @@ "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true, + "peer": true }, "is-weakref": { "version": "1.0.2", @@ -29761,15 +14805,6 @@ "call-bind": "^1.0.2" } }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "requires": { - "is-docker": "^2.0.0" - } - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -29817,6 +14852,15 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -29931,20 +14975,12 @@ "graceful-fs": "^4.1.9" } }, - "klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11" - } - }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", "dev": true, + "peer": true, "requires": { "invert-kv": "^1.0.0" } @@ -30097,8 +15133,9 @@ "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "dev": true, + "peer": true, "requires": { "graceful-fs": "^4.1.2", "parse-json": "^2.2.0", @@ -30110,8 +15147,9 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "peer": true } } }, @@ -30133,7 +15171,14 @@ "lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==", + "dev": true, + "peer": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, "lodash.debounce": { @@ -30379,16 +15424,6 @@ } } }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, "miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", @@ -30697,6 +15732,7 @@ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, + "peer": true, "requires": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -30705,10 +15741,11 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "peer": true } } }, @@ -30721,14 +15758,16 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "dev": true, + "peer": true }, "number-to-bn": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", - "integrity": "sha1-uzYjWS9+X54AMLGXe9QaDFP+HqA=", + "integrity": "sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==", "dev": true, + "peer": true, "requires": { "bn.js": "4.11.6", "strip-hex-prefix": "1.0.0" @@ -30737,8 +15776,9 @@ "bn.js": { "version": "4.11.6", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=", - "dev": true + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "dev": true, + "peer": true } } }, @@ -30806,16 +15846,6 @@ "mimic-fn": "^1.0.0" } }, - "open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, - "requires": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - } - }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -30833,8 +15863,9 @@ "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", "dev": true, + "peer": true, "requires": { "lcid": "^1.0.0" } @@ -30894,47 +15925,13 @@ "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", "dev": true, + "peer": true, "requires": { "error-ex": "^1.2.0" } }, - "patch-package": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.4.7.tgz", - "integrity": "sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==", - "dev": true, - "requires": { - "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^2.4.2", - "cross-spawn": "^6.0.5", - "find-yarn-workspace-root": "^2.0.0", - "fs-extra": "^7.0.1", - "is-ci": "^2.0.0", - "klaw-sync": "^6.0.0", - "minimist": "^1.2.0", - "open": "^7.4.2", - "rimraf": "^2.6.3", - "semver": "^5.6.0", - "slash": "^2.0.0", - "tmp": "^0.0.33" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - } - } - }, "path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -30973,8 +15970,9 @@ "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", "dev": true, + "peer": true, "requires": { "graceful-fs": "^4.1.2", "pify": "^2.0.0", @@ -30984,8 +15982,9 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "peer": true } } }, @@ -31031,24 +16030,20 @@ "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "peer": true }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, + "peer": true, "requires": { "pinkie": "^2.0.0" } }, - "postinstall-postinstall": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz", - "integrity": "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ==", - "dev": true - }, "precond": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", @@ -31113,9 +16108,9 @@ "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" }, "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dev": true, "requires": { "side-channel": "^1.0.4" @@ -31150,8 +16145,9 @@ "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", "dev": true, + "peer": true, "requires": { "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", @@ -31161,8 +16157,9 @@ "read-pkg-up": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", "dev": true, + "peer": true, "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" @@ -31171,8 +16168,9 @@ "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "dev": true, + "peer": true, "requires": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" @@ -31181,8 +16179,9 @@ "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", "dev": true, + "peer": true, "requires": { "pinkie-promise": "^2.0.0" } @@ -31212,6 +16211,12 @@ "picomatch": "^2.2.1" } }, + "reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true + }, "regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", @@ -31402,6 +16407,12 @@ "node-gyp-build": "^4.2.0" } }, + "seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true + }, "semaphore": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", @@ -31499,47 +16510,30 @@ } }, "solc": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/solc/-/solc-0.6.12.tgz", - "integrity": "sha512-Lm0Ql2G9Qc7yPP2Ba+WNmzw2jwsrd3u4PobHYlSOxaut3TtUbj9+5ZrT6f4DUpNPEoBaFUOEg9Op9C0mk7ge9g==", + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.15.tgz", + "integrity": "sha512-Riv0GNHNk/SddN/JyEuFKwbcWcEeho15iyupTSHw5Np6WuXA5D8kEHbyzDHi6sqmvLzu2l+8b1YmL8Ytple+8w==", "dev": true, "requires": { "command-exists": "^1.2.8", - "commander": "3.0.2", - "fs-extra": "^0.30.0", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", "js-sha3": "0.8.0", "memorystream": "^0.3.1", - "require-from-string": "^2.0.0", "semver": "^5.5.0", "tmp": "0.0.33" }, "dependencies": { - "fs-extra": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", - "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" - } - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -31615,10 +16609,11 @@ } }, "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, + "peer": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -31628,23 +16623,26 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true + "dev": true, + "peer": true }, "spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, + "peer": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "spdx-license-ids": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", - "dev": true + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true, + "peer": true }, "sprintf-js": { "version": "1.0.3", @@ -31706,6 +16704,12 @@ "safe-buffer": "~5.1.0" } }, + "string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", + "dev": true + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -31748,8 +16752,9 @@ "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", "dev": true, + "peer": true, "requires": { "is-utf8": "^0.2.0" } @@ -31821,24 +16826,29 @@ } } }, - "test-value": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", - "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=", + "table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", "dev": true, "requires": { - "array-back": "^1.0.3", - "typical": "^2.6.0" + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" }, "dependencies": { "array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", - "dev": true, - "requires": { - "typical": "^2.6.0" - } + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true + }, + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true } } }, @@ -31846,7 +16856,8 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/testrpc/-/testrpc-0.0.1.tgz", "integrity": "sha512-afH1hO+SQ/VPlmaLUFj2636QMeDvPCeQMc/9RBMW0IfjNe9gFD9Ra3ShqYkB7py0do1ZcCna/9acHyzTJ+GcNA==", - "dev": true + "dev": true, + "peer": true }, "text-table": { "version": "0.2.0", @@ -31929,29 +16940,76 @@ "querystring": "^0.2.1" } }, - "ts-essentials": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-1.0.4.tgz", - "integrity": "sha512-q3N1xS4vZpRouhYHDPwO0bDW3EZ6SK9CrrDHxi/D6BPReSjpVgWIOpLS2o0gSBZm+7q/wyKp6RVM1AeeW7uyfQ==", - "dev": true - }, - "ts-generator": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ts-generator/-/ts-generator-0.1.1.tgz", - "integrity": "sha512-N+ahhZxTLYu1HNTQetwWcx3so8hcYbkKBHTr4b4/YgObFTIKkOSSsaa+nal12w8mfrJAyzJfETXawbNjSfP2gQ==", + "ts-command-line-args": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", + "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", "dev": true, "requires": { - "@types/mkdirp": "^0.5.2", - "@types/prettier": "^2.1.1", - "@types/resolve": "^0.0.8", - "chalk": "^2.4.1", - "glob": "^7.1.2", - "mkdirp": "^0.5.1", - "prettier": "^2.1.2", - "resolve": "^1.8.1", - "ts-essentials": "^1.0.0" + "chalk": "^4.1.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.1.0", + "string-format": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, + "ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", + "dev": true, + "requires": {} + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -32006,40 +17064,56 @@ "dev": true }, "typechain": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/typechain/-/typechain-3.0.0.tgz", - "integrity": "sha512-ft4KVmiN3zH4JUFu2WJBrwfHeDf772Tt2d8bssDTo/YcckKW2D+OwFrHXRC6hJvO3mHjFQTihoMV6fJOi0Hngg==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.1.tgz", + "integrity": "sha512-fA7clol2IP/56yq6vkMTR+4URF1nGjV82Wx6Rf09EsqD4tkzMAvEaqYxVFCavJm/1xaRga/oD55K+4FtuXwQOQ==", "dev": true, "requires": { - "command-line-args": "^4.0.7", - "debug": "^4.1.1", + "@types/prettier": "^2.1.1", + "debug": "^4.3.1", "fs-extra": "^7.0.0", + "glob": "7.1.7", "js-sha3": "^0.8.0", "lodash": "^4.17.15", - "ts-essentials": "^6.0.3", - "ts-generator": "^0.1.1" + "mkdirp": "^1.0.4", + "prettier": "^2.3.1", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" }, "dependencies": { - "ts-essentials": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-6.0.7.tgz", - "integrity": "sha512-2E4HIIj4tQJlIHuATRHayv0EfMGK3ris/GRk1E3CFnsZzeNV+hUmelbaTZHLtXaZppM5oLhHRtO04gINC4Jusw==", + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "dev": true, - "requires": {} + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true } } }, "typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "peer": true }, "typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", "dev": true }, "unbox-primitive": { @@ -32075,25 +17149,19 @@ } }, "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.1.tgz", + "integrity": "sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==", "dev": true, "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" + "punycode": "^1.4.1", + "qs": "^6.11.0" }, "dependencies": { "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "dev": true } } @@ -32118,6 +17186,7 @@ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, + "peer": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -32141,12 +17210,13 @@ } }, "web3-utils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.0.tgz", - "integrity": "sha512-O8Tl4Ky40Sp6pe89Olk2FsaUkgHyb5QAXuaKo38ms3CxZZ4d3rPGfjP9DNKGm5+IUgAZBNpF1VmlSmNCqfDI1w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.0.tgz", + "integrity": "sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg==", "dev": true, + "peer": true, "requires": { - "bn.js": "^4.11.9", + "bn.js": "^5.2.1", "ethereum-bloom-filters": "^1.0.6", "ethereumjs-util": "^7.1.0", "ethjs-unit": "0.1.6", @@ -32155,25 +17225,25 @@ "utf8": "3.0.0" }, "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true, + "peer": true + }, "ethereumjs-util": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz", - "integrity": "sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz", + "integrity": "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==", "dev": true, + "peer": true, "requires": { "@types/bn.js": "^5.1.0", "bn.js": "^5.1.2", "create-hash": "^1.1.2", "ethereum-cryptography": "^0.1.3", "rlp": "^2.2.4" - }, - "dependencies": { - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - } } } } @@ -32239,8 +17309,9 @@ "window-size": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", - "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=", - "dev": true + "integrity": "sha512-UD7d8HFA2+PZsbKyaOCEy8gMh1oDtHgJh1LfgjQ4zVXmYjAT/kvz3PueITKuqDiIXQe7yzpPnxX3lNc+AhQMyw==", + "dev": true, + "peer": true }, "word-wrap": { "version": "1.2.3", @@ -32248,6 +17319,24 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dev": true, + "requires": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "dependencies": { + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true + } + } + }, "wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", diff --git a/contracts/package.json b/contracts/package.json index 028f73d313..8240cdb7a8 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -11,10 +11,10 @@ "devDependencies": { "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-etherscan": "^2.1.1", - "@nomiclabs/hardhat-waffle": "^2.0.1", + "@nomiclabs/hardhat-waffle": "^2.0.6", "@openzeppelin/contracts": "^3.2.0", "chai": "^4.3.4", - "ethereum-waffle": "^3.4.0", + "ethereum-waffle": "^4.0.10", "ethers": "^5.4.7", "hardhat": "^2.6.5", "prettier": "^2.5.1", From f2f2b107393535bdb0493bfaa2a672a77ac70444 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 12:22:16 +0000 Subject: [PATCH 1282/1392] build(deps): bump github.com/gin-gonic/gin from 1.7.0 to 1.9.1 Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.7.0 to 1.9.1. - [Release notes](https://github.com/gin-gonic/gin/releases) - [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md) - [Commits](https://github.com/gin-gonic/gin/compare/v1.7.0...v1.9.1) --- updated-dependencies: - dependency-name: github.com/gin-gonic/gin dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 49 ++++++++++----------- go.sum | 131 ++++++++++++++++++++++----------------------------------- 2 files changed, 74 insertions(+), 106 deletions(-) diff --git a/go.mod b/go.mod index f808adeaff..24d0d4add8 100644 --- a/go.mod +++ b/go.mod @@ -14,13 +14,12 @@ require ( github.com/cenkalti/backoff/v4 v4.2.0 github.com/cheggaaa/pb/v3 v3.0.8 github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 - github.com/ethereum/go-ethereum v1.10.23 github.com/evanphx/json-patch/v5 v5.6.0 github.com/fatih/camelcase v1.0.0 github.com/fatih/color v1.13.0 github.com/gertd/go-pluralize v0.2.1 github.com/gin-contrib/cors v1.3.1 - github.com/gin-gonic/gin v1.7.0 + github.com/gin-gonic/gin v1.9.1 github.com/go-redis/redis/v8 v8.8.0 github.com/go-sql-driver/mysql v1.6.0 github.com/gofrs/flock v0.8.1 @@ -47,7 +46,7 @@ require ( github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.3 github.com/valyala/fastjson v1.5.1 github.com/wcharczuk/go-chart/v2 v2.1.0 github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 @@ -60,7 +59,7 @@ require ( gonum.org/v1/gonum v0.8.2 google.golang.org/api v0.111.0 google.golang.org/grpc v1.53.0 - google.golang.org/protobuf v1.29.1 + google.golang.org/protobuf v1.30.0 gopkg.in/tucnak/telebot.v2 v2.5.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -68,29 +67,27 @@ require ( require ( cloud.google.com/go/compute v1.18.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect github.com/VividCortex/ewma v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-simplejson v0.5.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect - github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cockroachdb/apd v1.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/deckarep/golang-set v1.8.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/denisenkom/go-mssqldb v0.12.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-ole/go-ole v1.2.1 // indirect - github.com/go-playground/locales v0.13.0 // indirect - github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/go-playground/validator/v10 v10.4.1 // indirect - github.com/go-stack/stack v1.8.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-test/deep v1.0.6 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect @@ -102,14 +99,15 @@ require ( github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/leodido/go-urn v1.2.1 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/lestrrat-go/strftime v1.0.0 // indirect github.com/lib/pq v1.10.6 // indirect github.com/magiconair/properties v1.8.4 // indirect github.com/mattn/go-colorable v0.1.9 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-sqlite3 v1.14.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect @@ -118,41 +116,40 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml v1.8.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/rjeczalik/notify v0.9.1 // indirect github.com/rollbar/rollbar-go v1.4.5 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/afero v1.5.1 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/tebeka/strftime v0.1.3 // indirect - github.com/tklauser/go-sysconf v0.3.5 // indirect - github.com/tklauser/numcpus v0.2.2 // indirect - github.com/ugorji/go/codec v1.2.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect github.com/ziutek/mymysql v1.5.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v0.19.0 // indirect go.opentelemetry.io/otel/metric v0.19.0 // indirect go.opentelemetry.io/otel/trace v0.19.0 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 // indirect golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect golang.org/x/mod v0.9.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/term v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect golang.org/x/tools v0.7.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488 // indirect gopkg.in/ini.v1 v1.62.0 // indirect - gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index cdc1af5feb..0fd21efa46 100644 --- a/go.sum +++ b/go.sum @@ -50,9 +50,6 @@ github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvd github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= -github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/adshao/go-binance/v2 v2.4.2 h1:NBNMUyXrci45v3sr0RkZosiBYSw1/yuqCrJNkyEM8U0= @@ -80,9 +77,9 @@ github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= -github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/c-bata/goptuna v0.8.1 h1:25+n1MLv0yvCsD56xv4nqIus3oLHL9GuPAZDLIqmX1U= github.com/c-bata/goptuna v0.8.1/go.mod h1:knmS8+Iyq5PPy1YUeIEq0pMFR4Y6x7z/CySc9HlZTCY= github.com/c9s/requestgen v1.3.4 h1:kK2rIO3OAt9JoY5gT0OSkSpq0dy/+JeuI22FwSKpUrY= @@ -92,13 +89,15 @@ github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b/go.mod h1:EKObf66 github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0= github.com/chewxy/math32 v1.0.0/go.mod h1:Miac6hA1ohdDUTagnvJy/q+aNnEk16qWUdb8ZVhvCN0= github.com/chewxy/math32 v1.0.6/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= @@ -130,12 +129,6 @@ github.com/cznic/xc v0.0.0-20181122101856-45b06973881e/go.mod h1:3oFoiOvCDBYH+sw github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= -github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.12.2 h1:1OcPn5GBIobjWNd+8yjfHNIaFX14B1pWI3F9HZy5KXw= @@ -145,14 +138,11 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= -github.com/ethereum/go-ethereum v1.10.23 h1:Xk8XAT4/UuqcjMLIMF+7imjkg32kfVFKoeyQDaO2yWM= -github.com/ethereum/go-ethereum v1.10.23/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= @@ -163,12 +153,12 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -177,8 +167,8 @@ github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzF github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= -github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU= -github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -189,36 +179,33 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= -github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis/v8 v8.8.0 h1:fDZP58UN/1RD3DjtTXP/fFZ04TFohSYhjZDkcDe2dnw= github.com/go-redis/redis/v8 v8.8.0/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqWMnCV1iP5Y= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8= github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= @@ -259,7 +246,6 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -312,7 +298,6 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -325,7 +310,6 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -334,10 +318,7 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/heroku/rollrus v0.2.0 h1:b3AgcXJKFJNUwbQOC2S69/+mxuTpe4laznem9VJdPEo= github.com/heroku/rollrus v0.2.0/go.mod h1:B3MwEcr9nmf4xj0Sr5l9eSht7wLKMa1C+9ajgAU79ek= -github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= -github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -384,7 +365,6 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jedib0t/go-pretty/v6 v6.3.6 h1:A6w2BuyPMtf7M82BGRBys9bAba2C26ZX9lrlrZ7uH6U= github.com/jedib0t/go-pretty/v6 v6.3.6/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= @@ -403,7 +383,6 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -418,6 +397,9 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -438,9 +420,8 @@ github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4/go.mod h1:3tim github.com/leesper/go_rng v0.0.0-20171009123644-5344a9259b21/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lestrrat-go/file-rotatelogs v2.2.0+incompatible h1:eXEwY0f2h6mcobdAxm4VRSWds4tqmlLdUqxu8ybiEEA= @@ -472,8 +453,9 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -500,7 +482,6 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -519,7 +500,6 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= @@ -537,6 +517,8 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -575,7 +557,6 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -584,8 +565,6 @@ github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5H github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE= -github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -593,7 +572,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= github.com/rollbar/rollbar-go v1.4.5 h1:Z+5yGaZdB7MFv7t759KUR3VEkGdwHjo7Avvf3ApHTVI= github.com/rollbar/rollbar-go v1.4.5/go.mod h1:kLQ9gP3WCRGrvJmF0ueO3wK9xWocej8GRX98D8sa39w= -github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -605,8 +583,6 @@ github.com/sajari/regression v1.0.1 h1:iTVc6ZACGCkoXC+8NdqH5tIreslDTT/bXxT6OmHR5 github.com/sajari/regression v1.0.1/go.mod h1:NeG/XTW1lYfGY7YV/Z0nYDV/RGh3wxwd1yW46835flM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= -github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -645,7 +621,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 h1:Gb2Tyox57NRNuZ2d3rmvB3pcmbu7O1RS3m8WRx7ilrg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -663,27 +638,21 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/tebeka/strftime v0.1.3 h1:5HQXOqWKYRFfNyBMNVc9z5+QzuBtIXy03psIhtdJYto= github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ= -github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= -github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= -github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= -github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZW24CsNJDfeh9Ex6Pm0Rcpc7qrgKBiL44vF4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.3/go.mod h1:5l8GZ8hZvmL4uMdy+mhCO1LjswGRYco9Q3HfuisB21A= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0= -github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnjsFGI/Uc= -github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/fastjson v1.5.1 h1:SXaQZVSwLjZOVhDEhjiCcDtnX0Feu7Z7A1+C5atpoHM= github.com/valyala/fastjson v1.5.1/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= @@ -693,7 +662,6 @@ github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90/go.mod h1:rpXAuuHg github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -735,6 +703,9 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -749,8 +720,8 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -766,6 +737,7 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 h1:rxKZ2gOnYxjfmakvUUqh9Gyb6KXfrj7JWTxORTYqb0E= +golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -836,8 +808,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -910,7 +882,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -919,13 +890,15 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -933,8 +906,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1099,8 +1072,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1115,8 +1088,6 @@ gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:a gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= -gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= From 64114e48d755467ffccb65cd0315b6c03ea3e2d5 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 1 Aug 2023 20:22:29 +0800 Subject: [PATCH 1283/1392] CODEOWNERS: add python/ to @narumiruma --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 0ca1873a70..88a98150a7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1,2 @@ pkg/strategy/grid2 @kbearXD @gx578007 +python @narumiruna From 027d7648509ec62b5735e8b062132f1e8b305e92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 12:33:28 +0000 Subject: [PATCH 1284/1392] build(deps): bump golang.org/x/image Bumps [golang.org/x/image](https://github.com/golang/image) from 0.0.0-20200927104501-e162460cd6b5 to 0.5.0. - [Commits](https://github.com/golang/image/commits/v0.5.0) --- updated-dependencies: - dependency-name: golang.org/x/image dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 24d0d4add8..336b415662 100644 --- a/go.mod +++ b/go.mod @@ -141,7 +141,7 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 // indirect - golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect + golang.org/x/image v0.5.0 // indirect golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect diff --git a/go.sum b/go.sum index 0fd21efa46..da03de521f 100644 --- a/go.sum +++ b/go.sum @@ -668,6 +668,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= @@ -720,6 +721,7 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -741,8 +743,9 @@ golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5/go.mod h1:lgLbSvA5ygNOMpwM/9 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -762,6 +765,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -803,11 +807,13 @@ golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -829,6 +835,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -890,8 +897,10 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -906,6 +915,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -965,6 +975,7 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 5a0c6a30679c878e01590137718ba47eaa06bec3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 12:33:38 +0000 Subject: [PATCH 1285/1392] build(deps): bump github.com/prometheus/client_golang Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.11.0 to 1.11.1. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.11.0...v1.11.1) --- updated-dependencies: - dependency-name: github.com/prometheus/client_golang dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 24d0d4add8..6e12565172 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/muesli/kmeans v0.3.0 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.3.0 - github.com/prometheus/client_golang v1.11.0 + github.com/prometheus/client_golang v1.11.1 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/robfig/cron/v3 v3.0.0 github.com/sajari/regression v1.0.1 diff --git a/go.sum b/go.sum index 0fd21efa46..1e9d3251fb 100644 --- a/go.sum +++ b/go.sum @@ -536,8 +536,9 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= From c0e315fafee9d38d279df1b4143e72fc9b29b629 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 1 Aug 2023 20:11:33 +0800 Subject: [PATCH 1286/1392] core: fix trade collector dead lock --- pkg/core/tradecollector.go | 12 +++++++----- pkg/risk/riskcontrol/position.go | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go index 695cd8a8a4..05d7d7ff94 100644 --- a/pkg/core/tradecollector.go +++ b/pkg/core/tradecollector.go @@ -127,7 +127,6 @@ func (c *TradeCollector) RecoverTrade(td types.Trade) bool { // if we have the order in the order store, then the trade will be considered for the position. // profit will also be calculated. func (c *TradeCollector) Process() bool { - logrus.Debugf("TradeCollector.Process()") positionChanged := false var trades []types.Trade @@ -184,20 +183,24 @@ func (c *TradeCollector) Process() bool { // return true when the given trade is added // return false when the given trade is not added func (c *TradeCollector) processTrade(trade types.Trade) bool { - c.mu.Lock() - defer c.mu.Unlock() - key := trade.Key() + c.mu.Lock() + // if it's already done, remove the trade from the trade store if _, done := c.doneTrades[key]; done { + c.mu.Unlock() return false } if !c.orderStore.Exists(trade.OrderID) { + c.mu.Unlock() return false } + c.doneTrades[key] = struct{}{} + c.mu.Unlock() + if c.position != nil { profit, netProfit, madeProfit := c.position.AddTrade(trade) if madeProfit { @@ -213,7 +216,6 @@ func (c *TradeCollector) processTrade(trade types.Trade) bool { c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) } - c.doneTrades[key] = struct{}{} return true } diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go index 2714238582..851c6f5664 100644 --- a/pkg/risk/riskcontrol/position.go +++ b/pkg/risk/riskcontrol/position.go @@ -44,7 +44,7 @@ func NewPositionRiskControl(orderExecutor bbgo.OrderExecutorExtended, hardLimit, Quantity: quantity, } - log.Infof("submitting order: %+v", submitOrder) + log.Infof("RiskControl: position limit exceeded, submitting order to reduce position: %+v", submitOrder) createdOrders, err := orderExecutor.SubmitOrders(context.Background(), submitOrder) if err != nil { log.WithError(err).Errorf("failed to submit orders") From 71d86aa483fa7f427ec268d22ea8c631d3d74b00 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Aug 2023 00:17:25 +0800 Subject: [PATCH 1287/1392] core: add trade to the trade store when order is not matched --- pkg/core/tradecollector.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/core/tradecollector.go b/pkg/core/tradecollector.go index 05d7d7ff94..07c92b3dde 100644 --- a/pkg/core/tradecollector.go +++ b/pkg/core/tradecollector.go @@ -77,7 +77,7 @@ func (c *TradeCollector) BindStreamForBackground(stream types.Stream) { func (c *TradeCollector) BindStream(stream types.Stream) { stream.OnTradeUpdate(func(trade types.Trade) { - c.ProcessTrade(trade) + c.processTrade(trade) }) } @@ -194,6 +194,9 @@ func (c *TradeCollector) processTrade(trade types.Trade) bool { } if !c.orderStore.Exists(trade.OrderID) { + // not done yet + // add this trade to the trade store for the later processing + c.tradeStore.Add(trade) c.mu.Unlock() return false } @@ -241,7 +244,7 @@ func (c *TradeCollector) Run(ctx context.Context) { c.Process() case trade := <-c.tradeC: - c.ProcessTrade(trade) + c.processTrade(trade) } } } From 2af45f73b692a4873ad71b735725afb97e5a3216 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Aug 2023 11:01:46 +0800 Subject: [PATCH 1288/1392] compile and update migration package --- pkg/migrations/sqlite3/migration_api_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/migrations/sqlite3/migration_api_test.go b/pkg/migrations/sqlite3/migration_api_test.go index d7f77c875c..d1f4fe1ab0 100644 --- a/pkg/migrations/sqlite3/migration_api_test.go +++ b/pkg/migrations/sqlite3/migration_api_test.go @@ -14,7 +14,7 @@ func TestGetMigrationsMap(t *testing.T) { func TestMergeMigrationsMap(t *testing.T) { MergeMigrationsMap(map[int64]*rockhopper.Migration{ - 2: {}, - 3: {}, + 2: &rockhopper.Migration{}, + 3: &rockhopper.Migration{}, }) } From 129f6734790cf487c502e42b39fbe68ca7272e8e Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Aug 2023 11:01:52 +0800 Subject: [PATCH 1289/1392] update command doc files --- doc/commands/bbgo.md | 2 +- doc/commands/bbgo_account.md | 2 +- doc/commands/bbgo_backtest.md | 2 +- doc/commands/bbgo_balances.md | 2 +- doc/commands/bbgo_build.md | 2 +- doc/commands/bbgo_cancel-order.md | 2 +- doc/commands/bbgo_deposits.md | 2 +- doc/commands/bbgo_execute-order.md | 2 +- doc/commands/bbgo_get-order.md | 2 +- doc/commands/bbgo_hoptimize.md | 2 +- doc/commands/bbgo_kline.md | 2 +- doc/commands/bbgo_list-orders.md | 2 +- doc/commands/bbgo_margin.md | 2 +- doc/commands/bbgo_margin_interests.md | 2 +- doc/commands/bbgo_margin_loans.md | 2 +- doc/commands/bbgo_margin_repays.md | 2 +- doc/commands/bbgo_market.md | 2 +- doc/commands/bbgo_optimize.md | 2 +- doc/commands/bbgo_orderbook.md | 2 +- doc/commands/bbgo_orderupdate.md | 2 +- doc/commands/bbgo_pnl.md | 2 +- doc/commands/bbgo_run.md | 2 +- doc/commands/bbgo_submit-order.md | 2 +- doc/commands/bbgo_sync.md | 2 +- doc/commands/bbgo_trades.md | 2 +- doc/commands/bbgo_tradeupdate.md | 2 +- doc/commands/bbgo_transfer-history.md | 2 +- doc/commands/bbgo_userdatastream.md | 2 +- doc/commands/bbgo_version.md | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index b0f63424a5..0eae9c63d4 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -58,4 +58,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 6c50e4d61e..e14e9b3acf 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -41,4 +41,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index afa27126f2..069fd6e0f8 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -50,4 +50,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index d87c68b3bb..2b32168a21 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -40,4 +40,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index 4388fc82e3..da17527463 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -39,4 +39,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index a706e6992d..18b29ca5bf 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -49,4 +49,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index 94292542d3..b971fdb1a8 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -41,4 +41,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index ecceec3287..8aac740d99 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -48,4 +48,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index e52067afe3..a0a3033964 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -42,4 +42,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md index 5ca93451d3..6323327349 100644 --- a/doc/commands/bbgo_hoptimize.md +++ b/doc/commands/bbgo_hoptimize.md @@ -45,4 +45,4 @@ bbgo hoptimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 0677e91865..3b6f14a934 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -42,4 +42,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index 78f7f10343..8f9cc089c4 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -41,4 +41,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md index d507028599..01935cf345 100644 --- a/doc/commands/bbgo_margin.md +++ b/doc/commands/bbgo_margin.md @@ -38,4 +38,4 @@ margin related history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md index a18244310f..06b9c1ce25 100644 --- a/doc/commands/bbgo_margin_interests.md +++ b/doc/commands/bbgo_margin_interests.md @@ -41,4 +41,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md index 73822e3f11..c241d0442d 100644 --- a/doc/commands/bbgo_margin_loans.md +++ b/doc/commands/bbgo_margin_loans.md @@ -41,4 +41,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md index 5cd5d277c9..068140b28e 100644 --- a/doc/commands/bbgo_margin_repays.md +++ b/doc/commands/bbgo_margin_repays.md @@ -41,4 +41,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] * [bbgo margin](bbgo_margin.md) - margin related history -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index fdb662ffce..d32acd4446 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -40,4 +40,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md index 0cda733d0d..0ac4b451dc 100644 --- a/doc/commands/bbgo_optimize.md +++ b/doc/commands/bbgo_optimize.md @@ -44,4 +44,4 @@ bbgo optimize [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index b1dc5f60c5..56b1822adb 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -42,4 +42,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index 308f659811..7ef43eee9b 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -40,4 +40,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index 8bf81baf45..ba269a82c9 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -49,4 +49,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 70efdc0acb..bcaf0e392b 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -51,4 +51,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 4c30120405..2b4a17c2bb 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -46,4 +46,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index c43dafca78..e60a0edcf8 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -42,4 +42,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index a9e70be571..f92e0941f7 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -42,4 +42,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 153d25b239..c231842d88 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -40,4 +40,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index e2dd4b0a8a..3b53e86382 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -42,4 +42,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index cb1bd63242..d3254c8cf2 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -40,4 +40,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index c24fc84e21..1c1de0ddf2 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -39,4 +39,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 24-Jul-2023 +###### Auto generated by spf13/cobra on 2-Aug-2023 From d4abc16959c9312f8bb4c273fa22fe46a6c48b9d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Aug 2023 11:01:52 +0800 Subject: [PATCH 1290/1392] bump version to v1.51.1 --- pkg/version/dev.go | 4 ++-- pkg/version/version.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/version/dev.go b/pkg/version/dev.go index ad2a65e096..3a4b041fdd 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -3,6 +3,6 @@ package version -const Version = "v1.51.0-afc5dbb9-dev" +const Version = "v1.51.1-71d86aa4-dev" -const VersionGitRef = "afc5dbb9" +const VersionGitRef = "71d86aa4" diff --git a/pkg/version/version.go b/pkg/version/version.go index 016cae6c3c..ab00e5ac1e 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -3,6 +3,6 @@ package version -const Version = "v1.51.0-afc5dbb9" +const Version = "v1.51.1-71d86aa4" -const VersionGitRef = "afc5dbb9" +const VersionGitRef = "71d86aa4" From 04da8fded7579779a81580881542871ebe8cdf03 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Aug 2023 11:01:52 +0800 Subject: [PATCH 1291/1392] add v1.51.1 release note --- doc/release/v1.51.1.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 doc/release/v1.51.1.md diff --git a/doc/release/v1.51.1.md b/doc/release/v1.51.1.md new file mode 100644 index 0000000000..faaa957084 --- /dev/null +++ b/doc/release/v1.51.1.md @@ -0,0 +1,20 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.51.0...main) + + - [#1259](https://github.com/c9s/bbgo/pull/1259): FIX: core: fix trade collector dead lock + - [#1260](https://github.com/c9s/bbgo/pull/1260): build(deps): bump grpcio from 1.44.0 to 1.53.0 in /python + - [#1264](https://github.com/c9s/bbgo/pull/1264): build(deps): bump github.com/prometheus/client_golang from 1.11.0 to 1.11.1 + - [#1263](https://github.com/c9s/bbgo/pull/1263): build(deps): bump github.com/gin-gonic/gin from 1.7.0 to 1.9.1 + - [#1262](https://github.com/c9s/bbgo/pull/1262): build(deps): bump underscore, @nomiclabs/hardhat-waffle and ethereum-waffle in /contracts + - [#1255](https://github.com/c9s/bbgo/pull/1255): FEATURE: [bybit] add query trade api + - [#1257](https://github.com/c9s/bbgo/pull/1257): FEATURE: [grid2] fee discounted filtering + - [#1254](https://github.com/c9s/bbgo/pull/1254): merge back v1.50 into main + - [#1256](https://github.com/c9s/bbgo/pull/1256): FIX: fix batch query trade missing time range + - [#1247](https://github.com/c9s/bbgo/pull/1247): FEATURE: [max] add fee_discounted to Trade struct for RESTful api + - [#1252](https://github.com/c9s/bbgo/pull/1252): FEATURE: [bybit] query closed order + - [#1253](https://github.com/c9s/bbgo/pull/1253): FIX: [max] return err on max queryClosedOrdersByLastOrderID + - [#1250](https://github.com/c9s/bbgo/pull/1250): FEATURE: [bybit] support cancel order + - [#1249](https://github.com/c9s/bbgo/pull/1249): FEATURE: [bybit] support place order + - [#1248](https://github.com/c9s/bbgo/pull/1248): pkg/exchange: add QueryOpenOrders API for bybit + - [#1244](https://github.com/c9s/bbgo/pull/1244): FEATURE: support QueryTickers API on bybit + - [#1246](https://github.com/c9s/bbgo/pull/1246): TEST: add httptesting pkg + - [#1243](https://github.com/c9s/bbgo/pull/1243): FEATURE: pkg/exchange: add query market to bybit exchange From e61db95bd831d70752cf68b145d44eecdaecceac Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Aug 2023 14:07:35 +0800 Subject: [PATCH 1292/1392] types: exit ping worker when error is happened --- pkg/types/stream.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/types/stream.go b/pkg/types/stream.go index 4cc5e63b3d..05986208e0 100644 --- a/pkg/types/stream.go +++ b/pkg/types/stream.go @@ -274,6 +274,7 @@ func (s *StandardStream) ping(ctx context.Context, conn *websocket.Conn, cancel if err := conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(writeTimeout)); err != nil { log.WithError(err).Error("ping error", err) s.Reconnect() + return } } } @@ -306,6 +307,7 @@ func (s *StandardStream) Connect(ctx context.Context) error { } // start one re-connector goroutine with the base context + // reconnector goroutine does not exit when the connection is closed go s.reconnector(ctx) s.EmitStart() From 5064615df802791968386d25c755ba9a51904db7 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 2 Aug 2023 17:47:18 +0800 Subject: [PATCH 1293/1392] pkg/exchange: add custom heart beat func to StandardStream --- pkg/types/stream.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/types/stream.go b/pkg/types/stream.go index 05986208e0..1baa3700a6 100644 --- a/pkg/types/stream.go +++ b/pkg/types/stream.go @@ -41,6 +41,9 @@ type Parser func(message []byte) (interface{}, error) type Dispatcher func(e interface{}) +// HeartBeat keeps connection alive by sending the heartbeat packet. +type HeartBeat func(ctxConn context.Context, conn *websocket.Conn, cancelConn context.CancelFunc) + //go:generate callbackgen -type StandardStream -interface type StandardStream struct { parser Parser @@ -106,6 +109,8 @@ type StandardStream struct { FuturesPositionUpdateCallbacks []func(futuresPositions FuturesPositionMap) FuturesPositionSnapshotCallbacks []func(futuresPositions FuturesPositionMap) + + heartBeat HeartBeat } type StandardStreamEmitter interface { @@ -350,6 +355,9 @@ func (s *StandardStream) DialAndConnect(ctx context.Context) error { go s.Read(connCtx, conn, connCancel) go s.ping(connCtx, conn, connCancel, pingInterval) + if s.heartBeat != nil { + go s.heartBeat(connCtx, conn, connCancel) + } return nil } @@ -419,6 +427,11 @@ func (s *StandardStream) Close() error { return nil } +// SetHeartBeat sets the custom heart beat implementation if needed +func (s *StandardStream) SetHeartBeat(fn HeartBeat) { + s.heartBeat = fn +} + type Depth string const ( From e1bae5dba028e697aacb70b3a8a718d0e2b750c3 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 2 Aug 2023 16:57:30 +0800 Subject: [PATCH 1294/1392] pkg/exchange: implement bybit stream ping --- pkg/exchange/bybit/bybitapi/client.go | 9 +- pkg/exchange/bybit/exchange.go | 4 + pkg/exchange/bybit/stream.go | 96 +++++++++++++ pkg/exchange/bybit/types.go | 39 ++++++ pkg/exchange/bybit/types_test.go | 191 ++++++++++++++++++++++++++ 5 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 pkg/exchange/bybit/stream.go create mode 100644 pkg/exchange/bybit/types.go create mode 100644 pkg/exchange/bybit/types_test.go diff --git a/pkg/exchange/bybit/bybitapi/client.go b/pkg/exchange/bybit/bybitapi/client.go index 9184f9f5bb..fb3458d8d5 100644 --- a/pkg/exchange/bybit/bybitapi/client.go +++ b/pkg/exchange/bybit/bybitapi/client.go @@ -19,8 +19,13 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -const defaultHTTPTimeout = time.Second * 15 -const RestBaseURL = "https://api.bybit.com" +const ( + defaultHTTPTimeout = time.Second * 15 + + RestBaseURL = "https://api.bybit.com" + WsSpotPublicSpotUrl = "wss://stream.bybit.com/v5/public/spot" + WsSpotPrivateUrl = "wss://stream.bybit.com/v5/private" +) // defaultRequestWindowMilliseconds specify how long an HTTP request is valid. It is also used to prevent replay attacks. var defaultRequestWindowMilliseconds = fmt.Sprintf("%d", 5*time.Second.Milliseconds()) diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index d838283f5b..915d0d2ea9 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -387,3 +387,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type return trades, nil } + +func (e *Exchange) NewStream() types.Stream { + return NewStream() +} diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go new file mode 100644 index 0000000000..186cc3eeb9 --- /dev/null +++ b/pkg/exchange/bybit/stream.go @@ -0,0 +1,96 @@ +package bybit + +import ( + "context" + "encoding/json" + "time" + + "github.com/gorilla/websocket" + + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + "github.com/c9s/bbgo/pkg/types" +) + +const ( + // Bybit: To avoid network or program issues, we recommend that you send the ping heartbeat packet every 20 seconds + // to maintain the WebSocket connection. + pingInterval = 20 * time.Second +) + +type Stream struct { + types.StandardStream +} + +func NewStream() *Stream { + stream := &Stream{ + StandardStream: types.NewStandardStream(), + } + + stream.SetEndpointCreator(stream.createEndpoint) + stream.SetParser(stream.parseWebSocketEvent) + stream.SetDispatcher(stream.dispatchEvent) + stream.SetHeartBeat(stream.ping) + + return stream +} + +func (s *Stream) createEndpoint(_ context.Context) (string, error) { + var url string + if s.PublicOnly { + url = bybitapi.WsSpotPublicSpotUrl + } else { + url = bybitapi.WsSpotPrivateUrl + } + return url, nil +} + +func (s *Stream) dispatchEvent(event interface{}) { + switch e := event.(type) { + case *WebSocketEvent: + if err := e.IsValid(); err != nil { + log.Errorf("invalid event: %v", err) + } + } +} + +func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { + var resp WebSocketEvent + return &resp, json.Unmarshal(in, &resp) +} + +// ping implements the Bybit text message of WebSocket PingPong. +func (s *Stream) ping(ctx context.Context, conn *websocket.Conn, cancelFunc context.CancelFunc) { + defer func() { + log.Debug("[bybit] ping worker stopped") + cancelFunc() + }() + + var pingTicker = time.NewTicker(pingInterval) + defer pingTicker.Stop() + + for { + select { + + case <-ctx.Done(): + return + + case <-s.CloseC: + return + + case <-pingTicker.C: + // it's just for maintaining the liveliness of the connection, so comment out ReqId. + err := conn.WriteJSON(struct { + //ReqId string `json:"req_id"` + Op WsOpType `json:"op"` + }{ + //ReqId: uuid.NewString(), + Op: WsOpTypePing, + }) + if err != nil { + log.WithError(err).Error("ping error", err) + s.Reconnect() + return + } + } + } +} diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go new file mode 100644 index 0000000000..750e3d3d43 --- /dev/null +++ b/pkg/exchange/bybit/types.go @@ -0,0 +1,39 @@ +package bybit + +import ( + "fmt" +) + +type WsOpType string + +const ( + WsOpTypePing WsOpType = "ping" + WsOpTypePong WsOpType = "pong" +) + +type WebSocketEvent struct { + Success *bool `json:"success,omitempty"` + RetMsg *string `json:"ret_msg,omitempty"` + ReqId *string `json:"req_id,omitempty"` + + ConnId string `json:"conn_id"` + Op WsOpType `json:"op"` + Args []string `json:"args"` +} + +func (w *WebSocketEvent) IsValid() error { + switch w.Op { + case WsOpTypePing: + // public event + if (w.Success != nil && !*w.Success) || + (w.RetMsg != nil && WsOpType(*w.RetMsg) != WsOpTypePong) { + return fmt.Errorf("unexpeted response of pong: %#v", w) + } + return nil + case WsOpTypePong: + // private event + return nil + default: + return fmt.Errorf("unexpected op type: %#v", w) + } +} diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go new file mode 100644 index 0000000000..8cdbf86c87 --- /dev/null +++ b/pkg/exchange/bybit/types_test.go @@ -0,0 +1,191 @@ +package bybit + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_parseWebSocketEvent(t *testing.T) { + t.Run("[public] PingEvent without req id", func(t *testing.T) { + s := NewStream() + msg := `{"success":true,"ret_msg":"pong","conn_id":"a806f6c4-3608-4b6d-a225-9f5da975bc44","op":"ping"}` + raw, err := s.parseWebSocketEvent([]byte(msg)) + assert.NoError(t, err) + + expSucceeds := true + expRetMsg := string(WsOpTypePong) + e, ok := raw.(*WebSocketEvent) + assert.True(t, ok) + assert.Equal(t, &WebSocketEvent{ + Success: &expSucceeds, + RetMsg: &expRetMsg, + ConnId: "a806f6c4-3608-4b6d-a225-9f5da975bc44", + ReqId: nil, + Op: WsOpTypePing, + Args: nil, + }, e) + + assert.NoError(t, e.IsValid()) + }) + + t.Run("[public] PingEvent with req id", func(t *testing.T) { + s := NewStream() + msg := `{"success":true,"ret_msg":"pong","conn_id":"a806f6c4-3608-4b6d-a225-9f5da975bc44","req_id":"b26704da-f5af-44c2-bdf7-935d6739e1a0","op":"ping"}` + raw, err := s.parseWebSocketEvent([]byte(msg)) + assert.NoError(t, err) + + expSucceeds := true + expRetMsg := string(WsOpTypePong) + expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" + e, ok := raw.(*WebSocketEvent) + assert.True(t, ok) + assert.Equal(t, &WebSocketEvent{ + Success: &expSucceeds, + RetMsg: &expRetMsg, + ConnId: "a806f6c4-3608-4b6d-a225-9f5da975bc44", + ReqId: &expReqId, + Op: WsOpTypePing, + Args: nil, + }, e) + + assert.NoError(t, e.IsValid()) + }) + + t.Run("[private] PingEvent without req id", func(t *testing.T) { + s := NewStream() + msg := `{"op":"pong","args":["1690884539181"],"conn_id":"civn4p1dcjmtvb69ome0-yrt1"}` + raw, err := s.parseWebSocketEvent([]byte(msg)) + assert.NoError(t, err) + + e, ok := raw.(*WebSocketEvent) + assert.True(t, ok) + assert.Equal(t, &WebSocketEvent{ + Success: nil, + RetMsg: nil, + ConnId: "civn4p1dcjmtvb69ome0-yrt1", + ReqId: nil, + Op: WsOpTypePong, + Args: []string{"1690884539181"}, + }, e) + + assert.NoError(t, e.IsValid()) + }) + + t.Run("[private] PingEvent with req id", func(t *testing.T) { + s := NewStream() + msg := `{"req_id":"78d36b57-a142-47b7-9143-5843df77d44d","op":"pong","args":["1690884539181"],"conn_id":"civn4p1dcjmtvb69ome0-yrt1"}` + raw, err := s.parseWebSocketEvent([]byte(msg)) + assert.NoError(t, err) + + expReqId := "78d36b57-a142-47b7-9143-5843df77d44d" + e, ok := raw.(*WebSocketEvent) + assert.True(t, ok) + assert.Equal(t, &WebSocketEvent{ + Success: nil, + RetMsg: nil, + ConnId: "civn4p1dcjmtvb69ome0-yrt1", + ReqId: &expReqId, + Op: WsOpTypePong, + Args: []string{"1690884539181"}, + }, e) + + assert.NoError(t, e.IsValid()) + }) +} + +func Test_WebSocketEventIsValid(t *testing.T) { + t.Run("[public] valid op ping", func(t *testing.T) { + expSucceeds := true + expRetMsg := string(WsOpTypePong) + expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" + + w := &WebSocketEvent{ + Success: &expSucceeds, + RetMsg: &expRetMsg, + ReqId: &expReqId, + ConnId: "test-conndid", + Op: WsOpTypePing, + Args: nil, + } + assert.NoError(t, w.IsValid()) + }) + + t.Run("[private] valid op ping", func(t *testing.T) { + w := &WebSocketEvent{ + Success: nil, + RetMsg: nil, + ReqId: nil, + ConnId: "test-conndid", + Op: WsOpTypePong, + Args: nil, + } + assert.NoError(t, w.IsValid()) + }) + + t.Run("[public] un-Success", func(t *testing.T) { + expSucceeds := false + expRetMsg := string(WsOpTypePong) + expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" + + w := &WebSocketEvent{ + Success: &expSucceeds, + RetMsg: &expRetMsg, + ReqId: &expReqId, + ConnId: "test-conndid", + Op: WsOpTypePing, + Args: nil, + } + assert.Error(t, fmt.Errorf("unexpeted response of pong: %#v", w), w.IsValid()) + }) + + t.Run("[public] missing Success field", func(t *testing.T) { + expRetMsg := string(WsOpTypePong) + expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" + + w := &WebSocketEvent{ + RetMsg: &expRetMsg, + ReqId: &expReqId, + ConnId: "test-conndid", + Op: WsOpTypePing, + Args: nil, + } + assert.Error(t, fmt.Errorf("unexpeted response of pong: %#v", w), w.IsValid()) + }) + + t.Run("[public] invalid ret msg", func(t *testing.T) { + expSucceeds := false + expRetMsg := "PINGPONGPINGPONG" + expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" + + w := &WebSocketEvent{ + Success: &expSucceeds, + RetMsg: &expRetMsg, + ReqId: &expReqId, + ConnId: "test-conndid", + Op: WsOpTypePing, + Args: nil, + } + assert.Error(t, fmt.Errorf("unexpeted response of pong: %#v", w), w.IsValid()) + }) + + t.Run("[public] missing RetMsg field", func(t *testing.T) { + expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" + + w := &WebSocketEvent{ + ReqId: &expReqId, + ConnId: "test-conndid", + Op: WsOpTypePing, + Args: nil, + } + assert.Error(t, fmt.Errorf("unexpeted response of pong: %#v", w), w.IsValid()) + }) + + t.Run("unexpected op type", func(t *testing.T) { + w := &WebSocketEvent{ + Op: WsOpType("unexpected"), + } + assert.Error(t, fmt.Errorf("unexpected op type: %#v", w), w.IsValid()) + }) +} From 113041740193499be504ed241da070d16a0cd58f Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 4 Aug 2023 11:07:20 +0800 Subject: [PATCH 1295/1392] fix/supertrend: use strconv instead of fmt --- pkg/strategy/supertrend/strategy.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 2cb789c52a..2d78980042 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strconv" "sync" "github.com/pkg/errors" @@ -365,15 +366,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Add strategy parameters to report if s.TrackParameters && s.ProfitStatsTracker.AccumulatedProfitReport != nil { - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) - s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("window", strconv.Itoa(s.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", strconv.FormatFloat(s.SupertrendMultiplier, 'f', 2, 64)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", strconv.Itoa(s.FastDEMAWindow)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", strconv.Itoa(s.SlowDEMAWindow)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("linReg", strconv.Itoa(s.LinearRegression.Window)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", strconv.FormatFloat(s.TakeProfitAtrMultiplier, 'f', 2, 64)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", strconv.FormatBool(s.StopLossByTriggeringK)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", strconv.FormatBool(s.StopByReversedSupertrend)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", strconv.FormatBool(s.StopByReversedDema)) + s.ProfitStatsTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", strconv.FormatBool(s.StopByReversedLinGre)) } s.ProfitStatsTracker.Bind(s.session, s.orderExecutor.TradeCollector()) From a6047f629d83557c55b23463197ff7ecdabe874e Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 2 Aug 2023 16:57:30 +0800 Subject: [PATCH 1296/1392] pkg/exchange: implement bybit stream ping --- pkg/exchange/bybit/convert_test.go | 18 +- pkg/exchange/bybit/stream.go | 95 ++++++++++- pkg/exchange/bybit/stream_callbacks.go | 15 ++ pkg/exchange/bybit/stream_test.go | 155 +++++++++++++++++ pkg/exchange/bybit/types.go | 113 ++++++++++++- pkg/exchange/bybit/types_test.go | 221 ++++++++++++++++++++++--- pkg/types/stream.go | 1 + 7 files changed, 572 insertions(+), 46 deletions(-) create mode 100644 pkg/exchange/bybit/stream_callbacks.go create mode 100644 pkg/exchange/bybit/stream_test.go diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index 3051a33767..118853de0a 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -1,35 +1,19 @@ package bybit import ( - "context" "fmt" "math" "strconv" "testing" "time" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "go.uber.org/multierr" - "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" v3 "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi/v3" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" ) -func TestU(t *testing.T) { - e := returnErr() - - t.Log(errors.Is(e, context.DeadlineExceeded)) - -} - -func returnErr() error { - var err error - return multierr.Append(multierr.Append(err, fmt.Errorf("got err: %w", context.DeadlineExceeded)), fmt.Errorf("GG")) -} - func TestToGlobalMarket(t *testing.T) { // sample: //{ diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 186cc3eeb9..2f07181b20 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -3,6 +3,7 @@ package bybit import ( "context" "encoding/json" + "fmt" "time" "github.com/gorilla/websocket" @@ -15,10 +16,16 @@ const ( // Bybit: To avoid network or program issues, we recommend that you send the ping heartbeat packet every 20 seconds // to maintain the WebSocket connection. pingInterval = 20 * time.Second + + // spotArgsLimit can input up to 10 args for each subscription request sent to one connection. + spotArgsLimit = 10 ) +//go:generate callbackgen -type Stream type Stream struct { types.StandardStream + + bookEventCallbacks []func(e BookEvent) } func NewStream() *Stream { @@ -31,6 +38,8 @@ func NewStream() *Stream { stream.SetDispatcher(stream.dispatchEvent) stream.SetHeartBeat(stream.ping) + stream.OnConnect(stream.handlerConnect) + stream.OnBookEvent(stream.handleBookEvent) return stream } @@ -46,16 +55,43 @@ func (s *Stream) createEndpoint(_ context.Context) (string, error) { func (s *Stream) dispatchEvent(event interface{}) { switch e := event.(type) { - case *WebSocketEvent: + case *WebSocketOpEvent: if err := e.IsValid(); err != nil { log.Errorf("invalid event: %v", err) } + + case *BookEvent: + s.EmitBookEvent(*e) } } func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { - var resp WebSocketEvent - return &resp, json.Unmarshal(in, &resp) + var e WsEvent + + err := json.Unmarshal(in, &e) + if err != nil { + return nil, err + } + + switch { + case e.IsOp(): + return e.WebSocketOpEvent, nil + + case e.IsTopic(): + switch getTopicType(e.Topic) { + case TopicTypeOrderBook: + var book BookEvent + err = json.Unmarshal(e.WebSocketTopicEvent.Data, &book) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal data into BookEvent: %+v, : %w", string(e.WebSocketTopicEvent.Data), err) + } + + book.Type = e.WebSocketTopicEvent.Type + return &book, nil + } + } + + return nil, fmt.Errorf("unhandled websocket event: %+v", string(in)) } // ping implements the Bybit text message of WebSocket PingPong. @@ -94,3 +130,56 @@ func (s *Stream) ping(ctx context.Context, conn *websocket.Conn, cancelFunc cont } } } + +func (s *Stream) handlerConnect() { + if s.PublicOnly { + var topics []string + + for _, subscription := range s.Subscriptions { + topic, err := convertSubscription(subscription) + if err != nil { + log.WithError(err).Errorf("subscription convert error") + continue + } + + topics = append(topics, topic) + } + if len(topics) > spotArgsLimit { + log.Debugf("topics exceeds limit: %d, drop of: %v", spotArgsLimit, topics[spotArgsLimit:]) + topics = topics[:spotArgsLimit] + } + log.Infof("subscribing channels: %+v", topics) + if err := s.Conn.WriteJSON(WebsocketOp{ + Op: "subscribe", + Args: topics, + }); err != nil { + log.WithError(err).Error("failed to send subscription request") + } + } +} + +func convertSubscription(s types.Subscription) (string, error) { + switch s.Channel { + case types.BookChannel: + depth := types.DepthLevel1 + if len(s.Options.Depth) > 0 && s.Options.Depth == types.DepthLevel50 { + depth = types.DepthLevel50 + } + return genTopic(TopicTypeOrderBook, depth, s.Symbol), nil + } + + return "", fmt.Errorf("unsupported stream channel: %s", s.Channel) +} + +func (s *Stream) handleBookEvent(e BookEvent) { + orderBook := e.OrderBook() + switch { + // Occasionally, you'll receive "UpdateId"=1, which is a snapshot data due to the restart of + // the service. So please overwrite your local orderbook + case e.Type == DataTypeSnapshot || e.UpdateId.Int() == 1: + s.EmitBookSnapshot(orderBook) + + case e.Type == DataTypeDelta: + s.EmitBookUpdate(orderBook) + } +} diff --git a/pkg/exchange/bybit/stream_callbacks.go b/pkg/exchange/bybit/stream_callbacks.go new file mode 100644 index 0000000000..7940157b88 --- /dev/null +++ b/pkg/exchange/bybit/stream_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type Stream"; DO NOT EDIT. + +package bybit + +import () + +func (s *Stream) OnBookEvent(cb func(e BookEvent)) { + s.bookEventCallbacks = append(s.bookEventCallbacks, cb) +} + +func (s *Stream) EmitBookEvent(e BookEvent) { + for _, cb := range s.bookEventCallbacks { + cb(e) + } +} diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go new file mode 100644 index 0000000000..ea4183e047 --- /dev/null +++ b/pkg/exchange/bybit/stream_test.go @@ -0,0 +1,155 @@ +package bybit + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func TestStream_parseWebSocketEvent(t *testing.T) { + s := Stream{} + + t.Run("op", func(t *testing.T) { + input := `{ + "success":true, + "ret_msg":"subscribe", + "conn_id":"a403c8e5-e2b6-4edd-a8f0-1a64fa7227a5", + "op":"subscribe" + }` + res, err := s.parseWebSocketEvent([]byte(input)) + assert.NoError(t, err) + opEvent, ok := res.(*WebSocketOpEvent) + assert.True(t, ok) + expSucceeds := true + expRetMsg := "subscribe" + assert.Equal(t, WebSocketOpEvent{ + Success: &expSucceeds, + RetMsg: &expRetMsg, + ReqId: nil, + ConnId: "a403c8e5-e2b6-4edd-a8f0-1a64fa7227a5", + Op: WsOpTypeSubscribe, + Args: nil, + }, *opEvent) + }) + t.Run("TopicTypeOrderBook with delta", func(t *testing.T) { + input := `{ + "topic":"orderbook.50.BTCUSDT", + "ts":1691130685111, + "type":"delta", + "data":{ + "s":"BTCUSDT", + "b":[ + + ], + "a":[ + [ + "29239.37", + "0.082356" + ], + [ + "29236.1", + "0" + ] + ], + "u":1854104, + "seq":10559247733 + } + }` + + res, err := s.parseWebSocketEvent([]byte(input)) + assert.NoError(t, err) + book, ok := res.(*BookEvent) + assert.True(t, ok) + assert.Equal(t, BookEvent{ + Symbol: "BTCUSDT", + Bids: nil, + Asks: types.PriceVolumeSlice{ + { + fixedpoint.NewFromFloat(29239.37), + fixedpoint.NewFromFloat(0.082356), + }, + { + fixedpoint.NewFromFloat(29236.1), + fixedpoint.NewFromFloat(0), + }, + }, + UpdateId: fixedpoint.NewFromFloat(1854104), + SequenceId: fixedpoint.NewFromFloat(10559247733), + Type: DataTypeDelta, + }, *book) + }) + + t.Run("Parse fails", func(t *testing.T) { + input := `{ + "topic":"orderbook.50.BTCUSDT", + "ts":1691130685111, + "type":"delta", + "data":{ + "GG": "test", + } + }` + + res, err := s.parseWebSocketEvent([]byte(input)) + assert.Error(t, fmt.Errorf("failed to unmarshal data into BookEvent: %+v, : %w", `{ + "GG": "test", + }`, err), err) + assert.Equal(t, nil, res) + }) +} + +func Test_convertSubscription(t *testing.T) { + t.Run("BookChannel.DepthLevel1", func(t *testing.T) { + res, err := convertSubscription(types.Subscription{ + Symbol: "BTCUSDT", + Channel: types.BookChannel, + Options: types.SubscribeOptions{ + Depth: types.DepthLevel1, + }, + }) + assert.NoError(t, err) + assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel1, "BTCUSDT"), res) + }) + t.Run("BookChannel. with default depth", func(t *testing.T) { + res, err := convertSubscription(types.Subscription{ + Symbol: "BTCUSDT", + Channel: types.BookChannel, + }) + assert.NoError(t, err) + assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel1, "BTCUSDT"), res) + }) + t.Run("BookChannel.DepthLevel50", func(t *testing.T) { + res, err := convertSubscription(types.Subscription{ + Symbol: "BTCUSDT", + Channel: types.BookChannel, + Options: types.SubscribeOptions{ + Depth: types.DepthLevel50, + }, + }) + assert.NoError(t, err) + assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel50, "BTCUSDT"), res) + }) + t.Run("BookChannel. not support depth, use default level 1", func(t *testing.T) { + res, err := convertSubscription(types.Subscription{ + Symbol: "BTCUSDT", + Channel: types.BookChannel, + Options: types.SubscribeOptions{ + Depth: "20", + }, + }) + assert.NoError(t, err) + assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel1, "BTCUSDT"), res) + }) + + t.Run("unsupported channel", func(t *testing.T) { + res, err := convertSubscription(types.Subscription{ + Symbol: "BTCUSDT", + Channel: "unsupported", + }) + assert.Error(t, fmt.Errorf("unsupported stream channel: %s", "unsupported"), err) + assert.Equal(t, "", res) + }) +} diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index 750e3d3d43..8986451a26 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -1,17 +1,42 @@ package bybit import ( + "encoding/json" "fmt" + "strings" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) +type WsEvent struct { + // "op" and "topic" are exclusive. + *WebSocketOpEvent + *WebSocketTopicEvent +} + +func (w *WsEvent) IsOp() bool { + return w.WebSocketOpEvent != nil && w.WebSocketTopicEvent == nil +} + +func (w *WsEvent) IsTopic() bool { + return w.WebSocketOpEvent == nil && w.WebSocketTopicEvent != nil +} + type WsOpType string const ( - WsOpTypePing WsOpType = "ping" - WsOpTypePong WsOpType = "pong" + WsOpTypePing WsOpType = "ping" + WsOpTypePong WsOpType = "pong" + WsOpTypeSubscribe WsOpType = "subscribe" ) -type WebSocketEvent struct { +type WebsocketOp struct { + Op string `json:"op"` + Args []string `json:"args"` +} + +type WebSocketOpEvent struct { Success *bool `json:"success,omitempty"` RetMsg *string `json:"ret_msg,omitempty"` ReqId *string `json:"req_id,omitempty"` @@ -21,19 +46,95 @@ type WebSocketEvent struct { Args []string `json:"args"` } -func (w *WebSocketEvent) IsValid() error { +func (w *WebSocketOpEvent) IsValid() error { switch w.Op { case WsOpTypePing: // public event if (w.Success != nil && !*w.Success) || (w.RetMsg != nil && WsOpType(*w.RetMsg) != WsOpTypePong) { - return fmt.Errorf("unexpeted response of pong: %#v", w) + return fmt.Errorf("unexpeted response of pong: %+v", w) } return nil case WsOpTypePong: // private event return nil + case WsOpTypeSubscribe: + if w.Success != nil && !*w.Success { + return fmt.Errorf("unexpected subscribe result: %+v", w) + } + return nil default: - return fmt.Errorf("unexpected op type: %#v", w) + return fmt.Errorf("unexpected op type: %+v", w) + } +} + +type TopicType string + +const ( + TopicTypeOrderBook TopicType = "orderbook" +) + +type DataType string + +const ( + DataTypeSnapshot DataType = "snapshot" + DataTypeDelta DataType = "delta" +) + +type WebSocketTopicEvent struct { + Topic string `json:"topic"` + Type DataType `json:"type"` + // The timestamp (ms) that the system generates the data + Ts types.MillisecondTimestamp `json:"ts"` + Data json.RawMessage `json:"data"` +} + +// PriceVolumeSlice represents a slice of price and value. +// +// index 0 is Bid/Ask price. +// index 1 is Bid/Ask size. The *delta data* has size=0, which means that all quotations for this price have been filled or cancelled +type PriceVolumeSlice [2]fixedpoint.Value + +type BookEvent struct { + // Symbol name + Symbol string `json:"s"` + // Bids. For snapshot stream, the element is sorted by price in descending order + Bids types.PriceVolumeSlice `json:"b"` + // Asks. For snapshot stream, the element is sorted by price in ascending order + Asks types.PriceVolumeSlice `json:"a"` + // Update ID. Is a sequence. Occasionally, you'll receive "u"=1, which is a snapshot data due to the restart of + // the service. So please overwrite your local orderbook + UpdateId fixedpoint.Value `json:"u"` + // Cross sequence. You can use this field to compare different levels orderbook data, and for the smaller seq, + // then it means the data is generated earlier. + SequenceId fixedpoint.Value `json:"seq"` + + // internal use + // Type can be one of snapshot or delta. Copied from WebSocketTopicEvent.Type + Type DataType +} + +func (e *BookEvent) OrderBook() (snapshot types.SliceOrderBook) { + snapshot.Symbol = e.Symbol + snapshot.Bids = e.Bids + snapshot.Asks = e.Asks + return snapshot +} + +const topicSeparator = "." + +func genTopic(in ...interface{}) string { + out := make([]string, len(in)) + for k, v := range in { + out[k] = fmt.Sprintf("%v", v) + } + return strings.Join(out, topicSeparator) +} + +func getTopicType(topic string) TopicType { + slice := strings.Split(topic, topicSeparator) + if len(slice) == 0 { + return "" } + return TopicType(slice[0]) } diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go index 8cdbf86c87..27dd310540 100644 --- a/pkg/exchange/bybit/types_test.go +++ b/pkg/exchange/bybit/types_test.go @@ -5,6 +5,9 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) func Test_parseWebSocketEvent(t *testing.T) { @@ -16,9 +19,9 @@ func Test_parseWebSocketEvent(t *testing.T) { expSucceeds := true expRetMsg := string(WsOpTypePong) - e, ok := raw.(*WebSocketEvent) + e, ok := raw.(*WebSocketOpEvent) assert.True(t, ok) - assert.Equal(t, &WebSocketEvent{ + assert.Equal(t, &WebSocketOpEvent{ Success: &expSucceeds, RetMsg: &expRetMsg, ConnId: "a806f6c4-3608-4b6d-a225-9f5da975bc44", @@ -39,9 +42,9 @@ func Test_parseWebSocketEvent(t *testing.T) { expSucceeds := true expRetMsg := string(WsOpTypePong) expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" - e, ok := raw.(*WebSocketEvent) + e, ok := raw.(*WebSocketOpEvent) assert.True(t, ok) - assert.Equal(t, &WebSocketEvent{ + assert.Equal(t, &WebSocketOpEvent{ Success: &expSucceeds, RetMsg: &expRetMsg, ConnId: "a806f6c4-3608-4b6d-a225-9f5da975bc44", @@ -59,9 +62,9 @@ func Test_parseWebSocketEvent(t *testing.T) { raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) - e, ok := raw.(*WebSocketEvent) + e, ok := raw.(*WebSocketOpEvent) assert.True(t, ok) - assert.Equal(t, &WebSocketEvent{ + assert.Equal(t, &WebSocketOpEvent{ Success: nil, RetMsg: nil, ConnId: "civn4p1dcjmtvb69ome0-yrt1", @@ -80,9 +83,9 @@ func Test_parseWebSocketEvent(t *testing.T) { assert.NoError(t, err) expReqId := "78d36b57-a142-47b7-9143-5843df77d44d" - e, ok := raw.(*WebSocketEvent) + e, ok := raw.(*WebSocketOpEvent) assert.True(t, ok) - assert.Equal(t, &WebSocketEvent{ + assert.Equal(t, &WebSocketOpEvent{ Success: nil, RetMsg: nil, ConnId: "civn4p1dcjmtvb69ome0-yrt1", @@ -101,7 +104,7 @@ func Test_WebSocketEventIsValid(t *testing.T) { expRetMsg := string(WsOpTypePong) expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" - w := &WebSocketEvent{ + w := &WebSocketOpEvent{ Success: &expSucceeds, RetMsg: &expRetMsg, ReqId: &expReqId, @@ -113,7 +116,7 @@ func Test_WebSocketEventIsValid(t *testing.T) { }) t.Run("[private] valid op ping", func(t *testing.T) { - w := &WebSocketEvent{ + w := &WebSocketOpEvent{ Success: nil, RetMsg: nil, ReqId: nil, @@ -129,7 +132,7 @@ func Test_WebSocketEventIsValid(t *testing.T) { expRetMsg := string(WsOpTypePong) expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" - w := &WebSocketEvent{ + w := &WebSocketOpEvent{ Success: &expSucceeds, RetMsg: &expRetMsg, ReqId: &expReqId, @@ -137,21 +140,21 @@ func Test_WebSocketEventIsValid(t *testing.T) { Op: WsOpTypePing, Args: nil, } - assert.Error(t, fmt.Errorf("unexpeted response of pong: %#v", w), w.IsValid()) + assert.Error(t, fmt.Errorf("unexpeted response of pong: %+v", w), w.IsValid()) }) t.Run("[public] missing Success field", func(t *testing.T) { expRetMsg := string(WsOpTypePong) expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" - w := &WebSocketEvent{ + w := &WebSocketOpEvent{ RetMsg: &expRetMsg, ReqId: &expReqId, ConnId: "test-conndid", Op: WsOpTypePing, Args: nil, } - assert.Error(t, fmt.Errorf("unexpeted response of pong: %#v", w), w.IsValid()) + assert.Error(t, fmt.Errorf("unexpeted response of pong: %+v", w), w.IsValid()) }) t.Run("[public] invalid ret msg", func(t *testing.T) { @@ -159,7 +162,7 @@ func Test_WebSocketEventIsValid(t *testing.T) { expRetMsg := "PINGPONGPINGPONG" expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" - w := &WebSocketEvent{ + w := &WebSocketOpEvent{ Success: &expSucceeds, RetMsg: &expRetMsg, ReqId: &expReqId, @@ -167,25 +170,203 @@ func Test_WebSocketEventIsValid(t *testing.T) { Op: WsOpTypePing, Args: nil, } - assert.Error(t, fmt.Errorf("unexpeted response of pong: %#v", w), w.IsValid()) + assert.Error(t, fmt.Errorf("unexpeted response of pong: %+v", w), w.IsValid()) }) t.Run("[public] missing RetMsg field", func(t *testing.T) { expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" - w := &WebSocketEvent{ + w := &WebSocketOpEvent{ ReqId: &expReqId, ConnId: "test-conndid", Op: WsOpTypePing, Args: nil, } - assert.Error(t, fmt.Errorf("unexpeted response of pong: %#v", w), w.IsValid()) + assert.Error(t, fmt.Errorf("unexpeted response of pong: %+v", w), w.IsValid()) }) t.Run("unexpected op type", func(t *testing.T) { - w := &WebSocketEvent{ + w := &WebSocketOpEvent{ Op: WsOpType("unexpected"), } - assert.Error(t, fmt.Errorf("unexpected op type: %#v", w), w.IsValid()) + assert.Error(t, fmt.Errorf("unexpected op type: %+v", w), w.IsValid()) + }) + + t.Run("[subscribe] valid", func(t *testing.T) { + expSucceeds := true + expRetMsg := "" + w := &WebSocketOpEvent{ + Success: &expSucceeds, + RetMsg: &expRetMsg, + ReqId: nil, + ConnId: "test-conndid", + Op: WsOpTypeSubscribe, + Args: nil, + } + assert.NoError(t, w.IsValid()) + }) + + t.Run("[subscribe] un-succeeds", func(t *testing.T) { + expSucceeds := false + expRetMsg := "" + w := &WebSocketOpEvent{ + Success: &expSucceeds, + RetMsg: &expRetMsg, + ReqId: nil, + ConnId: "test-conndid", + Op: WsOpTypeSubscribe, + Args: nil, + } + assert.Error(t, fmt.Errorf("unexpected subscribe result: %+v", w), w.IsValid()) + }) +} + +func TestBookEvent_OrderBook(t *testing.T) { + t.Run("snapshot", func(t *testing.T) { + /* + { + "topic":"orderbook.50.BTCUSDT", + "ts":1691129753071, + "type":"snapshot", + "data":{ + "s":"BTCUSDT", + "b":[ + [ + "29230.81", + "4.713817" + ], + [ + "29230", + "0.1646" + ], + [ + "29229.92", + "0.036" + ], + ], + "a":[ + [ + "29230.82", + "2.745421" + ], + [ + "29231.41", + "1.6" + ], + [ + "29231.42", + "0.513654" + ], + ], + "u":1841364, + "seq":10558648910 + } + } + */ + event := &BookEvent{ + Symbol: "BTCUSDT", + Bids: types.PriceVolumeSlice{ + { + fixedpoint.NewFromFloat(29230.81), + fixedpoint.NewFromFloat(4.713817), + }, + { + fixedpoint.NewFromFloat(29230), + fixedpoint.NewFromFloat(0.1646), + }, + { + fixedpoint.NewFromFloat(29229.92), + fixedpoint.NewFromFloat(0.036), + }, + }, + Asks: types.PriceVolumeSlice{ + { + fixedpoint.NewFromFloat(29230.82), + fixedpoint.NewFromFloat(2.745421), + }, + { + fixedpoint.NewFromFloat(29231.41), + fixedpoint.NewFromFloat(1.6), + }, + { + fixedpoint.NewFromFloat(29231.42), + fixedpoint.NewFromFloat(0.513654), + }, + }, + UpdateId: fixedpoint.NewFromFloat(1841364), + SequenceId: fixedpoint.NewFromFloat(10558648910), + Type: DataTypeSnapshot, + } + + expSliceOrderBook := types.SliceOrderBook{ + Symbol: event.Symbol, + Bids: event.Bids, + Asks: event.Asks, + } + + assert.Equal(t, expSliceOrderBook, event.OrderBook()) }) + t.Run("delta", func(t *testing.T) { + /* + { + "topic":"orderbook.50.BTCUSDT", + "ts":1691130685111, + "type":"delta", + "data":{ + "s":"BTCUSDT", + "b":[ + + ], + "a":[ + [ + "29239.37", + "0.082356" + ], + [ + "29236.1", + "0" + ] + ], + "u":1854104, + "seq":10559247733 + } + } + */ + event := &BookEvent{ + Symbol: "BTCUSDT", + Bids: types.PriceVolumeSlice{}, + Asks: types.PriceVolumeSlice{ + { + fixedpoint.NewFromFloat(29239.37), + fixedpoint.NewFromFloat(0.082356), + }, + { + fixedpoint.NewFromFloat(29236.1), + fixedpoint.NewFromFloat(0), + }, + }, + UpdateId: fixedpoint.NewFromFloat(1854104), + SequenceId: fixedpoint.NewFromFloat(10559247733), + Type: DataTypeDelta, + } + + expSliceOrderBook := types.SliceOrderBook{ + Symbol: event.Symbol, + Bids: types.PriceVolumeSlice{}, + Asks: event.Asks, + } + + assert.Equal(t, expSliceOrderBook, event.OrderBook()) + }) + +} + +func Test_genTopicName(t *testing.T) { + exp := "orderbook.50.BTCUSDT" + assert.Equal(t, exp, genTopic(TopicTypeOrderBook, types.DepthLevel50, "BTCUSDT")) +} + +func Test_getTopicName(t *testing.T) { + exp := TopicTypeOrderBook + assert.Equal(t, exp, getTopicType("orderbook.50.BTCUSDT")) } diff --git a/pkg/types/stream.go b/pkg/types/stream.go index 1baa3700a6..6bcd5e39e2 100644 --- a/pkg/types/stream.go +++ b/pkg/types/stream.go @@ -440,6 +440,7 @@ const ( DepthLevel1 Depth = "1" DepthLevel5 Depth = "5" DepthLevel20 Depth = "20" + DepthLevel50 Depth = "50" ) type Speed string From 7060fd4ecbcc71e2301f28637338e7a63cf40056 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 4 Aug 2023 18:02:24 +0800 Subject: [PATCH 1297/1392] bbgo: add simple order executor --- pkg/bbgo/activeorderbook.go | 9 ++++- pkg/bbgo/order_executor_general.go | 20 +++++++--- pkg/bbgo/order_executor_simple.go | 62 ++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 pkg/bbgo/order_executor_simple.go diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index c089d69fa5..8347d6aab7 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -122,12 +122,13 @@ func (b *ActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout ti // It calls the exchange cancel order api and then remove the orders from the active orderbook directly. func (b *ActiveOrderBook) FastCancel(ctx context.Context, ex types.Exchange, orders ...types.Order) error { // if no orders are given, set to cancelAll + hasSymbol := b.Symbol != "" if len(orders) == 0 { orders = b.Orders() } else { // simple check on given input for _, o := range orders { - if b.Symbol != "" && o.Symbol != b.Symbol { + if hasSymbol && o.Symbol != b.Symbol { return errors.New("[ActiveOrderBook] cancel " + b.Symbol + " orderbook with different order symbol: " + o.Symbol) } } @@ -157,8 +158,9 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange, orders = b.Orders() } else { // simple check on given input + hasSymbol := b.Symbol != "" for _, o := range orders { - if b.Symbol != "" && o.Symbol != b.Symbol { + if hasSymbol && o.Symbol != b.Symbol { return errors.New("[ActiveOrderBook] cancel " + b.Symbol + " orderbook with different symbol: " + o.Symbol) } } @@ -222,6 +224,7 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange, } } } + orders = leftOrders } @@ -231,6 +234,7 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange, func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) { hasSymbol := len(b.Symbol) > 0 + if hasSymbol && order.Symbol != b.Symbol { return } @@ -297,6 +301,7 @@ func (b *ActiveOrderBook) Update(orders ...types.Order) { func (b *ActiveOrderBook) Add(orders ...types.Order) { hasSymbol := len(b.Symbol) > 0 + for _, order := range orders { if hasSymbol && b.Symbol != order.Symbol { continue diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 2e6601b3b4..f34590b823 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -27,15 +27,20 @@ var quantityReduceDelta = fixedpoint.NewFromFloat(0.005) // This is for the maximum retries const submitOrderRetryLimit = 5 +type BaseOrderExecutor struct { + session *ExchangeSession + activeMakerOrders *ActiveOrderBook + orderStore *core.OrderStore +} + // GeneralOrderExecutor implements the general order executor for strategy type GeneralOrderExecutor struct { - session *ExchangeSession + BaseOrderExecutor + symbol string strategy string strategyInstanceID string position *types.Position - activeMakerOrders *ActiveOrderBook - orderStore *core.OrderStore tradeCollector *core.TradeCollector logger log.FieldLogger @@ -54,13 +59,16 @@ func NewGeneralOrderExecutor(session *ExchangeSession, symbol, strategy, strateg orderStore := core.NewOrderStore(symbol) executor := &GeneralOrderExecutor{ - session: session, + BaseOrderExecutor: BaseOrderExecutor{ + session: session, + activeMakerOrders: NewActiveOrderBook(symbol), + orderStore: orderStore, + }, + symbol: symbol, strategy: strategy, strategyInstanceID: strategyInstanceID, position: position, - activeMakerOrders: NewActiveOrderBook(symbol), - orderStore: orderStore, tradeCollector: core.NewTradeCollector(symbol, position, orderStore), } diff --git a/pkg/bbgo/order_executor_simple.go b/pkg/bbgo/order_executor_simple.go new file mode 100644 index 0000000000..a9e36bbbfb --- /dev/null +++ b/pkg/bbgo/order_executor_simple.go @@ -0,0 +1,62 @@ +package bbgo + +import ( + "context" + + log "github.com/sirupsen/logrus" + "go.uber.org/multierr" + + "github.com/c9s/bbgo/pkg/core" + "github.com/c9s/bbgo/pkg/types" +) + +// SimpleOrderExecutor implements the minimal order executor +// This order executor does not handle position and profit stats update +type SimpleOrderExecutor struct { + BaseOrderExecutor + + logger log.FieldLogger +} + +func NewSimpleOrderExecutor(session *ExchangeSession) *SimpleOrderExecutor { + return &SimpleOrderExecutor{ + BaseOrderExecutor: BaseOrderExecutor{ + session: session, + activeMakerOrders: NewActiveOrderBook(""), + orderStore: core.NewOrderStore(""), + }, + } +} + +func (e *SimpleOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { + formattedOrders, err := e.session.FormatOrders(submitOrders) + if err != nil { + return nil, err + } + + orderCreateCallback := func(createdOrder types.Order) { + e.orderStore.Add(createdOrder) + e.activeMakerOrders.Add(createdOrder) + } + + createdOrders, _, err := BatchPlaceOrder(ctx, e.session.Exchange, orderCreateCallback, formattedOrders...) + return createdOrders, err +} + +// CancelOrders cancels the given order objects directly +func (e *SimpleOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error { + err := e.session.Exchange.CancelOrders(ctx, orders...) + if err != nil { // Retry once + err2 := e.session.Exchange.CancelOrders(ctx, orders...) + if err2 != nil { + return multierr.Append(err, err2) + } + } + + return err +} + +func (e *SimpleOrderExecutor) Bind() { + e.activeMakerOrders.BindStream(e.session.UserDataStream) + e.orderStore.BindStream(e.session.UserDataStream) +} From 7d4d2f3e410763ecd5117b8901643d954c59639b Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 01:59:04 +0800 Subject: [PATCH 1298/1392] types: add truncate quote quantity method --- pkg/types/market.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/types/market.go b/pkg/types/market.go index a4e0a308c6..0c598799fa 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -71,6 +71,19 @@ func (m Market) TruncateQuantity(quantity fixedpoint.Value) fixedpoint.Value { return fixedpoint.MustNewFromString(qs) } +// TruncateQuoteQuantity uses the tick size to truncate floating number, in order to avoid the rounding issue +func (m Market) TruncateQuoteQuantity(quantity fixedpoint.Value) fixedpoint.Value { + var ts = m.TickSize.Float64() + var prec = int(math.Round(math.Log10(ts) * -1.0)) + var pow10 = math.Pow10(prec) + + qf := math.Trunc(quantity.Float64() * pow10) + qf = qf / pow10 + + qs := strconv.FormatFloat(qf, 'f', prec, 64) + return fixedpoint.MustNewFromString(qs) +} + // RoundDownQuantityByPrecision uses the volume precision to round down the quantity // This is different from the TruncateQuantity, which uses StepSize (it uses fewer fractions to truncate) func (m Market) RoundDownQuantityByPrecision(quantity fixedpoint.Value) fixedpoint.Value { From 348c8a61e418adc0b5ce22a56f0625960d06244b Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 01:59:20 +0800 Subject: [PATCH 1299/1392] add convert strategy --- pkg/strategy/convert/strategy.go | 421 +++++++++++++++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 pkg/strategy/convert/strategy.go diff --git a/pkg/strategy/convert/strategy.go b/pkg/strategy/convert/strategy.go new file mode 100644 index 0000000000..c5d620af88 --- /dev/null +++ b/pkg/strategy/convert/strategy.go @@ -0,0 +1,421 @@ +package convert + +import ( + "context" + "fmt" + "strconv" + "sync" + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util/tradingutil" +) + +const ID = "convert" + +var log = logrus.WithField("strategy", ID) + +var stableCoins = []string{"USDT", "USDC"} + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +// Strategy "convert" converts your specific asset into other asset +type Strategy struct { + Market types.Market + + From string `json:"from"` + To string `json:"to"` + + // Interval is the period that you want to submit order + Interval types.Interval `json:"interval"` + + UseLimitOrder bool `json:"useLimitOrder"` + + UseTakerOrder bool `json:"useTakerOrder"` + + MinBalance fixedpoint.Value `json:"minBalance"` + MaxQuantity fixedpoint.Value `json:"maxQuantity"` + + Position *types.Position `persistence:"position"` + + directMarket *types.Market + indirectMarkets []types.Market + + markets map[string]types.Market + session *bbgo.ExchangeSession + orderExecutor *bbgo.SimpleOrderExecutor + + pendingQuantity map[string]fixedpoint.Value + pendingQuantityLock sync.Mutex +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s-%s", ID, s.From, s.To) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + +} + +func (s *Strategy) Validate() error { + return nil +} + +func (s *Strategy) handleOrderFilled(ctx context.Context, order types.Order) { + var fees = map[string]fixedpoint.Value{} + + if service, ok := s.session.Exchange.(types.ExchangeOrderQueryService); ok { + trades, err := service.QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: order.Symbol, + OrderID: strconv.FormatUint(order.OrderID, 10), + }) + + if err != nil { + return + } + + fees = tradingutil.CollectTradeFee(trades) + log.Infof("aggregated order fees: %+v", fees) + } + + if s.directMarket != nil { + if order.Symbol != s.directMarket.Symbol { + return + } + + // TODO: notification + return + } else if len(s.indirectMarkets) > 0 { + for i := 0; i < len(s.indirectMarkets); i++ { + market := s.indirectMarkets[i] + if market.Symbol != order.Symbol { + continue + } + + if i == len(s.indirectMarkets)-1 { + // TODO: handle the final order here + continue + } + + nextMarket := s.indirectMarkets[i+1] + + ticker, err := s.session.Exchange.QueryTicker(ctx, nextMarket.Symbol) + if err != nil { + log.WithError(err).Errorf("unable to query ticker") + return + } + + quantity := order.Quantity + quoteQuantity := quantity.Mul(order.Price) + + switch order.Side { + case types.SideTypeSell: + // convert quote asset + if quoteFee, ok := fees[market.QuoteCurrency]; ok { + quoteQuantity = quoteQuantity.Sub(quoteFee) + } + + if err := s.convertBalance(ctx, market.QuoteCurrency, quoteQuantity, nextMarket, ticker); err != nil { + log.WithError(err).Errorf("unable to convert balance") + } + + case types.SideTypeBuy: + if baseFee, ok := fees[market.BaseCurrency]; ok { + quantity = quantity.Sub(baseFee) + } + + if err := s.convertBalance(ctx, market.BaseCurrency, quantity, nextMarket, ticker); err != nil { + log.WithError(err).Errorf("unable to convert balance") + } + } + } + } + +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.pendingQuantity = make(map[string]fixedpoint.Value) + s.session = session + s.markets = session.Markets() + + if market, ok := findDirectMarket(s.markets, s.From, s.To); ok { + s.directMarket = &market + } else if marketChain, ok := findIndirectMarket(s.markets, s.From, s.To); ok { + s.indirectMarkets = marketChain + } + + s.orderExecutor = bbgo.NewSimpleOrderExecutor(session) + s.orderExecutor.ActiveMakerOrders().OnFilled(func(o types.Order) { + s.handleOrderFilled(ctx, o) + }) + s.orderExecutor.Bind() + + if s.Interval != "" { + session.UserDataStream.OnStart(func() { + go s.tickWatcher(ctx, s.Interval.Duration()) + }) + } + + return nil +} + +func (s *Strategy) tickWatcher(ctx context.Context, interval time.Duration) { + if err := s.convert(ctx); err != nil { + log.WithError(err).Errorf("unable to convert asset %s", s.From) + } + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + if err := s.convert(ctx); err != nil { + log.WithError(err).Errorf("unable to convert asset %s", s.From) + } + } + } +} + +func (s *Strategy) getSourceMarket() (types.Market, bool) { + if s.directMarket != nil { + return *s.directMarket, true + } else if len(s.indirectMarkets) > 0 { + return s.indirectMarkets[0], true + } + + return types.Market{}, false +} + +// convert triggers a convert order +func (s *Strategy) convert(ctx context.Context) error { + account := s.session.GetAccount() + fromAsset, ok := account.Balance(s.From) + if !ok { + return nil + } + + log.Debugf("converting %s to %s, current balance: %+v", s.From, s.To, fromAsset) + + if sourceMarket, ok := s.getSourceMarket(); ok { + ticker, err := s.session.Exchange.QueryTicker(ctx, sourceMarket.Symbol) + if err != nil { + return err + } + + quantity := fromAsset.Available + + if !s.MinBalance.IsZero() { + quantity = quantity.Sub(s.MinBalance) + if quantity.Sign() < 0 { + return nil + } + } + + if !s.MaxQuantity.IsZero() { + quantity = fixedpoint.Min(s.MaxQuantity, quantity) + } + + if err := s.convertBalance(ctx, fromAsset.Currency, quantity, sourceMarket, ticker); err != nil { + return err + } + } + + return nil +} + +func (s *Strategy) collectPendingQuantity() { + s.pendingQuantityLock.Lock() + defer s.pendingQuantityLock.Unlock() + + activeOrders := s.orderExecutor.ActiveMakerOrders().Orders() + for _, o := range activeOrders { + if m, ok := s.markets[o.Symbol]; ok { + switch o.Side { + case types.SideTypeBuy: + qq := o.Quantity.Sub(o.ExecutedQuantity).Mul(o.Price) + if q2, ok := s.pendingQuantity[m.QuoteCurrency]; ok { + s.pendingQuantity[m.QuoteCurrency] = q2.Add(qq) + } else { + s.pendingQuantity[m.QuoteCurrency] = qq + } + case types.SideTypeSell: + q := o.Quantity.Sub(o.ExecutedQuantity) + if q2, ok := s.pendingQuantity[m.BaseCurrency]; ok { + s.pendingQuantity[m.BaseCurrency] = q2.Add(q) + } else { + s.pendingQuantity[m.BaseCurrency] = q + } + } + } + } +} + +func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, available fixedpoint.Value, market types.Market, ticker *types.Ticker) error { + s.collectPendingQuantity() + + if err := s.orderExecutor.CancelOrders(ctx); err != nil { + log.WithError(err).Warn("unable to cancel orders") + } + + s.pendingQuantityLock.Lock() + if pendingQ, ok := s.pendingQuantity[fromAsset]; ok { + available = available.Add(pendingQ) + + delete(s.pendingQuantity, fromAsset) + } + s.pendingQuantityLock.Unlock() + + switch fromAsset { + + case market.BaseCurrency: + log.Infof("converting %s %s to %s...", available, fromAsset, market.QuoteCurrency) + + available = market.TruncateQuantity(available) + + // from = Base -> action = sell + if available.Compare(market.MinQuantity) < 0 { + log.Debugf("asset %s %s is less than minQuantity %s, skip convert", available, fromAsset, market.MinQuantity) + return nil + } + + price := ticker.Sell + if s.UseTakerOrder { + price = ticker.Buy + } + + quoteAmount := price.Mul(available) + if quoteAmount.Compare(market.MinNotional) < 0 { + log.Debugf("asset %s %s (%s %s) is less than minNotional %s, skip convert", + available, fromAsset, + quoteAmount, market.QuoteCurrency, + market.MinNotional) + return nil + } + + orderForm := types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: available, + Price: price, + Market: market, + TimeInForce: types.TimeInForceGTC, + } + if _, err := s.orderExecutor.SubmitOrders(ctx, orderForm); err != nil { + log.WithError(err).Errorf("unable to submit order: %+v", orderForm) + } + + case market.QuoteCurrency: + log.Infof("converting %s %s to %s...", available, fromAsset, market.BaseCurrency) + + available = market.TruncateQuoteQuantity(available) + + // from = Quote -> action = buy + if available.Compare(market.MinNotional) < 0 { + log.Debugf("asset %s %s is less than minNotional %s, skip convert", available, fromAsset, market.MinNotional) + return nil + } + + price := ticker.Buy + if s.UseTakerOrder { + price = ticker.Sell + } + + quantity := available.Div(price) + quantity = market.TruncateQuantity(quantity) + if quantity.Compare(market.MinQuantity) < 0 { + log.Debugf("asset %s %s is less than minQuantity %s, skip convert", + quantity, fromAsset, + market.MinQuantity) + return nil + } + + notional := quantity.Mul(price) + if notional.Compare(market.MinNotional) < 0 { + log.Debugf("asset %s %s (%s %s) is less than minNotional %s, skip convert", + quantity, fromAsset, + notional, market.QuoteCurrency, + market.MinNotional) + return nil + } + + orderForm := types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + Market: market, + TimeInForce: types.TimeInForceGTC, + } + if _, err := s.orderExecutor.SubmitOrders(ctx, orderForm); err != nil { + log.WithError(err).Errorf("unable to submit order: %+v", orderForm) + } + } + + return nil +} + +func findIndirectMarket(markets map[string]types.Market, from, to string) ([]types.Market, bool) { + var sourceMarkets = map[string]types.Market{} + var targetMarkets = map[string]types.Market{} + + for _, market := range markets { + if market.BaseCurrency == from { + sourceMarkets[market.QuoteCurrency] = market + } else if market.QuoteCurrency == from { + sourceMarkets[market.BaseCurrency] = market + } + + if market.BaseCurrency == to { + targetMarkets[market.QuoteCurrency] = market + } else if market.QuoteCurrency == to { + targetMarkets[market.BaseCurrency] = market + } + } + + // prefer stable coins for better liquidity + for _, stableCoin := range stableCoins { + m1, ok1 := sourceMarkets[stableCoin] + m2, ok2 := targetMarkets[stableCoin] + if ok1 && ok2 { + return []types.Market{m1, m2}, true + } + } + + for sourceCurrency, m1 := range sourceMarkets { + if m2, ok := targetMarkets[sourceCurrency]; ok { + return []types.Market{m1, m2}, true + } + } + + return nil, false +} + +func findDirectMarket(markets map[string]types.Market, from, to string) (types.Market, bool) { + symbol := from + to + if m, ok := markets[symbol]; ok { + return m, true + } + + symbol = to + from + if m, ok := markets[symbol]; ok { + return m, true + } + + return types.Market{}, false +} From c605761c4f2bfa509d8f663f84aa00bae29ad6c3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 01:59:36 +0800 Subject: [PATCH 1300/1392] add tradingutil package --- pkg/util/tradingutil/trades.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 pkg/util/tradingutil/trades.go diff --git a/pkg/util/tradingutil/trades.go b/pkg/util/tradingutil/trades.go new file mode 100644 index 0000000000..dc80016522 --- /dev/null +++ b/pkg/util/tradingutil/trades.go @@ -0,0 +1,20 @@ +package tradingutil + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// CollectTradeFee collects the fee from the given trade slice +func CollectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { + fees := make(map[string]fixedpoint.Value) + for _, t := range trades { + if fee, ok := fees[t.FeeCurrency]; ok { + fees[t.FeeCurrency] = fee.Add(t.Fee) + } else { + fees[t.FeeCurrency] = t.Fee + } + } + + return fees +} From eaaab914e0c4ea99d7a652bcf61043368008e35e Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 01:59:52 +0800 Subject: [PATCH 1301/1392] refactor order executor accessors --- pkg/bbgo/order_executor_general.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index f34590b823..7279667402 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -33,6 +33,14 @@ type BaseOrderExecutor struct { orderStore *core.OrderStore } +func (e *BaseOrderExecutor) OrderStore() *core.OrderStore { + return e.orderStore +} + +func (e *BaseOrderExecutor) ActiveMakerOrders() *ActiveOrderBook { + return e.activeMakerOrders +} + // GeneralOrderExecutor implements the general order executor for strategy type GeneralOrderExecutor struct { BaseOrderExecutor @@ -131,14 +139,6 @@ func (e *GeneralOrderExecutor) marginAssetMaxBorrowableUpdater(ctx context.Conte } } -func (e *GeneralOrderExecutor) OrderStore() *core.OrderStore { - return e.orderStore -} - -func (e *GeneralOrderExecutor) ActiveMakerOrders() *ActiveOrderBook { - return e.activeMakerOrders -} - func (e *GeneralOrderExecutor) BindEnvironment(environ *Environment) { e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { environ.RecordPosition(e.position, trade, profit) From 951672fc82b27c327fe3c1e0fe2dc76674389690 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 02:00:07 +0800 Subject: [PATCH 1302/1392] improve cancelOrders method --- pkg/bbgo/order_executor_simple.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/bbgo/order_executor_simple.go b/pkg/bbgo/order_executor_simple.go index a9e36bbbfb..e7c5308d01 100644 --- a/pkg/bbgo/order_executor_simple.go +++ b/pkg/bbgo/order_executor_simple.go @@ -45,6 +45,14 @@ func (e *SimpleOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ... // CancelOrders cancels the given order objects directly func (e *SimpleOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error { + if len(orders) == 0 { + orders = e.activeMakerOrders.Orders() + } + + if len(orders) == 0 { + return nil + } + err := e.session.Exchange.CancelOrders(ctx, orders...) if err != nil { // Retry once err2 := e.session.Exchange.CancelOrders(ctx, orders...) From 430b22f5e99029a86e0a011102129196d8bd9c34 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 02:00:22 +0800 Subject: [PATCH 1303/1392] cmd: register convert strategy --- pkg/cmd/strategy/builtin.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 3a7e338888..7eb31ca204 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -6,6 +6,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/autoborrow" _ "github.com/c9s/bbgo/pkg/strategy/bollgrid" _ "github.com/c9s/bbgo/pkg/strategy/bollmaker" + _ "github.com/c9s/bbgo/pkg/strategy/convert" _ "github.com/c9s/bbgo/pkg/strategy/dca" _ "github.com/c9s/bbgo/pkg/strategy/drift" _ "github.com/c9s/bbgo/pkg/strategy/elliottwave" From 4982a858a06e96ebd657b7b0e94ef71783cda2ac Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 02:00:35 +0800 Subject: [PATCH 1304/1392] config: update convert strategy sample config --- config/convert.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 config/convert.yaml diff --git a/config/convert.yaml b/config/convert.yaml new file mode 100644 index 0000000000..ae5f144855 --- /dev/null +++ b/config/convert.yaml @@ -0,0 +1,26 @@ +--- +exchangeStrategies: +- on: binance + convert: + ## the initial asset you want to convert to + from: BNB + + ## the final asset you want to convert to + to: BTC + + ## interval is the period of trigger + interval: 1m + + ## minBalance is the minimal balance line you want to keep + ## in this example, it means 1 BNB + minBalance: 1.0 + + ## maxQuantity is the max quantity per order for converting asset + ## in this example, it means 2 BNB + maxQuantity: 2.0 + + ## useTakerOrder uses the taker price for the order, so the order will be a taker + ## which will be filled immediately + useTakerOrder: true + + From 71186b6794e13c5ed7d8c5ed3ac667aa68f064dd Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 02:02:30 +0800 Subject: [PATCH 1305/1392] update readme --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 91c73b334b..b947f5acad 100644 --- a/README.md +++ b/README.md @@ -98,15 +98,15 @@ the implementation. | grid2 | the second generation grid strategy, it can convert your quote asset into a grid, supports base+quote mode | maker | | | bollgrid | strategy implements a basic grid strategy with the built-in bollinger indicator | maker | | | xmaker | cross exchange market making strategy, it hedges your inventory risk on the other side | maker | no | -| xnav | this strategy helps you record the current net asset value | tool | | -| xalign | this strategy aligns your balance position automatically | tool | | -| xfunding | a funding rate fee strategy | funding | | +| xnav | this strategy helps you record the current net asset value | tool | no | +| xalign | this strategy aligns your balance position automatically | tool | no | +| xfunding | a funding rate fee strategy | funding | no | | autoborrow | this strategy uses margin to borrow assets, to help you keep the minimal balance | tool | no | | pivotshort | this strategy finds the pivot low and entry the trade when the price breaks the previous low | long/short | | | schedule | this strategy buy/sell with a fixed quantity periodically, you can use this as a single DCA, or to refill the fee asset like BNB. | tool | | irr | this strategy opens the position based on the predicated return rate | long/short | | | bollmaker | this strategy holds a long-term long/short position, places maker orders on both side, uses bollinger band to control the position size | maker | | -| wall | this strategy creates wall (large amount order) on the order book | maker | | +| wall | this strategy creates wall (large amount order) on the order book | maker | no | | scmaker | this market making strategy is desgiend for stable coin markets, like USDC/USDT | maker | | | drift | | long/short | | | rsicross | this strategy opens a long position when the fast rsi cross over the slow rsi, this is a demo strategy for using the v2 indicator | long/short | | @@ -119,6 +119,8 @@ the implementation. | factoryzoo | | long/short | | | fmaker | | maker | | | linregmaker | a linear regression based market maker | maker | | +| convert | convert strategy is a tool that helps you convert specific asset to a target asset | tool | no | + From bc8fe22e70cd7b13a7844adec50f3cb33ceefc58 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 02:15:16 +0800 Subject: [PATCH 1306/1392] convert: fix collectPendingQuantity and use graceful order cancel --- pkg/bbgo/order_executor_general.go | 9 +++++++++ pkg/strategy/convert/strategy.go | 29 ++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 7279667402..7b04a17c5e 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -41,6 +41,15 @@ func (e *BaseOrderExecutor) ActiveMakerOrders() *ActiveOrderBook { return e.activeMakerOrders } +// GracefulCancel cancels all active maker orders if orders are not given, otherwise cancel all the given orders +func (e *BaseOrderExecutor) GracefulCancel(ctx context.Context, orders ...types.Order) error { + if err := e.activeMakerOrders.GracefulCancel(ctx, e.session.Exchange, orders...); err != nil { + return errors.Wrap(err, "graceful cancel error") + } + + return nil +} + // GeneralOrderExecutor implements the general order executor for strategy type GeneralOrderExecutor struct { BaseOrderExecutor diff --git a/pkg/strategy/convert/strategy.go b/pkg/strategy/convert/strategy.go index c5d620af88..0ceaf10e90 100644 --- a/pkg/strategy/convert/strategy.go +++ b/pkg/strategy/convert/strategy.go @@ -202,6 +202,15 @@ func (s *Strategy) getSourceMarket() (types.Market, bool) { // convert triggers a convert order func (s *Strategy) convert(ctx context.Context) error { + s.collectPendingQuantity() + + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Warn("unable to cancel orders") + } + + // sleep one second for exchange to unlock the balance + time.Sleep(time.Second) + account := s.session.GetAccount() fromAsset, ok := account.Balance(s.From) if !ok { @@ -238,6 +247,8 @@ func (s *Strategy) convert(ctx context.Context) error { } func (s *Strategy) collectPendingQuantity() { + log.Infof("collecting pending quantity...") + s.pendingQuantityLock.Lock() defer s.pendingQuantityLock.Unlock() @@ -246,13 +257,22 @@ func (s *Strategy) collectPendingQuantity() { if m, ok := s.markets[o.Symbol]; ok { switch o.Side { case types.SideTypeBuy: + if m.QuoteCurrency == s.From { + continue + } + qq := o.Quantity.Sub(o.ExecutedQuantity).Mul(o.Price) if q2, ok := s.pendingQuantity[m.QuoteCurrency]; ok { s.pendingQuantity[m.QuoteCurrency] = q2.Add(qq) } else { s.pendingQuantity[m.QuoteCurrency] = qq } + case types.SideTypeSell: + if m.BaseCurrency == s.From { + continue + } + q := o.Quantity.Sub(o.ExecutedQuantity) if q2, ok := s.pendingQuantity[m.BaseCurrency]; ok { s.pendingQuantity[m.BaseCurrency] = q2.Add(q) @@ -262,17 +282,16 @@ func (s *Strategy) collectPendingQuantity() { } } } + + log.Infof("collected pending quantity: %+v", s.pendingQuantity) } func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, available fixedpoint.Value, market types.Market, ticker *types.Ticker) error { - s.collectPendingQuantity() - - if err := s.orderExecutor.CancelOrders(ctx); err != nil { - log.WithError(err).Warn("unable to cancel orders") - } s.pendingQuantityLock.Lock() if pendingQ, ok := s.pendingQuantity[fromAsset]; ok { + + log.Infof("adding pending quantity %s to the current quantity %s", pendingQ, available) available = available.Add(pendingQ) delete(s.pendingQuantity, fromAsset) From 4d293121d72b892237107eb3db5657add08e25c9 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 02:37:53 +0800 Subject: [PATCH 1307/1392] convert: fix pending quantity collector with trade query --- pkg/strategy/convert/strategy.go | 90 ++++++++++++++++++++------------ pkg/util/tradingutil/trades.go | 8 +++ 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/pkg/strategy/convert/strategy.go b/pkg/strategy/convert/strategy.go index 0ceaf10e90..5f86188646 100644 --- a/pkg/strategy/convert/strategy.go +++ b/pkg/strategy/convert/strategy.go @@ -51,7 +51,7 @@ type Strategy struct { session *bbgo.ExchangeSession orderExecutor *bbgo.SimpleOrderExecutor - pendingQuantity map[string]fixedpoint.Value + pendingQuantity map[string]fixedpoint.Value `persistence:"pendingQuantities"` pendingQuantityLock sync.Mutex } @@ -109,12 +109,6 @@ func (s *Strategy) handleOrderFilled(ctx context.Context, order types.Order) { nextMarket := s.indirectMarkets[i+1] - ticker, err := s.session.Exchange.QueryTicker(ctx, nextMarket.Symbol) - if err != nil { - log.WithError(err).Errorf("unable to query ticker") - return - } - quantity := order.Quantity quoteQuantity := quantity.Mul(order.Price) @@ -125,7 +119,7 @@ func (s *Strategy) handleOrderFilled(ctx context.Context, order types.Order) { quoteQuantity = quoteQuantity.Sub(quoteFee) } - if err := s.convertBalance(ctx, market.QuoteCurrency, quoteQuantity, nextMarket, ticker); err != nil { + if err := s.convertBalance(ctx, market.QuoteCurrency, quoteQuantity, nextMarket); err != nil { log.WithError(err).Errorf("unable to convert balance") } @@ -134,7 +128,7 @@ func (s *Strategy) handleOrderFilled(ctx context.Context, order types.Order) { quantity = quantity.Sub(baseFee) } - if err := s.convertBalance(ctx, market.BaseCurrency, quantity, nextMarket, ticker); err != nil { + if err := s.convertBalance(ctx, market.BaseCurrency, quantity, nextMarket); err != nil { log.WithError(err).Errorf("unable to convert balance") } } @@ -166,6 +160,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) } + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + s.collectPendingQuantity(ctx) + + _ = s.orderExecutor.GracefulCancel(ctx) + }) + return nil } @@ -202,11 +202,7 @@ func (s *Strategy) getSourceMarket() (types.Market, bool) { // convert triggers a convert order func (s *Strategy) convert(ctx context.Context) error { - s.collectPendingQuantity() - - if err := s.orderExecutor.GracefulCancel(ctx); err != nil { - log.WithError(err).Warn("unable to cancel orders") - } + s.collectPendingQuantity(ctx) // sleep one second for exchange to unlock the balance time.Sleep(time.Second) @@ -220,11 +216,6 @@ func (s *Strategy) convert(ctx context.Context) error { log.Debugf("converting %s to %s, current balance: %+v", s.From, s.To, fromAsset) if sourceMarket, ok := s.getSourceMarket(); ok { - ticker, err := s.session.Exchange.QueryTicker(ctx, sourceMarket.Symbol) - if err != nil { - return err - } - quantity := fromAsset.Available if !s.MinBalance.IsZero() { @@ -238,7 +229,7 @@ func (s *Strategy) convert(ctx context.Context) error { quantity = fixedpoint.Min(s.MaxQuantity, quantity) } - if err := s.convertBalance(ctx, fromAsset.Currency, quantity, sourceMarket, ticker); err != nil { + if err := s.convertBalance(ctx, fromAsset.Currency, quantity, sourceMarket); err != nil { return err } } @@ -246,39 +237,70 @@ func (s *Strategy) convert(ctx context.Context) error { return nil } -func (s *Strategy) collectPendingQuantity() { +func (s *Strategy) addPendingQuantity(asset string, q fixedpoint.Value) { + if q2, ok := s.pendingQuantity[asset]; ok { + s.pendingQuantity[asset] = q2.Add(q) + } else { + s.pendingQuantity[asset] = q + } +} + +func (s *Strategy) collectPendingQuantity(ctx context.Context) { log.Infof("collecting pending quantity...") s.pendingQuantityLock.Lock() defer s.pendingQuantityLock.Unlock() activeOrders := s.orderExecutor.ActiveMakerOrders().Orders() + log.Infof("found %d active orders", len(activeOrders)) + + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Warn("unable to cancel orders") + } + for _, o := range activeOrders { + log.Infof("checking order: %+v", o) + + if service, ok := s.session.Exchange.(types.ExchangeOrderQueryService); ok { + trades, err := service.QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: o.Symbol, + OrderID: strconv.FormatUint(o.OrderID, 10), + }) + + if err != nil { + return + } + + o.ExecutedQuantity = tradingutil.AggregateTradesQuantity(trades) + + log.Infof("updated executed quantity to %s", o.ExecutedQuantity) + } + if m, ok := s.markets[o.Symbol]; ok { switch o.Side { case types.SideTypeBuy: + if !o.ExecutedQuantity.IsZero() { + s.addPendingQuantity(m.BaseCurrency, o.ExecutedQuantity) + } + if m.QuoteCurrency == s.From { continue } qq := o.Quantity.Sub(o.ExecutedQuantity).Mul(o.Price) - if q2, ok := s.pendingQuantity[m.QuoteCurrency]; ok { - s.pendingQuantity[m.QuoteCurrency] = q2.Add(qq) - } else { - s.pendingQuantity[m.QuoteCurrency] = qq + s.addPendingQuantity(m.QuoteCurrency, qq) + case types.SideTypeSell: + + if !o.ExecutedQuantity.IsZero() { + s.addPendingQuantity(m.QuoteCurrency, o.ExecutedQuantity.Mul(o.Price)) } - case types.SideTypeSell: if m.BaseCurrency == s.From { continue } q := o.Quantity.Sub(o.ExecutedQuantity) - if q2, ok := s.pendingQuantity[m.BaseCurrency]; ok { - s.pendingQuantity[m.BaseCurrency] = q2.Add(q) - } else { - s.pendingQuantity[m.BaseCurrency] = q - } + s.addPendingQuantity(m.BaseCurrency, q) } } } @@ -286,7 +308,11 @@ func (s *Strategy) collectPendingQuantity() { log.Infof("collected pending quantity: %+v", s.pendingQuantity) } -func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, available fixedpoint.Value, market types.Market, ticker *types.Ticker) error { +func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, available fixedpoint.Value, market types.Market) error { + ticker, err2 := s.session.Exchange.QueryTicker(ctx, market.Symbol) + if err2 != nil { + return err2 + } s.pendingQuantityLock.Lock() if pendingQ, ok := s.pendingQuantity[fromAsset]; ok { diff --git a/pkg/util/tradingutil/trades.go b/pkg/util/tradingutil/trades.go index dc80016522..1bedf7f2cb 100644 --- a/pkg/util/tradingutil/trades.go +++ b/pkg/util/tradingutil/trades.go @@ -18,3 +18,11 @@ func CollectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { return fees } + +func AggregateTradesQuantity(trades []types.Trade) fixedpoint.Value { + tq := fixedpoint.Zero + for _, t := range trades { + tq = tq.Add(t.Quantity) + } + return tq +} From ce8063654dcfd016b3d94210867ab92a06c29247 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 16:38:46 +0800 Subject: [PATCH 1308/1392] tradingutil: add test on CollectTradeFee --- pkg/util/tradingutil/trades_test.go | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 pkg/util/tradingutil/trades_test.go diff --git a/pkg/util/tradingutil/trades_test.go b/pkg/util/tradingutil/trades_test.go new file mode 100644 index 0000000000..381eed483b --- /dev/null +++ b/pkg/util/tradingutil/trades_test.go @@ -0,0 +1,41 @@ +package tradingutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +var number = fixedpoint.MustNewFromString + +func Test_CollectTradeFee(t *testing.T) { + trades := []types.Trade{ + { + ID: 1, + Price: number("21000"), + Quantity: number("0.001"), + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Fee: number("0.00001"), + FeeCurrency: "BTC", + FeeDiscounted: false, + }, + { + ID: 2, + Price: number("21200"), + Quantity: number("0.001"), + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Fee: number("0.00002"), + FeeCurrency: "BTC", + FeeDiscounted: false, + }, + } + + fees := CollectTradeFee(trades) + assert.NotNil(t, fees) + assert.Equal(t, number("0.00003"), fees["BTC"]) +} From 8b6a8aeb7b773fd7a8f094837c9153fae0cee200 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 16:39:03 +0800 Subject: [PATCH 1309/1392] convert: move moq check/adjustment to types.Market --- pkg/strategy/convert/strategy.go | 53 +++++++------------------------- pkg/types/market.go | 46 +++++++++++++++++++++++++++ pkg/types/market_test.go | 25 +++++++++++++++ 3 files changed, 82 insertions(+), 42 deletions(-) diff --git a/pkg/strategy/convert/strategy.go b/pkg/strategy/convert/strategy.go index 5f86188646..419ea658dc 100644 --- a/pkg/strategy/convert/strategy.go +++ b/pkg/strategy/convert/strategy.go @@ -162,7 +162,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { s.collectPendingQuantity(ctx) - + _ = s.orderExecutor.GracefulCancel(ctx) }) @@ -327,27 +327,16 @@ func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, availab switch fromAsset { case market.BaseCurrency: - log.Infof("converting %s %s to %s...", available, fromAsset, market.QuoteCurrency) - - available = market.TruncateQuantity(available) - - // from = Base -> action = sell - if available.Compare(market.MinQuantity) < 0 { - log.Debugf("asset %s %s is less than minQuantity %s, skip convert", available, fromAsset, market.MinQuantity) - return nil - } - price := ticker.Sell if s.UseTakerOrder { price = ticker.Buy } - quoteAmount := price.Mul(available) - if quoteAmount.Compare(market.MinNotional) < 0 { - log.Debugf("asset %s %s (%s %s) is less than minNotional %s, skip convert", - available, fromAsset, - quoteAmount, market.QuoteCurrency, - market.MinNotional) + log.Infof("converting %s %s to %s...", available, fromAsset, market.QuoteCurrency) + + quantity, ok := market.GreaterThanMinimalOrderQuantity(types.SideTypeSell, price, available) + if !ok { + log.Debugf("asset %s %s is less than MoQ, skip convert", available, fromAsset) return nil } @@ -355,7 +344,7 @@ func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, availab Symbol: market.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimit, - Quantity: available, + Quantity: quantity, Price: price, Market: market, TimeInForce: types.TimeInForceGTC, @@ -365,36 +354,16 @@ func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, availab } case market.QuoteCurrency: - log.Infof("converting %s %s to %s...", available, fromAsset, market.BaseCurrency) - - available = market.TruncateQuoteQuantity(available) - - // from = Quote -> action = buy - if available.Compare(market.MinNotional) < 0 { - log.Debugf("asset %s %s is less than minNotional %s, skip convert", available, fromAsset, market.MinNotional) - return nil - } - price := ticker.Buy if s.UseTakerOrder { price = ticker.Sell } - quantity := available.Div(price) - quantity = market.TruncateQuantity(quantity) - if quantity.Compare(market.MinQuantity) < 0 { - log.Debugf("asset %s %s is less than minQuantity %s, skip convert", - quantity, fromAsset, - market.MinQuantity) - return nil - } + log.Infof("converting %s %s to %s...", available, fromAsset, market.BaseCurrency) - notional := quantity.Mul(price) - if notional.Compare(market.MinNotional) < 0 { - log.Debugf("asset %s %s (%s %s) is less than minNotional %s, skip convert", - quantity, fromAsset, - notional, market.QuoteCurrency, - market.MinNotional) + quantity, ok := market.GreaterThanMinimalOrderQuantity(types.SideTypeBuy, price, available) + if !ok { + log.Debugf("asset %s %s is less than MoQ, skip convert", available, fromAsset) return nil } diff --git a/pkg/types/market.go b/pkg/types/market.go index 0c598799fa..07f2cc0da3 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -72,6 +72,7 @@ func (m Market) TruncateQuantity(quantity fixedpoint.Value) fixedpoint.Value { } // TruncateQuoteQuantity uses the tick size to truncate floating number, in order to avoid the rounding issue +// this is usually used for calculating the order size from the quote quantity. func (m Market) TruncateQuoteQuantity(quantity fixedpoint.Value) fixedpoint.Value { var ts = m.TickSize.Float64() var prec = int(math.Round(math.Log10(ts) * -1.0)) @@ -84,6 +85,51 @@ func (m Market) TruncateQuoteQuantity(quantity fixedpoint.Value) fixedpoint.Valu return fixedpoint.MustNewFromString(qs) } +// GreaterThanMinimalOrderQuantity ensures that your given balance could fit the minimal order quantity +// when side = sell, then available = base balance +// when side = buy, then available = quote balance +// The balance will be truncated first in order to calculate the minimal notional and minimal quantity +// The adjusted (truncated) order quantity will be returned +func (m Market) GreaterThanMinimalOrderQuantity(side SideType, price, available fixedpoint.Value) (fixedpoint.Value, bool) { + switch side { + case SideTypeSell: + available = m.TruncateQuantity(available) + + if available.Compare(m.MinQuantity) < 0 { + return fixedpoint.Zero, false + } + + quoteAmount := price.Mul(available) + if quoteAmount.Compare(m.MinNotional) < 0 { + return fixedpoint.Zero, false + } + + return available, true + + case SideTypeBuy: + available = m.TruncateQuoteQuantity(available) + + if available.Compare(m.MinNotional) < 0 { + return fixedpoint.Zero, false + } + + quantity := available.Div(price) + quantity = m.TruncateQuantity(quantity) + if quantity.Compare(m.MinQuantity) < 0 { + return fixedpoint.Zero, false + } + + notional := quantity.Mul(price) + if notional.Compare(m.MinNotional) < 0 { + return fixedpoint.Zero, false + } + + return quantity, true + } + + return available, true +} + // RoundDownQuantityByPrecision uses the volume precision to round down the quantity // This is different from the TruncateQuantity, which uses StepSize (it uses fewer fractions to truncate) func (m Market) RoundDownQuantityByPrecision(quantity fixedpoint.Value) fixedpoint.Value { diff --git a/pkg/types/market_test.go b/pkg/types/market_test.go index 7966a6d5aa..a20121750e 100644 --- a/pkg/types/market_test.go +++ b/pkg/types/market_test.go @@ -13,6 +13,31 @@ import ( var s func(string) fixedpoint.Value = fixedpoint.MustNewFromString +func TestMarket_GreaterThanMinimalOrderQuantity(t *testing.T) { + market := Market{ + Symbol: "BTCUSDT", + LocalSymbol: "BTCUSDT", + PricePrecision: 8, + VolumePrecision: 8, + QuoteCurrency: "USDT", + BaseCurrency: "BTC", + MinNotional: number(10.0), + MinAmount: number(10.0), + MinQuantity: number(0.0001), + StepSize: number(0.00001), + TickSize: number(0.01), + } + + _, ok := market.GreaterThanMinimalOrderQuantity(SideTypeSell, number(20000.0), number(0.00051)) + assert.True(t, ok) + + _, ok = market.GreaterThanMinimalOrderQuantity(SideTypeBuy, number(20000.0), number(10.0)) + assert.True(t, ok) + + _, ok = market.GreaterThanMinimalOrderQuantity(SideTypeBuy, number(20000.0), number(0.99999)) + assert.False(t, ok) +} + func TestFormatQuantity(t *testing.T) { quantity := formatQuantity( s("0.12511"), From c3cce05bdd541dde42e7b54d9684359222a0b2fc Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 16:49:25 +0800 Subject: [PATCH 1310/1392] xalign: apply market.GreaterThanMinimalOrderQuantity on xalign --- pkg/strategy/xalign/strategy.go | 77 +++++++++++++++------------------ 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 012bf56cbc..140c1fb500 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -145,8 +145,9 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st // changeQuantity < 0 = sell q := changeQuantity.Abs() + // a fast filtering if q.Compare(market.MinQuantity) < 0 { - log.Infof("skip dust quantity: %f", q.Float64()) + log.Debugf("skip dust quantity: %f", q.Float64()) continue } @@ -155,11 +156,6 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st switch side { case types.SideTypeBuy: - quoteBalance, ok := session.Account.Balance(quoteCurrency) - if !ok { - continue - } - price := ticker.Sell if taker { price = ticker.Sell @@ -169,6 +165,11 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st price = ticker.Buy } + quoteBalance, ok := session.Account.Balance(quoteCurrency) + if !ok { + continue + } + requiredQuoteAmount := q.Mul(price) requiredQuoteAmount = requiredQuoteAmount.Round(market.PricePrecision, fixedpoint.Up) if requiredQuoteAmount.Compare(quoteBalance.Available) > 0 { @@ -176,34 +177,19 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st continue } - if market.IsDustQuantity(q, price) { - log.Infof("%s ignore dust quantity: %f", currency, q.Float64()) - return nil, nil - } - - q = market.AdjustQuantityByMinNotional(q, price) - - return session, &types.SubmitOrder{ - Symbol: symbol, - Side: side, - Type: types.OrderTypeLimit, - Quantity: q, - Price: price, - Market: market, - TimeInForce: "GTC", + if quantity, ok := market.GreaterThanMinimalOrderQuantity(side, price, requiredQuoteAmount); ok { + return session, &types.SubmitOrder{ + Symbol: symbol, + Side: side, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + Market: market, + TimeInForce: types.TimeInForceGTC, + } } case types.SideTypeSell: - baseBalance, ok := session.Account.Balance(currency) - if !ok { - continue - } - - if q.Compare(baseBalance.Available) > 0 { - log.Warnf("required base amount %f < available base balance %v, skip", q.Float64(), baseBalance) - continue - } - price := ticker.Buy if taker { price = ticker.Buy @@ -213,19 +199,26 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st price = ticker.Sell } - if market.IsDustQuantity(q, price) { - log.Infof("%s ignore dust quantity: %f", currency, q.Float64()) - return nil, nil + baseBalance, ok := session.Account.Balance(currency) + if !ok { + continue + } + + if q.Compare(baseBalance.Available) > 0 { + log.Warnf("required base amount %f < available base balance %v, skip", q.Float64(), baseBalance) + continue } - return session, &types.SubmitOrder{ - Symbol: symbol, - Side: side, - Type: types.OrderTypeLimit, - Quantity: q, - Price: price, - Market: market, - TimeInForce: "GTC", + if quantity, ok := market.GreaterThanMinimalOrderQuantity(side, price, q); ok { + return session, &types.SubmitOrder{ + Symbol: symbol, + Side: side, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + Market: market, + TimeInForce: types.TimeInForceGTC, + } } } From 84fa19afee4e72190b9e0ca6b9cf87d4cb3fc3bc Mon Sep 17 00:00:00 2001 From: Edwin Date: Mon, 7 Aug 2023 14:55:09 +0800 Subject: [PATCH 1311/1392] pkg/exchange: add auth function for ws --- pkg/exchange/bybit/bybitapi/client.go | 4 ++-- pkg/exchange/bybit/bybitapi/v3/client.go | 4 ++++ pkg/exchange/bybit/exchange.go | 7 +++--- pkg/exchange/bybit/stream.go | 27 +++++++++++++++++++++-- pkg/exchange/bybit/stream_test.go | 28 ++++++++++++++++++++++++ pkg/exchange/bybit/types.go | 14 ++++++------ pkg/exchange/bybit/types_test.go | 8 +++---- 7 files changed, 74 insertions(+), 18 deletions(-) diff --git a/pkg/exchange/bybit/bybitapi/client.go b/pkg/exchange/bybit/bybitapi/client.go index fb3458d8d5..034d297500 100644 --- a/pkg/exchange/bybit/bybitapi/client.go +++ b/pkg/exchange/bybit/bybitapi/client.go @@ -107,7 +107,7 @@ func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL // 2. Use the HMAC_SHA256 or RSA_SHA256 algorithm to sign the string in step 1, and convert it to a hex // string (HMAC_SHA256) / base64 (RSA_SHA256) to obtain the sign parameter. // 3. Append the sign parameter to request header, and send the HTTP request. - signature := sign(signKey, c.secret) + signature := Sign(signKey, c.secret) req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) if err != nil { @@ -122,7 +122,7 @@ func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL return req, nil } -func sign(payload string, secret string) string { +func Sign(payload string, secret string) string { var sig = hmac.New(sha256.New, []byte(secret)) _, err := sig.Write([]byte(payload)) if err != nil { diff --git a/pkg/exchange/bybit/bybitapi/v3/client.go b/pkg/exchange/bybit/bybitapi/v3/client.go index 9fdb889fb3..d9dd3c2e06 100644 --- a/pkg/exchange/bybit/bybitapi/v3/client.go +++ b/pkg/exchange/bybit/bybitapi/v3/client.go @@ -11,3 +11,7 @@ type APIResponse = bybitapi.APIResponse type Client struct { Client requestgen.AuthenticatedAPIClient } + +func NewClient(client *bybitapi.RestClient) *Client { + return &Client{Client: client} +} diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 915d0d2ea9..76c0c2edfd 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -57,8 +57,9 @@ func New(key, secret string) (*Exchange, error) { return &Exchange{ key: key, // pragma: allowlist nextline secret - secret: secret, - client: client, + secret: secret, + client: client, + v3client: v3.NewClient(client), }, nil } @@ -389,5 +390,5 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type } func (e *Exchange) NewStream() types.Stream { - return NewStream() + return NewStream(e.key, e.secret) } diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 2f07181b20..687d9452d8 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" "time" "github.com/gorilla/websocket" @@ -21,16 +22,25 @@ const ( spotArgsLimit = 10 ) +var ( + // wsAuthRequest specifies the duration for which a websocket request's authentication is valid. + wsAuthRequest = 10 * time.Second +) + //go:generate callbackgen -type Stream type Stream struct { + key, secret string types.StandardStream bookEventCallbacks []func(e BookEvent) } -func NewStream() *Stream { +func NewStream(key, secret string) *Stream { stream := &Stream{ StandardStream: types.NewStandardStream(), + // pragma: allowlist nextline secret + key: key, + secret: secret, } stream.SetEndpointCreator(stream.createEndpoint) @@ -150,11 +160,24 @@ func (s *Stream) handlerConnect() { } log.Infof("subscribing channels: %+v", topics) if err := s.Conn.WriteJSON(WebsocketOp{ - Op: "subscribe", + Op: WsOpTypeSubscribe, Args: topics, }); err != nil { log.WithError(err).Error("failed to send subscription request") } + } else { + expires := strconv.FormatInt(time.Now().Add(wsAuthRequest).In(time.UTC).UnixMilli(), 10) + + if err := s.Conn.WriteJSON(WebsocketOp{ + Op: WsOpTypeAuth, + Args: []string{ + s.key, + expires, + bybitapi.Sign(fmt.Sprintf("GET/realtime%s", expires), s.secret), + }, + }); err != nil { + log.WithError(err).Error("failed to auth request") + } } } diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go index ea4183e047..6ff3c3fcb6 100644 --- a/pkg/exchange/bybit/stream_test.go +++ b/pkg/exchange/bybit/stream_test.go @@ -1,15 +1,43 @@ package bybit import ( + "context" "fmt" + "os" + "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/testutil" "github.com/c9s/bbgo/pkg/types" ) +func getTestClientOrSkip(t *testing.T) *Stream { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + + key, secret, ok := testutil.IntegrationTestConfigured(t, "BYBIT") + if !ok { + t.Skip("BYBIT_* env vars are not configured") + return nil + } + + return NewStream(key, secret) +} + +func TestStream(t *testing.T) { + s := getTestClientOrSkip(t) + + t.Run("Auth test", func(t *testing.T) { + s.Connect(context.Background()) + c := make(chan struct{}) + <-c + }) +} + func TestStream_parseWebSocketEvent(t *testing.T) { s := Stream{} diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index 8986451a26..6adbafcd10 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -28,11 +28,12 @@ type WsOpType string const ( WsOpTypePing WsOpType = "ping" WsOpTypePong WsOpType = "pong" + WsOpTypeAuth WsOpType = "auth" WsOpTypeSubscribe WsOpType = "subscribe" ) type WebsocketOp struct { - Op string `json:"op"` + Op WsOpType `json:"op"` Args []string `json:"args"` } @@ -58,6 +59,11 @@ func (w *WebSocketOpEvent) IsValid() error { case WsOpTypePong: // private event return nil + case WsOpTypeAuth: + if w.Success != nil && !*w.Success { + return fmt.Errorf("unexpected response of auth: %#v", w) + } + return nil case WsOpTypeSubscribe: if w.Success != nil && !*w.Success { return fmt.Errorf("unexpected subscribe result: %+v", w) @@ -89,12 +95,6 @@ type WebSocketTopicEvent struct { Data json.RawMessage `json:"data"` } -// PriceVolumeSlice represents a slice of price and value. -// -// index 0 is Bid/Ask price. -// index 1 is Bid/Ask size. The *delta data* has size=0, which means that all quotations for this price have been filled or cancelled -type PriceVolumeSlice [2]fixedpoint.Value - type BookEvent struct { // Symbol name Symbol string `json:"s"` diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go index 27dd310540..20f0527de2 100644 --- a/pkg/exchange/bybit/types_test.go +++ b/pkg/exchange/bybit/types_test.go @@ -12,7 +12,7 @@ import ( func Test_parseWebSocketEvent(t *testing.T) { t.Run("[public] PingEvent without req id", func(t *testing.T) { - s := NewStream() + s := NewStream("", "") msg := `{"success":true,"ret_msg":"pong","conn_id":"a806f6c4-3608-4b6d-a225-9f5da975bc44","op":"ping"}` raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) @@ -34,7 +34,7 @@ func Test_parseWebSocketEvent(t *testing.T) { }) t.Run("[public] PingEvent with req id", func(t *testing.T) { - s := NewStream() + s := NewStream("", "") msg := `{"success":true,"ret_msg":"pong","conn_id":"a806f6c4-3608-4b6d-a225-9f5da975bc44","req_id":"b26704da-f5af-44c2-bdf7-935d6739e1a0","op":"ping"}` raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) @@ -57,7 +57,7 @@ func Test_parseWebSocketEvent(t *testing.T) { }) t.Run("[private] PingEvent without req id", func(t *testing.T) { - s := NewStream() + s := NewStream("", "") msg := `{"op":"pong","args":["1690884539181"],"conn_id":"civn4p1dcjmtvb69ome0-yrt1"}` raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) @@ -77,7 +77,7 @@ func Test_parseWebSocketEvent(t *testing.T) { }) t.Run("[private] PingEvent with req id", func(t *testing.T) { - s := NewStream() + s := NewStream("", "") msg := `{"req_id":"78d36b57-a142-47b7-9143-5843df77d44d","op":"pong","args":["1690884539181"],"conn_id":"civn4p1dcjmtvb69ome0-yrt1"}` raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) From 3e4e46de20455940f15fc247c12743b5ec3e85d7 Mon Sep 17 00:00:00 2001 From: Edwin Date: Mon, 7 Aug 2023 15:59:50 +0800 Subject: [PATCH 1312/1392] pkg/exchange: to de-pointer the value in WsOpEvent and fix test assertion --- pkg/exchange/bybit/convert_test.go | 4 +- pkg/exchange/bybit/stream_test.go | 12 ++- pkg/exchange/bybit/types.go | 21 +++-- pkg/exchange/bybit/types_test.go | 120 +++++++++++++++-------------- 4 files changed, 79 insertions(+), 78 deletions(-) diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index 118853de0a..e6eb5b0491 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -371,7 +371,7 @@ func Test_toLocalOrderType(t *testing.T) { assert.Equal(t, bybitapi.OrderTypeMarket, orderType) orderType, err = toLocalOrderType("wrong type") - assert.Error(t, fmt.Errorf("order type %s not supported", "wrong side"), err) + assert.Equal(t, fmt.Errorf("order type wrong type not supported"), err) assert.Equal(t, bybitapi.OrderType(""), orderType) } @@ -385,7 +385,7 @@ func Test_toLocalSide(t *testing.T) { assert.Equal(t, bybitapi.SideBuy, side) side, err = toLocalSide("wrong side") - assert.Error(t, fmt.Errorf("side type %s not supported", "wrong side"), err) + assert.Equal(t, fmt.Errorf("side type %s not supported", "wrong side"), err) assert.Equal(t, bybitapi.Side(""), side) } diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go index 6ff3c3fcb6..2eff7d4918 100644 --- a/pkg/exchange/bybit/stream_test.go +++ b/pkg/exchange/bybit/stream_test.go @@ -55,9 +55,9 @@ func TestStream_parseWebSocketEvent(t *testing.T) { expSucceeds := true expRetMsg := "subscribe" assert.Equal(t, WebSocketOpEvent{ - Success: &expSucceeds, - RetMsg: &expRetMsg, - ReqId: nil, + Success: expSucceeds, + RetMsg: expRetMsg, + ReqId: "", ConnId: "a403c8e5-e2b6-4edd-a8f0-1a64fa7227a5", Op: WsOpTypeSubscribe, Args: nil, @@ -122,9 +122,7 @@ func TestStream_parseWebSocketEvent(t *testing.T) { }` res, err := s.parseWebSocketEvent([]byte(input)) - assert.Error(t, fmt.Errorf("failed to unmarshal data into BookEvent: %+v, : %w", `{ - "GG": "test", - }`, err), err) + assert.Error(t, err) assert.Equal(t, nil, res) }) } @@ -177,7 +175,7 @@ func Test_convertSubscription(t *testing.T) { Symbol: "BTCUSDT", Channel: "unsupported", }) - assert.Error(t, fmt.Errorf("unsupported stream channel: %s", "unsupported"), err) + assert.Equal(t, fmt.Errorf("unsupported stream channel: %s", "unsupported"), err) assert.Equal(t, "", res) }) } diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index 6adbafcd10..6ccc8f7e59 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -38,9 +38,9 @@ type WebsocketOp struct { } type WebSocketOpEvent struct { - Success *bool `json:"success,omitempty"` - RetMsg *string `json:"ret_msg,omitempty"` - ReqId *string `json:"req_id,omitempty"` + Success bool `json:"success"` + RetMsg string `json:"ret_msg"` + ReqId string `json:"req_id,omitempty"` ConnId string `json:"conn_id"` Op WsOpType `json:"op"` @@ -51,22 +51,21 @@ func (w *WebSocketOpEvent) IsValid() error { switch w.Op { case WsOpTypePing: // public event - if (w.Success != nil && !*w.Success) || - (w.RetMsg != nil && WsOpType(*w.RetMsg) != WsOpTypePong) { - return fmt.Errorf("unexpeted response of pong: %+v", w) + if !w.Success || WsOpType(w.RetMsg) != WsOpTypePong { + return fmt.Errorf("unexpected response result: %+v", w) } return nil case WsOpTypePong: - // private event + // private event, no success and ret_msg fields in response return nil case WsOpTypeAuth: - if w.Success != nil && !*w.Success { - return fmt.Errorf("unexpected response of auth: %#v", w) + if !w.Success || w.RetMsg != "" { + return fmt.Errorf("unexpected response result: %+v", w) } return nil case WsOpTypeSubscribe: - if w.Success != nil && !*w.Success { - return fmt.Errorf("unexpected subscribe result: %+v", w) + if !w.Success || WsOpType(w.RetMsg) != WsOpTypeSubscribe { + return fmt.Errorf("unexpected response result: %+v", w) } return nil default: diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go index 20f0527de2..670098503f 100644 --- a/pkg/exchange/bybit/types_test.go +++ b/pkg/exchange/bybit/types_test.go @@ -17,15 +17,14 @@ func Test_parseWebSocketEvent(t *testing.T) { raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) - expSucceeds := true expRetMsg := string(WsOpTypePong) e, ok := raw.(*WebSocketOpEvent) assert.True(t, ok) assert.Equal(t, &WebSocketOpEvent{ - Success: &expSucceeds, - RetMsg: &expRetMsg, + Success: true, + RetMsg: expRetMsg, ConnId: "a806f6c4-3608-4b6d-a225-9f5da975bc44", - ReqId: nil, + ReqId: "", Op: WsOpTypePing, Args: nil, }, e) @@ -39,16 +38,15 @@ func Test_parseWebSocketEvent(t *testing.T) { raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) - expSucceeds := true expRetMsg := string(WsOpTypePong) expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" e, ok := raw.(*WebSocketOpEvent) assert.True(t, ok) assert.Equal(t, &WebSocketOpEvent{ - Success: &expSucceeds, - RetMsg: &expRetMsg, + Success: true, + RetMsg: expRetMsg, ConnId: "a806f6c4-3608-4b6d-a225-9f5da975bc44", - ReqId: &expReqId, + ReqId: expReqId, Op: WsOpTypePing, Args: nil, }, e) @@ -65,10 +63,10 @@ func Test_parseWebSocketEvent(t *testing.T) { e, ok := raw.(*WebSocketOpEvent) assert.True(t, ok) assert.Equal(t, &WebSocketOpEvent{ - Success: nil, - RetMsg: nil, + Success: false, + RetMsg: "", ConnId: "civn4p1dcjmtvb69ome0-yrt1", - ReqId: nil, + ReqId: "", Op: WsOpTypePong, Args: []string{"1690884539181"}, }, e) @@ -86,10 +84,10 @@ func Test_parseWebSocketEvent(t *testing.T) { e, ok := raw.(*WebSocketOpEvent) assert.True(t, ok) assert.Equal(t, &WebSocketOpEvent{ - Success: nil, - RetMsg: nil, + Success: false, + RetMsg: "", ConnId: "civn4p1dcjmtvb69ome0-yrt1", - ReqId: &expReqId, + ReqId: expReqId, Op: WsOpTypePong, Args: []string{"1690884539181"}, }, e) @@ -100,14 +98,13 @@ func Test_parseWebSocketEvent(t *testing.T) { func Test_WebSocketEventIsValid(t *testing.T) { t.Run("[public] valid op ping", func(t *testing.T) { - expSucceeds := true expRetMsg := string(WsOpTypePong) expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" w := &WebSocketOpEvent{ - Success: &expSucceeds, - RetMsg: &expRetMsg, - ReqId: &expReqId, + Success: true, + RetMsg: expRetMsg, + ReqId: expReqId, ConnId: "test-conndid", Op: WsOpTypePing, Args: nil, @@ -117,9 +114,9 @@ func Test_WebSocketEventIsValid(t *testing.T) { t.Run("[private] valid op ping", func(t *testing.T) { w := &WebSocketOpEvent{ - Success: nil, - RetMsg: nil, - ReqId: nil, + Success: false, + RetMsg: "", + ReqId: "", ConnId: "test-conndid", Op: WsOpTypePong, Args: nil, @@ -128,77 +125,60 @@ func Test_WebSocketEventIsValid(t *testing.T) { }) t.Run("[public] un-Success", func(t *testing.T) { - expSucceeds := false expRetMsg := string(WsOpTypePong) expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" w := &WebSocketOpEvent{ - Success: &expSucceeds, - RetMsg: &expRetMsg, - ReqId: &expReqId, + Success: false, + RetMsg: expRetMsg, + ReqId: expReqId, ConnId: "test-conndid", Op: WsOpTypePing, Args: nil, } - assert.Error(t, fmt.Errorf("unexpeted response of pong: %+v", w), w.IsValid()) - }) - - t.Run("[public] missing Success field", func(t *testing.T) { - expRetMsg := string(WsOpTypePong) - expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" - - w := &WebSocketOpEvent{ - RetMsg: &expRetMsg, - ReqId: &expReqId, - ConnId: "test-conndid", - Op: WsOpTypePing, - Args: nil, - } - assert.Error(t, fmt.Errorf("unexpeted response of pong: %+v", w), w.IsValid()) + assert.Equal(t, fmt.Errorf("unexpected response result: %+v", w), w.IsValid()) }) t.Run("[public] invalid ret msg", func(t *testing.T) { - expSucceeds := false expRetMsg := "PINGPONGPINGPONG" expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" w := &WebSocketOpEvent{ - Success: &expSucceeds, - RetMsg: &expRetMsg, - ReqId: &expReqId, + Success: false, + RetMsg: expRetMsg, + ReqId: expReqId, ConnId: "test-conndid", Op: WsOpTypePing, Args: nil, } - assert.Error(t, fmt.Errorf("unexpeted response of pong: %+v", w), w.IsValid()) + assert.Equal(t, fmt.Errorf("unexpected response result: %+v", w), w.IsValid()) }) t.Run("[public] missing RetMsg field", func(t *testing.T) { expReqId := "b26704da-f5af-44c2-bdf7-935d6739e1a0" w := &WebSocketOpEvent{ - ReqId: &expReqId, + ReqId: expReqId, ConnId: "test-conndid", Op: WsOpTypePing, Args: nil, } - assert.Error(t, fmt.Errorf("unexpeted response of pong: %+v", w), w.IsValid()) + assert.Equal(t, fmt.Errorf("unexpected response result: %+v", w), w.IsValid()) }) t.Run("unexpected op type", func(t *testing.T) { w := &WebSocketOpEvent{ Op: WsOpType("unexpected"), } - assert.Error(t, fmt.Errorf("unexpected op type: %+v", w), w.IsValid()) + assert.Equal(t, fmt.Errorf("unexpected op type: %+v", w), w.IsValid()) }) t.Run("[subscribe] valid", func(t *testing.T) { - expSucceeds := true - expRetMsg := "" + expRetMsg := "subscribe" w := &WebSocketOpEvent{ - Success: &expSucceeds, - RetMsg: &expRetMsg, - ReqId: nil, + Success: true, + RetMsg: expRetMsg, + ReqId: "", ConnId: "test-conndid", Op: WsOpTypeSubscribe, Args: nil, @@ -207,17 +187,41 @@ func Test_WebSocketEventIsValid(t *testing.T) { }) t.Run("[subscribe] un-succeeds", func(t *testing.T) { - expSucceeds := false expRetMsg := "" w := &WebSocketOpEvent{ - Success: &expSucceeds, - RetMsg: &expRetMsg, - ReqId: nil, + Success: false, + RetMsg: expRetMsg, + ReqId: "", ConnId: "test-conndid", Op: WsOpTypeSubscribe, Args: nil, } - assert.Error(t, fmt.Errorf("unexpected subscribe result: %+v", w), w.IsValid()) + assert.Equal(t, fmt.Errorf("unexpected response result: %+v", w), w.IsValid()) + }) + + t.Run("[auth] valid", func(t *testing.T) { + w := &WebSocketOpEvent{ + Success: true, + RetMsg: "", + ReqId: "", + ConnId: "test-conndid", + Op: WsOpTypeAuth, + Args: nil, + } + assert.NoError(t, w.IsValid()) + }) + + t.Run("[subscribe] un-succeeds", func(t *testing.T) { + expRetMsg := "invalid signature" + w := &WebSocketOpEvent{ + Success: false, + RetMsg: expRetMsg, + ReqId: "", + ConnId: "test-conndid", + Op: WsOpTypeAuth, + Args: nil, + } + assert.Equal(t, fmt.Errorf("unexpected response result: %+v", w), w.IsValid()) }) } From 8b68354d89bf4fff732589689c4e4668f5afcc0d Mon Sep 17 00:00:00 2001 From: Edwin Date: Mon, 7 Aug 2023 18:48:16 +0800 Subject: [PATCH 1313/1392] pkg/exchange: add balance snapshot event --- pkg/exchange/bybit/stream.go | 42 +++++++++++++++- pkg/exchange/bybit/stream_callbacks.go | 10 ++++ pkg/exchange/bybit/stream_test.go | 29 +++++++++++ pkg/exchange/bybit/types.go | 66 +++++++++++++++++++++++++- pkg/exchange/bybit/types_test.go | 14 +++++- 5 files changed, 158 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 687d9452d8..6846d62589 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -32,7 +32,8 @@ type Stream struct { key, secret string types.StandardStream - bookEventCallbacks []func(e BookEvent) + bookEventCallbacks []func(e BookEvent) + walletEventCallbacks []func(e []*WalletEvent) } func NewStream(key, secret string) *Stream { @@ -50,6 +51,7 @@ func NewStream(key, secret string) *Stream { stream.OnConnect(stream.handlerConnect) stream.OnBookEvent(stream.handleBookEvent) + stream.OnWalletEvent(stream.handleWalletEvent) return stream } @@ -72,6 +74,9 @@ func (s *Stream) dispatchEvent(event interface{}) { case *BookEvent: s.EmitBookEvent(*e) + + case []*WalletEvent: + s.EmitWalletEvent(e) } } @@ -98,6 +103,10 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { book.Type = e.WebSocketTopicEvent.Type return &book, nil + + case TopicTypeWallet: + var wallets []*WalletEvent + return wallets, json.Unmarshal(e.WebSocketTopicEvent.Data, &wallets) } } @@ -164,6 +173,7 @@ func (s *Stream) handlerConnect() { Args: topics, }); err != nil { log.WithError(err).Error("failed to send subscription request") + return } } else { expires := strconv.FormatInt(time.Now().Add(wsAuthRequest).In(time.UTC).UnixMilli(), 10) @@ -177,6 +187,17 @@ func (s *Stream) handlerConnect() { }, }); err != nil { log.WithError(err).Error("failed to auth request") + return + } + + if err := s.Conn.WriteJSON(WebsocketOp{ + Op: WsOpTypeSubscribe, + Args: []string{ + string(TopicTypeWallet), + }, + }); err != nil { + log.WithError(err).Error("failed to send subscription request") + return } } } @@ -206,3 +227,22 @@ func (s *Stream) handleBookEvent(e BookEvent) { s.EmitBookUpdate(orderBook) } } + +func (s *Stream) handleWalletEvent(events []*WalletEvent) { + bm := types.BalanceMap{} + for _, event := range events { + if event.AccountType != AccountTypeSpot { + return + } + + for _, obj := range event.Coins { + bm[obj.Coin] = types.Balance{ + Currency: obj.Coin, + Available: obj.Free, + Locked: obj.Locked, + } + } + } + + s.StandardStream.EmitBalanceSnapshot(bm) +} diff --git a/pkg/exchange/bybit/stream_callbacks.go b/pkg/exchange/bybit/stream_callbacks.go index 7940157b88..fee4e3cd41 100644 --- a/pkg/exchange/bybit/stream_callbacks.go +++ b/pkg/exchange/bybit/stream_callbacks.go @@ -13,3 +13,13 @@ func (s *Stream) EmitBookEvent(e BookEvent) { cb(e) } } + +func (s *Stream) OnWalletEvent(cb func(e []*WalletEvent)) { + s.walletEventCallbacks = append(s.walletEventCallbacks, cb) +} + +func (s *Stream) EmitWalletEvent(e []*WalletEvent) { + for _, cb := range s.walletEventCallbacks { + cb(e) + } +} diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go index 2eff7d4918..64c48ec71e 100644 --- a/pkg/exchange/bybit/stream_test.go +++ b/pkg/exchange/bybit/stream_test.go @@ -36,6 +36,35 @@ func TestStream(t *testing.T) { c := make(chan struct{}) <-c }) + + t.Run("book test", func(t *testing.T) { + s.Subscribe(types.BookChannel, "BTCUSDT", types.SubscribeOptions{ + Depth: types.DepthLevel50, + }) + s.SetPublicOnly() + err := s.Connect(context.Background()) + assert.NoError(t, err) + + s.OnBookSnapshot(func(book types.SliceOrderBook) { + t.Log("got snapshot", book) + }) + s.OnBookUpdate(func(book types.SliceOrderBook) { + t.Log("got update", book) + }) + c := make(chan struct{}) + <-c + }) + + t.Run("wallet test", func(t *testing.T) { + err := s.Connect(context.Background()) + assert.NoError(t, err) + + s.OnBalanceSnapshot(func(balances types.BalanceMap) { + t.Log("got snapshot", balances) + }) + c := make(chan struct{}) + <-c + }) } func TestStream_parseWebSocketEvent(t *testing.T) { diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index 6ccc8f7e59..a1a0d71345 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -64,7 +64,9 @@ func (w *WebSocketOpEvent) IsValid() error { } return nil case WsOpTypeSubscribe: - if !w.Success || WsOpType(w.RetMsg) != WsOpTypeSubscribe { + // in the public channel, you can get RetMsg = 'subscribe', but in the private channel, you cannot. + // so, we only verify that success is true. + if !w.Success { return fmt.Errorf("unexpected response result: %+v", w) } return nil @@ -77,6 +79,7 @@ type TopicType string const ( TopicTypeOrderBook TopicType = "orderbook" + TopicTypeWallet TopicType = "wallet" ) type DataType string @@ -137,3 +140,64 @@ func getTopicType(topic string) TopicType { } return TopicType(slice[0]) } + +type AccountType string + +const AccountTypeSpot AccountType = "SPOT" + +type WalletEvent struct { + AccountType AccountType `json:"accountType"` + AccountIMRate fixedpoint.Value `json:"accountIMRate"` + AccountMMRate fixedpoint.Value `json:"accountMMRate"` + TotalEquity fixedpoint.Value `json:"totalEquity"` + TotalWalletBalance fixedpoint.Value `json:"totalWalletBalance"` + TotalMarginBalance fixedpoint.Value `json:"totalMarginBalance"` + TotalAvailableBalance fixedpoint.Value `json:"totalAvailableBalance"` + TotalPerpUPL fixedpoint.Value `json:"totalPerpUPL"` + TotalInitialMargin fixedpoint.Value `json:"totalInitialMargin"` + TotalMaintenanceMargin fixedpoint.Value `json:"totalMaintenanceMargin"` + // Account LTV: account total borrowed size / (account total equity + account total borrowed size). + // In non-unified mode & unified (inverse) & unified (isolated_margin), the field will be returned as an empty string. + AccountLTV fixedpoint.Value `json:"accountLTV"` + Coins []struct { + Coin string `json:"coin"` + // Equity of current coin + Equity fixedpoint.Value `json:"equity"` + // UsdValue of current coin. If this coin cannot be collateral, then it is 0 + UsdValue fixedpoint.Value `json:"usdValue"` + // WalletBalance of current coin + WalletBalance fixedpoint.Value `json:"walletBalance"` + // Free available balance for Spot wallet. This is a unique field for Normal SPOT + Free fixedpoint.Value + // Locked balance for Spot wallet. This is a unique field for Normal SPOT + Locked fixedpoint.Value + // Available amount to withdraw of current coin + AvailableToWithdraw fixedpoint.Value `json:"availableToWithdraw"` + // Available amount to borrow of current coin + AvailableToBorrow fixedpoint.Value `json:"availableToBorrow"` + // Borrow amount of current coin + BorrowAmount fixedpoint.Value `json:"borrowAmount"` + // Accrued interest + AccruedInterest fixedpoint.Value `json:"accruedInterest"` + // Pre-occupied margin for order. For portfolio margin mode, it returns "" + TotalOrderIM fixedpoint.Value `json:"totalOrderIM"` + // Sum of initial margin of all positions + Pre-occupied liquidation fee. For portfolio margin mode, it returns "" + TotalPositionIM fixedpoint.Value `json:"totalPositionIM"` + // Sum of maintenance margin for all positions. For portfolio margin mode, it returns "" + TotalPositionMM fixedpoint.Value `json:"totalPositionMM"` + // Unrealised P&L + UnrealisedPnl fixedpoint.Value `json:"unrealisedPnl"` + // Cumulative Realised P&L + CumRealisedPnl fixedpoint.Value `json:"cumRealisedPnl"` + // Bonus. This is a unique field for UNIFIED account + Bonus fixedpoint.Value `json:"bonus"` + // Whether it can be used as a margin collateral currency (platform) + // - When marginCollateral=false, then collateralSwitch is meaningless + // - This is a unique field for UNIFIED account + CollateralSwitch bool `json:"collateralSwitch"` + // Whether the collateral is turned on by user (user) + // - When marginCollateral=true, then collateralSwitch is meaningful + // - This is a unique field for UNIFIED account + MarginCollateral bool `json:"marginCollateral"` + } `json:"coin"` +} diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go index 670098503f..3332814cf8 100644 --- a/pkg/exchange/bybit/types_test.go +++ b/pkg/exchange/bybit/types_test.go @@ -173,7 +173,7 @@ func Test_WebSocketEventIsValid(t *testing.T) { assert.Equal(t, fmt.Errorf("unexpected op type: %+v", w), w.IsValid()) }) - t.Run("[subscribe] valid", func(t *testing.T) { + t.Run("[subscribe] valid with public channel", func(t *testing.T) { expRetMsg := "subscribe" w := &WebSocketOpEvent{ Success: true, @@ -186,6 +186,18 @@ func Test_WebSocketEventIsValid(t *testing.T) { assert.NoError(t, w.IsValid()) }) + t.Run("[subscribe] valid with private channel", func(t *testing.T) { + w := &WebSocketOpEvent{ + Success: true, + RetMsg: "", + ReqId: "", + ConnId: "test-conndid", + Op: WsOpTypeSubscribe, + Args: nil, + } + assert.NoError(t, w.IsValid()) + }) + t.Run("[subscribe] un-succeeds", func(t *testing.T) { expRetMsg := "" w := &WebSocketOpEvent{ From 0c6b68c4f6c29a7f2fc6362382bad9def23370f3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 19:03:14 +0800 Subject: [PATCH 1314/1392] add deposit2transfer strategy --- pkg/strategy/deposit2transfer/strategy.go | 138 ++++++++++++++++++++++ pkg/strategy/deposit2transfer/transfer.go | 104 ++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 pkg/strategy/deposit2transfer/strategy.go create mode 100644 pkg/strategy/deposit2transfer/transfer.go diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go new file mode 100644 index 0000000000..c9741889dd --- /dev/null +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -0,0 +1,138 @@ +package deposit2transfer + +import ( + "context" + "errors" + "fmt" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "deposit2transfer" + +var log = logrus.WithField("strategy", ID) + +var errNotBinanceExchange = errors.New("not binance exchange, currently only support binance exchange") + +func init() { + // Register the pointer of the strategy struct, + // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) + // Note: built-in strategies need to imported manually in the bbgo cmd package. + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + Environment *bbgo.Environment + + Interval types.Interval `json:"interval"` + + SpotSession string `json:"spotSession"` + FuturesSession string `json:"futuresSession"` + + spotSession, futuresSession *bbgo.ExchangeSession + + binanceFutures, binanceSpot *binance.Exchange +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {} + +func (s *Strategy) Defaults() error { + if s.Interval == "" { + s.Interval = types.Interval1m + } + + return nil +} + +func (s *Strategy) Validate() error { + if len(s.SpotSession) == 0 { + return errors.New("spotSession name is required") + } + + if len(s.FuturesSession) == 0 { + return errors.New("futuresSession name is required") + } + + return nil +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s", ID) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + var ok bool + s.binanceFutures, ok = session.Exchange.(*binance.Exchange) + if !ok { + return errNotBinanceExchange + } + + s.binanceSpot, ok = session.Exchange.(*binance.Exchange) + if !ok { + return errNotBinanceExchange + } + + // instanceID := s.InstanceID() + /* + if err := s.transferIn(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, fixedpoint.Zero); err != nil { + log.WithError(err).Errorf("futures asset transfer in error") + } + + if err := s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, fixedpoint.Zero); err != nil { + log.WithError(err).Errorf("futures asset transfer out error") + } + + if err := backoff.RetryGeneral(ctx, func() error { + return s.transferIn(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, trade.Quantity) + }); err != nil { + log.WithError(err).Errorf("spot-to-futures transfer in retry failed") + return err + } + + if err := backoff.RetryGeneral(ctx, func() error { + return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, quantity) + }); err != nil { + log.WithError(err).Errorf("spot-to-futures transfer in retry failed") + return err + } + */ + + if binanceStream, ok := s.futuresSession.UserDataStream.(*binance.Stream); ok { + binanceStream.OnAccountUpdateEvent(func(e *binance.AccountUpdateEvent) { + s.handleAccountUpdate(ctx, e) + }) + } + + return nil +} + +func (s *Strategy) handleAccountUpdate(ctx context.Context, e *binance.AccountUpdateEvent) { + switch e.AccountUpdate.EventReasonType { + case binance.AccountUpdateEventReasonDeposit: + case binance.AccountUpdateEventReasonWithdraw: + case binance.AccountUpdateEventReasonFundingFee: + // EventBase:{ + // Event:ACCOUNT_UPDATE + // Time:1679760000932 + // } + // Transaction:1679760000927 + // AccountUpdate:{ + // EventReasonType:FUNDING_FEE + // Balances:[{ + // Asset:USDT + // WalletBalance:56.64251742 + // CrossWalletBalance:56.64251742 + // BalanceChange:-0.00037648 + // }] + // } + // } + } +} diff --git a/pkg/strategy/deposit2transfer/transfer.go b/pkg/strategy/deposit2transfer/transfer.go new file mode 100644 index 0000000000..d7573f6a62 --- /dev/null +++ b/pkg/strategy/deposit2transfer/transfer.go @@ -0,0 +1,104 @@ +package deposit2transfer + +import ( + "context" + "fmt" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type FuturesTransfer interface { + TransferFuturesAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error + QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) +} + +func (s *Strategy) resetTransfer(ctx context.Context, ex FuturesTransfer, asset string) error { + balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) + if err != nil { + return err + } + + b, ok := balances[asset] + if !ok { + return nil + } + + amount := b.MaxWithdrawAmount + if amount.IsZero() { + return nil + } + + log.Infof("transfering out futures account asset %s %s", amount, asset) + + err = ex.TransferFuturesAccountAsset(ctx, asset, amount, types.TransferOut) + if err != nil { + return err + } + return nil +} + +func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset string, quantity fixedpoint.Value) error { + balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) + if err != nil { + return err + } + + b, ok := balances[asset] + if !ok { + return fmt.Errorf("%s balance not found", asset) + } + + log.Infof("found futures balance: %+v", b) + + // add the previous pending base transfer and the current trade quantity + amount := b.MaxWithdrawAmount + + // try to transfer more if we enough balance + amount = fixedpoint.Min(amount, b.MaxWithdrawAmount) + + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here + if amount.IsZero() { + return nil + } + + // de-leverage and get the collateral base quantity + collateralBase := s.FuturesPosition.GetBase().Abs().Div(s.Leverage) + _ = collateralBase + + // if s.State.TotalBaseTransfer.Compare(collateralBase) + + log.Infof("transfering out futures account asset %s %s", amount, asset) + if err := ex.TransferFuturesAccountAsset(ctx, asset, amount, types.TransferOut); err != nil { + return err + } + + return nil +} + +func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, asset string, quantity fixedpoint.Value) error { + balances, err := s.spotSession.Exchange.QueryAccountBalances(ctx) + if err != nil { + return err + } + + b, ok := balances[asset] + if !ok { + return fmt.Errorf("%s balance not found", asset) + } + + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here + if !quantity.IsZero() && b.Available.Compare(quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", quantity, asset) + return nil + } + + amount := b.Available + + log.Infof("transfering in futures account asset %s %s", amount, asset) + if err := ex.TransferFuturesAccountAsset(ctx, asset, amount, types.TransferIn); err != nil { + return err + } + + return nil +} From 92691eda242d200710ee1c2dae0d7ee9a4dddaf1 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Aug 2023 15:27:00 +0800 Subject: [PATCH 1315/1392] binanceapi: add margin transfer api --- .../transfer_cross_margin_account_request.go | 27 +++ ...cross_margin_account_request_requestgen.go | 157 +++++++++++++ ...ransfer_isolated_margin_account_request.go | 32 +++ ...lated_margin_account_request_requestgen.go | 209 ++++++++++++++++++ pkg/exchange/binance/exchange.go | 8 + 5 files changed, 433 insertions(+) create mode 100644 pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request.go create mode 100644 pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/transfer_isolated_margin_account_request.go create mode 100644 pkg/exchange/binance/binanceapi/transfer_isolated_margin_account_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request.go b/pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request.go new file mode 100644 index 0000000000..34dcfad28a --- /dev/null +++ b/pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request.go @@ -0,0 +1,27 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" +) + +type TransferResponse struct { + TranId int `json:"tranId"` +} + +//go:generate requestgen -method POST -url "/sapi/v1/margin/transfer" -type TransferCrossMarginAccountRequest -responseType .TransferResponse +type TransferCrossMarginAccountRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + + // transferType: + // 1: transfer from main account to cross margin account + // 2: transfer from cross margin account to main account + transferType int `param:"type"` + + amount string `param:"amount"` +} + +func (c *RestClient) NewTransferCrossMarginAccountRequest() *TransferCrossMarginAccountRequest { + return &TransferCrossMarginAccountRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request_requestgen.go b/pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request_requestgen.go new file mode 100644 index 0000000000..48aa1f5203 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request_requestgen.go @@ -0,0 +1,157 @@ +// Code generated by "requestgen -method POST -url /sapi/v1/margin/transfer -type TransferCrossMarginAccountRequest -responseType .TransferResponse"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (t *TransferCrossMarginAccountRequest) Asset(asset string) *TransferCrossMarginAccountRequest { + t.asset = asset + return t +} + +func (t *TransferCrossMarginAccountRequest) Amount(amount string) *TransferCrossMarginAccountRequest { + t.amount = amount + return t +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (t *TransferCrossMarginAccountRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (t *TransferCrossMarginAccountRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := t.asset + + // assign parameter of asset + params["asset"] = asset + // check amount field -> json key amount + amount := t.amount + + // assign parameter of amount + params["amount"] = amount + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (t *TransferCrossMarginAccountRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := t.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if t.isVarSlice(_v) { + t.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (t *TransferCrossMarginAccountRequest) GetParametersJSON() ([]byte, error) { + params, err := t.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (t *TransferCrossMarginAccountRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (t *TransferCrossMarginAccountRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (t *TransferCrossMarginAccountRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (t *TransferCrossMarginAccountRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (t *TransferCrossMarginAccountRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := t.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (t *TransferCrossMarginAccountRequest) Do(ctx context.Context) (*TransferResponse, error) { + + params, err := t.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/sapi/v1/margin/transfer" + + req, err := t.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := t.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse TransferResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/transfer_isolated_margin_account_request.go b/pkg/exchange/binance/binanceapi/transfer_isolated_margin_account_request.go new file mode 100644 index 0000000000..276e204491 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/transfer_isolated_margin_account_request.go @@ -0,0 +1,32 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" +) + +type AccountType string + +const ( + AccountTypeSpot AccountType = "SPOT" + AccountTypeIsolatedMargin AccountType = "ISOLATED_MARGIN" +) + +//go:generate requestgen -method POST -url "/sapi/v1/margin/isolated/transfer" -type TransferIsolatedMarginAccountRequest -responseType .TransferResponse +type TransferIsolatedMarginAccountRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + symbol string `param:"symbol"` + + // transFrom: "SPOT", "ISOLATED_MARGIN" + transFrom AccountType `param:"transFrom"` + + // transTo: "SPOT", "ISOLATED_MARGIN" + transTo AccountType `param:"transTo"` + + amount string `param:"amount"` +} + +func (c *RestClient) NewTransferIsolatedMarginAccountRequest() *TransferIsolatedMarginAccountRequest { + return &TransferIsolatedMarginAccountRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/transfer_isolated_margin_account_request_requestgen.go b/pkg/exchange/binance/binanceapi/transfer_isolated_margin_account_request_requestgen.go new file mode 100644 index 0000000000..1d6ce0ec80 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/transfer_isolated_margin_account_request_requestgen.go @@ -0,0 +1,209 @@ +// Code generated by "requestgen -method POST -url /sapi/v1/margin/isolated/transfer -type TransferIsolatedMarginAccountRequest -responseType .TransferResponse"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (t *TransferIsolatedMarginAccountRequest) Asset(asset string) *TransferIsolatedMarginAccountRequest { + t.asset = asset + return t +} + +func (t *TransferIsolatedMarginAccountRequest) Symbol(symbol string) *TransferIsolatedMarginAccountRequest { + t.symbol = symbol + return t +} + +func (t *TransferIsolatedMarginAccountRequest) TransFrom(transFrom AccountType) *TransferIsolatedMarginAccountRequest { + t.transFrom = transFrom + return t +} + +func (t *TransferIsolatedMarginAccountRequest) TransTo(transTo AccountType) *TransferIsolatedMarginAccountRequest { + t.transTo = transTo + return t +} + +func (t *TransferIsolatedMarginAccountRequest) Amount(amount string) *TransferIsolatedMarginAccountRequest { + t.amount = amount + return t +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (t *TransferIsolatedMarginAccountRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (t *TransferIsolatedMarginAccountRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := t.asset + + // assign parameter of asset + params["asset"] = asset + // check symbol field -> json key symbol + symbol := t.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check transFrom field -> json key transFrom + transFrom := t.transFrom + + // TEMPLATE check-valid-values + switch transFrom { + case AccountTypeSpot, AccountTypeIsolatedMargin: + params["transFrom"] = transFrom + + default: + return nil, fmt.Errorf("transFrom value %v is invalid", transFrom) + + } + // END TEMPLATE check-valid-values + + // assign parameter of transFrom + params["transFrom"] = transFrom + // check transTo field -> json key transTo + transTo := t.transTo + + // TEMPLATE check-valid-values + switch transTo { + case AccountTypeSpot, AccountTypeIsolatedMargin: + params["transTo"] = transTo + + default: + return nil, fmt.Errorf("transTo value %v is invalid", transTo) + + } + // END TEMPLATE check-valid-values + + // assign parameter of transTo + params["transTo"] = transTo + // check amount field -> json key amount + amount := t.amount + + // assign parameter of amount + params["amount"] = amount + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (t *TransferIsolatedMarginAccountRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := t.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if t.isVarSlice(_v) { + t.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (t *TransferIsolatedMarginAccountRequest) GetParametersJSON() ([]byte, error) { + params, err := t.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (t *TransferIsolatedMarginAccountRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (t *TransferIsolatedMarginAccountRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (t *TransferIsolatedMarginAccountRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (t *TransferIsolatedMarginAccountRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (t *TransferIsolatedMarginAccountRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := t.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (t *TransferIsolatedMarginAccountRequest) Do(ctx context.Context) (*TransferResponse, error) { + + params, err := t.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/sapi/v1/margin/isolated/transfer" + + req, err := t.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := t.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse TransferResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 9f8534fc8b..82116097e7 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -372,6 +372,14 @@ func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) e return nil } +func (e *Exchange) TransferMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { + if e.IsMargin && !e.IsIsolatedMargin { + return e.transferCrossMarginAccountAsset(ctx, asset, amount, io) + } + + return errors.New("isolated margin transfer is not supported") +} + // transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { req := e.client.NewMarginTransferService() From 8b1cefc699ee53a9da39b4d947027e64e00ce3a4 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Aug 2023 17:14:48 +0800 Subject: [PATCH 1316/1392] binance: integerate isolated margin / cross margin transfer --- ...cross_margin_account_request_requestgen.go | 10 ++++ pkg/exchange/binance/exchange.go | 51 +++++++++++++++---- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request_requestgen.go b/pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request_requestgen.go index 48aa1f5203..c48500a007 100644 --- a/pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request_requestgen.go +++ b/pkg/exchange/binance/binanceapi/transfer_cross_margin_account_request_requestgen.go @@ -16,6 +16,11 @@ func (t *TransferCrossMarginAccountRequest) Asset(asset string) *TransferCrossMa return t } +func (t *TransferCrossMarginAccountRequest) TransferType(transferType int) *TransferCrossMarginAccountRequest { + t.transferType = transferType + return t +} + func (t *TransferCrossMarginAccountRequest) Amount(amount string) *TransferCrossMarginAccountRequest { t.amount = amount return t @@ -41,6 +46,11 @@ func (t *TransferCrossMarginAccountRequest) GetParameters() (map[string]interfac // assign parameter of asset params["asset"] = asset + // check transferType field -> json key type + transferType := t.transferType + + // assign parameter of transferType + params["type"] = transferType // check amount field -> json key amount amount := t.amount diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 82116097e7..5954a6d82a 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -373,34 +373,53 @@ func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) e } func (e *Exchange) TransferMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { - if e.IsMargin && !e.IsIsolatedMargin { - return e.transferCrossMarginAccountAsset(ctx, asset, amount, io) + if e.IsMargin { + if e.IsIsolatedMargin { + return e.transferIsolatedMarginAccountAsset(ctx, asset, amount, io) + } else { + return e.transferCrossMarginAccountAsset(ctx, asset, amount, io) + } } return errors.New("isolated margin transfer is not supported") } +func (e *Exchange) transferIsolatedMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { + req := e.client2.NewTransferIsolatedMarginAccountRequest() + req.Symbol(e.IsolatedMarginSymbol) + + switch io { + case types.TransferIn: + req.TransFrom(binanceapi.AccountTypeSpot) + req.TransTo(binanceapi.AccountTypeIsolatedMargin) + + case types.TransferOut: + req.TransFrom(binanceapi.AccountTypeIsolatedMargin) + req.TransTo(binanceapi.AccountTypeSpot) + } + + req.Asset(asset) + req.Amount(amount.String()) + resp, err := req.Do(ctx) + return logResponse(resp, err, req) +} + // transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { - req := e.client.NewMarginTransferService() + req := e.client2.NewTransferCrossMarginAccountRequest() req.Asset(asset) req.Amount(amount.String()) if io == types.TransferIn { - req.Type(binance.MarginTransferTypeToMargin) + req.TransferType(int(binance.MarginTransferTypeToMargin)) } else if io == types.TransferOut { - req.Type(binance.MarginTransferTypeToMain) + req.TransferType(int(binance.MarginTransferTypeToMain)) } else { return fmt.Errorf("unexpected transfer direction: %d given", io) } resp, err := req.Do(ctx) - if err != nil { - return err - } - - log.Debugf("cross margin transfer %f %s, transaction id = %d", amount.Float64(), asset, resp.TranID) - return err + return logResponse(resp, err, req) } func (e *Exchange) QueryCrossMarginAccount(ctx context.Context) (*types.Account, error) { @@ -1450,3 +1469,13 @@ func calculateMarginTolerance(marginLevel fixedpoint.Value) fixedpoint.Value { // = 1.0 - (1.1 / marginLevel) return fixedpoint.One.Sub(fixedpoint.NewFromFloat(1.1).Div(marginLevel)) } + +func logResponse(resp interface{}, err error, req interface{}) error { + if err != nil { + log.WithError(err).Errorf("%T: error %+v", req, resp) + return err + } + + log.Infof("%T: response: %+v", req, resp) + return nil +} From 0118f33bfc29590e457db90f0feba795764dc566 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Aug 2023 19:32:10 +0800 Subject: [PATCH 1317/1392] binance: finalize TransferMarginAccountAsset method --- pkg/exchange/binance/exchange.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 5954a6d82a..a8ad9438fc 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -372,16 +372,22 @@ func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) e return nil } -func (e *Exchange) TransferMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { - if e.IsMargin { - if e.IsIsolatedMargin { - return e.transferIsolatedMarginAccountAsset(ctx, asset, amount, io) - } else { - return e.transferCrossMarginAccountAsset(ctx, asset, amount, io) - } +// TransferMarginAccountAsset transfers the asset into/out from the margin account +// +// types.TransferIn => Spot to Margin +// types.TransferOut => Margin to Spot +// +// to call this method, you must set the IsMargin = true +func (e *Exchange) TransferMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { + if !e.IsMargin { + return errors.New("you can not operate margin transfer on a non-margin session") + } + + if e.IsIsolatedMargin { + return e.transferIsolatedMarginAccountAsset(ctx, asset, amount, io) } - return errors.New("isolated margin transfer is not supported") + return e.transferCrossMarginAccountAsset(ctx, asset, amount, io) } func (e *Exchange) transferIsolatedMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { From 9346e7d1f611c021d4d4ce0f03a5ee9a8b6f0da9 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Aug 2023 20:03:52 +0800 Subject: [PATCH 1318/1392] binance: replace emptyTime with IsZero --- pkg/exchange/binance/exchange.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index a8ad9438fc..23c49e56d3 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -595,8 +595,7 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since } func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []types.Deposit, err error) { - var emptyTime = time.Time{} - if since == emptyTime { + if since.IsZero() { since, err = getLaunchDate() if err != nil { return nil, err From 9248f8ac24748e526f3a9f0fca7a156d66d6e5aa Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Aug 2023 20:11:55 +0800 Subject: [PATCH 1319/1392] binance: define DepositStatus for binance --- .../binanceapi/get_deposit_history_request.go | 17 +++++++++++++++-- pkg/exchange/binance/exchange.go | 11 +++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/binance/binanceapi/get_deposit_history_request.go b/pkg/exchange/binance/binanceapi/get_deposit_history_request.go index e000de98ce..41879e6bb7 100644 --- a/pkg/exchange/binance/binanceapi/get_deposit_history_request.go +++ b/pkg/exchange/binance/binanceapi/get_deposit_history_request.go @@ -9,18 +9,31 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +type DepositStatus int + +const ( + DepositStatusPending DepositStatus = 0 + DepositStatusSuccess DepositStatus = 1 + DepositStatusCredited DepositStatus = 6 + DepositStatusWrong DepositStatus = 7 + DepositStatusWaitingUserConfirm DepositStatus = 8 +) + type DepositHistory struct { Amount fixedpoint.Value `json:"amount"` Coin string `json:"coin"` Network string `json:"network"` - Status int `json:"status"` + Status DepositStatus `json:"status"` Address string `json:"address"` AddressTag string `json:"addressTag"` TxId string `json:"txId"` InsertTime types.MillisecondTimestamp `json:"insertTime"` TransferType int `json:"transferType"` UnlockConfirm int `json:"unlockConfirm"` - ConfirmTimes string `json:"confirmTimes"` + + // ConfirmTimes format = "current/required", for example: "7/16" + ConfirmTimes string `json:"confirmTimes"` + WalletType int `json:"walletType"` } //go:generate requestgen -method GET -url "/sapi/v1/capital/deposit/hisrec" -type GetDepositHistoryRequest -responseType []DepositHistory diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 23c49e56d3..62127f4048 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -625,13 +625,16 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, // 0(0:pending,6: credited but cannot withdraw, 1:success) // set the default status status := types.DepositStatus(fmt.Sprintf("code: %d", d.Status)) + + // https://www.binance.com/en/support/faq/115003736451 switch d.Status { - case 0: + case binanceapi.DepositStatusPending: status = types.DepositPending - case 6: - // https://www.binance.com/en/support/faq/115003736451 + + case binanceapi.DepositStatusCredited: status = types.DepositCredited - case 1: + + case binanceapi.DepositStatusSuccess: status = types.DepositSuccess } From 5f40dfa46262b4843d7b32d9e9ba98b8bc7895d3 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 7 Aug 2023 09:51:07 +0800 Subject: [PATCH 1320/1392] deposit2transfer: scan deposit history --- pkg/strategy/deposit2transfer/strategy.go | 154 +++++++++++++--------- pkg/strategy/deposit2transfer/transfer.go | 103 --------------- 2 files changed, 92 insertions(+), 165 deletions(-) diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index c9741889dd..4d91637f20 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -4,20 +4,32 @@ import ( "context" "errors" "fmt" + "sort" + "sync" + "time" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) +type marginTransferService interface { + TransferMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error +} + const ID = "deposit2transfer" var log = logrus.WithField("strategy", ID) var errNotBinanceExchange = errors.New("not binance exchange, currently only support binance exchange") +var errMarginTransferNotSupport = errors.New("exchange session does not support margin transfer") + +var errDepositHistoryNotSupport = errors.New("exchange session does not support deposit history query") + func init() { // Register the pointer of the strategy struct, // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) @@ -28,14 +40,20 @@ func init() { type Strategy struct { Environment *bbgo.Environment + Asset string `json:"asset"` + Interval types.Interval `json:"interval"` - SpotSession string `json:"spotSession"` - FuturesSession string `json:"futuresSession"` + binanceSpot *binance.Exchange + + marginTransferService marginTransferService - spotSession, futuresSession *bbgo.ExchangeSession + depositHistoryService types.ExchangeTransferService - binanceFutures, binanceSpot *binance.Exchange + lastDeposit *types.Deposit + + watchingDeposits map[string]types.Deposit + mu sync.Mutex } func (s *Strategy) ID() string { @@ -53,14 +71,6 @@ func (s *Strategy) Defaults() error { } func (s *Strategy) Validate() error { - if len(s.SpotSession) == 0 { - return errors.New("spotSession name is required") - } - - if len(s.FuturesSession) == 0 { - return errors.New("futuresSession name is required") - } - return nil } @@ -69,70 +79,90 @@ func (s *Strategy) InstanceID() string { } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.watchingDeposits = make(map[string]types.Deposit) + var ok bool - s.binanceFutures, ok = session.Exchange.(*binance.Exchange) - if !ok { - return errNotBinanceExchange - } s.binanceSpot, ok = session.Exchange.(*binance.Exchange) if !ok { return errNotBinanceExchange } - // instanceID := s.InstanceID() - /* - if err := s.transferIn(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, fixedpoint.Zero); err != nil { - log.WithError(err).Errorf("futures asset transfer in error") - } + s.marginTransferService, ok = session.Exchange.(marginTransferService) + if !ok { + return errMarginTransferNotSupport + } - if err := s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, fixedpoint.Zero); err != nil { - log.WithError(err).Errorf("futures asset transfer out error") - } + s.depositHistoryService, ok = session.Exchange.(types.ExchangeTransferService) + if !ok { + return errDepositHistoryNotSupport + } - if err := backoff.RetryGeneral(ctx, func() error { - return s.transferIn(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, trade.Quantity) - }); err != nil { - log.WithError(err).Errorf("spot-to-futures transfer in retry failed") - return err - } + return nil +} - if err := backoff.RetryGeneral(ctx, func() error { - return s.transferOut(ctx, s.binanceSpot, s.spotMarket.BaseCurrency, quantity) - }); err != nil { - log.WithError(err).Errorf("spot-to-futures transfer in retry failed") - return err +func (s *Strategy) tickWatcher(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + succeededDeposits, err := s.scanDepositHistory(ctx) + if err != nil { + log.WithError(err).Errorf("unable to scan deposit history") + continue + } + + for _, d := range succeededDeposits { + log.Infof("succeeded deposit: %+v", d) + } } - */ + } +} - if binanceStream, ok := s.futuresSession.UserDataStream.(*binance.Stream); ok { - binanceStream.OnAccountUpdateEvent(func(e *binance.AccountUpdateEvent) { - s.handleAccountUpdate(ctx, e) - }) +func (s *Strategy) scanDepositHistory(ctx context.Context) ([]types.Deposit, error) { + now := time.Now() + since := now.Add(-time.Hour) + deposits, err := s.depositHistoryService.QueryDepositHistory(ctx, s.Asset, since, now) + if err != nil { + return nil, err } - return nil -} + // sort the recent deposit records in descending order + sort.Slice(deposits, func(i, j int) bool { + return deposits[i].Time.Time().Before(deposits[j].Time.Time()) + }) + + s.mu.Lock() + defer s.mu.Lock() + + for _, deposit := range deposits { + if _, ok := s.watchingDeposits[deposit.TransactionID]; ok { + // if the deposit record is in the watch list, update it + s.watchingDeposits[deposit.TransactionID] = deposit + } else { + switch deposit.Status { + case types.DepositSuccess: + // ignore all deposits that are already success + continue + + case types.DepositCredited, types.DepositPending: + s.watchingDeposits[deposit.TransactionID] = deposit + } + } + } -func (s *Strategy) handleAccountUpdate(ctx context.Context, e *binance.AccountUpdateEvent) { - switch e.AccountUpdate.EventReasonType { - case binance.AccountUpdateEventReasonDeposit: - case binance.AccountUpdateEventReasonWithdraw: - case binance.AccountUpdateEventReasonFundingFee: - // EventBase:{ - // Event:ACCOUNT_UPDATE - // Time:1679760000932 - // } - // Transaction:1679760000927 - // AccountUpdate:{ - // EventReasonType:FUNDING_FEE - // Balances:[{ - // Asset:USDT - // WalletBalance:56.64251742 - // CrossWalletBalance:56.64251742 - // BalanceChange:-0.00037648 - // }] - // } - // } + var succeededDeposits []types.Deposit + for _, deposit := range deposits { + if deposit.Status == types.DepositSuccess { + succeededDeposits = append(succeededDeposits, deposit) + delete(s.watchingDeposits, deposit.TransactionID) + } } + + return succeededDeposits, nil } diff --git a/pkg/strategy/deposit2transfer/transfer.go b/pkg/strategy/deposit2transfer/transfer.go index d7573f6a62..7e8a8df125 100644 --- a/pkg/strategy/deposit2transfer/transfer.go +++ b/pkg/strategy/deposit2transfer/transfer.go @@ -1,104 +1 @@ package deposit2transfer - -import ( - "context" - "fmt" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -type FuturesTransfer interface { - TransferFuturesAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error - QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) -} - -func (s *Strategy) resetTransfer(ctx context.Context, ex FuturesTransfer, asset string) error { - balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) - if err != nil { - return err - } - - b, ok := balances[asset] - if !ok { - return nil - } - - amount := b.MaxWithdrawAmount - if amount.IsZero() { - return nil - } - - log.Infof("transfering out futures account asset %s %s", amount, asset) - - err = ex.TransferFuturesAccountAsset(ctx, asset, amount, types.TransferOut) - if err != nil { - return err - } - return nil -} - -func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, asset string, quantity fixedpoint.Value) error { - balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) - if err != nil { - return err - } - - b, ok := balances[asset] - if !ok { - return fmt.Errorf("%s balance not found", asset) - } - - log.Infof("found futures balance: %+v", b) - - // add the previous pending base transfer and the current trade quantity - amount := b.MaxWithdrawAmount - - // try to transfer more if we enough balance - amount = fixedpoint.Min(amount, b.MaxWithdrawAmount) - - // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if amount.IsZero() { - return nil - } - - // de-leverage and get the collateral base quantity - collateralBase := s.FuturesPosition.GetBase().Abs().Div(s.Leverage) - _ = collateralBase - - // if s.State.TotalBaseTransfer.Compare(collateralBase) - - log.Infof("transfering out futures account asset %s %s", amount, asset) - if err := ex.TransferFuturesAccountAsset(ctx, asset, amount, types.TransferOut); err != nil { - return err - } - - return nil -} - -func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, asset string, quantity fixedpoint.Value) error { - balances, err := s.spotSession.Exchange.QueryAccountBalances(ctx) - if err != nil { - return err - } - - b, ok := balances[asset] - if !ok { - return fmt.Errorf("%s balance not found", asset) - } - - // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if !quantity.IsZero() && b.Available.Compare(quantity) < 0 { - log.Infof("adding to pending base transfer: %s %s", quantity, asset) - return nil - } - - amount := b.Available - - log.Infof("transfering in futures account asset %s %s", amount, asset) - if err := ex.TransferFuturesAccountAsset(ctx, asset, amount, types.TransferIn); err != nil { - return err - } - - return nil -} From 4c4b9db47afd1af28121a437f652d5c5e5d6fe20 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 10:36:35 +0800 Subject: [PATCH 1321/1392] types,binance: add confirmation and unlockConfirm fields to Deposit --- pkg/exchange/binance/exchange.go | 2 ++ pkg/types/deposit.go | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 62127f4048..8f5a9217bf 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -647,6 +647,8 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, AddressTag: d.AddressTag, TransactionID: d.TxId, Status: status, + UnlockConfirm: d.UnlockConfirm, + Confirmation: d.ConfirmTimes, }) } diff --git a/pkg/types/deposit.go b/pkg/types/deposit.go index 2414da8fba..8d45fdbec5 100644 --- a/pkg/types/deposit.go +++ b/pkg/types/deposit.go @@ -34,6 +34,12 @@ type Deposit struct { AddressTag string `json:"addressTag"` TransactionID string `json:"transactionID" db:"txn_id"` Status DepositStatus `json:"status"` + + // Required confirm for unlock balance + UnlockConfirm int `json:"unlockConfirm"` + + // Confirmation format = "current/required", for example: "7/16" + Confirmation string `json:"confirmation"` } func (d Deposit) EffectiveTime() time.Time { From c55a6a46af456aa4caadef64f9f5a9cfd8306daa Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 11:07:21 +0800 Subject: [PATCH 1322/1392] deposit2transfer: check confirmation for deposits --- pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/deposit2transfer/strategy.go | 42 ++++++++++++++++------- pkg/types/deposit.go | 17 +++++++++ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 7eb31ca204..45ac2d7343 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -8,6 +8,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/bollmaker" _ "github.com/c9s/bbgo/pkg/strategy/convert" _ "github.com/c9s/bbgo/pkg/strategy/dca" + _ "github.com/c9s/bbgo/pkg/strategy/deposit2transfer" _ "github.com/c9s/bbgo/pkg/strategy/drift" _ "github.com/c9s/bbgo/pkg/strategy/elliottwave" _ "github.com/c9s/bbgo/pkg/strategy/emastop" diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index 4d91637f20..fea9e5a811 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -40,7 +40,7 @@ func init() { type Strategy struct { Environment *bbgo.Environment - Asset string `json:"asset"` + Assets []string `json:"assets"` Interval types.Interval `json:"interval"` @@ -111,23 +111,32 @@ func (s *Strategy) tickWatcher(ctx context.Context, interval time.Duration) { return case <-ticker.C: - succeededDeposits, err := s.scanDepositHistory(ctx) - if err != nil { - log.WithError(err).Errorf("unable to scan deposit history") - continue - } - - for _, d := range succeededDeposits { - log.Infof("succeeded deposit: %+v", d) + for _, asset := range s.Assets { + succeededDeposits, err := s.scanDepositHistory(ctx, asset, 4*time.Hour) + if err != nil { + log.WithError(err).Errorf("unable to scan deposit history") + continue + } + + for _, d := range succeededDeposits { + log.Infof("found succeeded deposit: %+v", d) + + bbgo.Notify("Found succeeded deposit %s %s, transferring asset into the margin account", d.Amount.String(), d.Asset) + if err2 := s.marginTransferService.TransferMarginAccountAsset(ctx, d.Asset, d.Amount, types.TransferIn); err2 != nil { + log.WithError(err2).Errorf("unable to transfer deposit asset into the margin account") + } + } } } } } -func (s *Strategy) scanDepositHistory(ctx context.Context) ([]types.Deposit, error) { +func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duration time.Duration) ([]types.Deposit, error) { + log.Infof("scanning %s deposit history...", asset) + now := time.Now() - since := now.Add(-time.Hour) - deposits, err := s.depositHistoryService.QueryDepositHistory(ctx, s.Asset, since, now) + since := now.Add(-duration) + deposits, err := s.depositHistoryService.QueryDepositHistory(ctx, asset, since, now) if err != nil { return nil, err } @@ -141,6 +150,10 @@ func (s *Strategy) scanDepositHistory(ctx context.Context) ([]types.Deposit, err defer s.mu.Lock() for _, deposit := range deposits { + if deposit.Asset != asset { + continue + } + if _, ok := s.watchingDeposits[deposit.TransactionID]; ok { // if the deposit record is in the watch list, update it s.watchingDeposits[deposit.TransactionID] = deposit @@ -159,6 +172,11 @@ func (s *Strategy) scanDepositHistory(ctx context.Context) ([]types.Deposit, err var succeededDeposits []types.Deposit for _, deposit := range deposits { if deposit.Status == types.DepositSuccess { + current, required := deposit.GetCurrentConfirmation() + if required > 0 && deposit.UnlockConfirm > 0 && current < deposit.UnlockConfirm { + continue + } + succeededDeposits = append(succeededDeposits, deposit) delete(s.watchingDeposits, deposit.TransactionID) } diff --git a/pkg/types/deposit.go b/pkg/types/deposit.go index 8d45fdbec5..9942732a17 100644 --- a/pkg/types/deposit.go +++ b/pkg/types/deposit.go @@ -2,6 +2,8 @@ package types import ( "fmt" + "strconv" + "strings" "time" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -42,6 +44,21 @@ type Deposit struct { Confirmation string `json:"confirmation"` } +func (d Deposit) GetCurrentConfirmation() (current int, required int) { + if len(d.Confirmation) == 0 { + return 0, 0 + } + + strs := strings.Split(d.Confirmation, "/") + if len(strs) < 2 { + return 0, 0 + } + + current, _ = strconv.Atoi(strs[0]) + required, _ = strconv.Atoi(strs[1]) + return current, required +} + func (d Deposit) EffectiveTime() time.Time { return d.Time.Time() } From f664ef2262c21da98ed45e773e3b1f96429f2901 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 8 Aug 2023 11:40:50 +0800 Subject: [PATCH 1323/1392] pkg/exchange: add order event --- pkg/exchange/bybit/stream.go | 26 ++++++++++++++++++++++++++ pkg/exchange/bybit/stream_callbacks.go | 10 ++++++++++ pkg/exchange/bybit/stream_test.go | 11 +++++++++++ pkg/exchange/bybit/types.go | 8 ++++++++ 4 files changed, 55 insertions(+) diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 6846d62589..e388722798 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -34,6 +34,7 @@ type Stream struct { bookEventCallbacks []func(e BookEvent) walletEventCallbacks []func(e []*WalletEvent) + orderEventCallbacks []func(e []*OrderEvent) } func NewStream(key, secret string) *Stream { @@ -52,6 +53,7 @@ func NewStream(key, secret string) *Stream { stream.OnConnect(stream.handlerConnect) stream.OnBookEvent(stream.handleBookEvent) stream.OnWalletEvent(stream.handleWalletEvent) + stream.OnOrderEvent(stream.handleOrderEvent) return stream } @@ -77,6 +79,9 @@ func (s *Stream) dispatchEvent(event interface{}) { case []*WalletEvent: s.EmitWalletEvent(e) + + case []*OrderEvent: + s.EmitOrderEvent(e) } } @@ -107,6 +112,11 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { case TopicTypeWallet: var wallets []*WalletEvent return wallets, json.Unmarshal(e.WebSocketTopicEvent.Data, &wallets) + + case TopicTypeOrder: + var orders []*OrderEvent + return orders, json.Unmarshal(e.WebSocketTopicEvent.Data, &orders) + } } @@ -194,6 +204,7 @@ func (s *Stream) handlerConnect() { Op: WsOpTypeSubscribe, Args: []string{ string(TopicTypeWallet), + string(TopicTypeOrder), }, }); err != nil { log.WithError(err).Error("failed to send subscription request") @@ -246,3 +257,18 @@ func (s *Stream) handleWalletEvent(events []*WalletEvent) { s.StandardStream.EmitBalanceSnapshot(bm) } + +func (s *Stream) handleOrderEvent(events []*OrderEvent) { + for _, event := range events { + if event.Category != bybitapi.CategorySpot { + return + } + + gOrder, err := toGlobalOrder(event.Order) + if err != nil { + log.WithError(err).Error("failed to convert to global order") + continue + } + s.StandardStream.EmitOrderUpdate(*gOrder) + } +} diff --git a/pkg/exchange/bybit/stream_callbacks.go b/pkg/exchange/bybit/stream_callbacks.go index fee4e3cd41..79b1bd7be0 100644 --- a/pkg/exchange/bybit/stream_callbacks.go +++ b/pkg/exchange/bybit/stream_callbacks.go @@ -23,3 +23,13 @@ func (s *Stream) EmitWalletEvent(e []*WalletEvent) { cb(e) } } + +func (s *Stream) OnOrderEvent(cb func(e []*OrderEvent)) { + s.orderEventCallbacks = append(s.orderEventCallbacks, cb) +} + +func (s *Stream) EmitOrderEvent(e []*OrderEvent) { + for _, cb := range s.orderEventCallbacks { + cb(e) + } +} diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go index 64c48ec71e..07ced5e43c 100644 --- a/pkg/exchange/bybit/stream_test.go +++ b/pkg/exchange/bybit/stream_test.go @@ -65,6 +65,17 @@ func TestStream(t *testing.T) { c := make(chan struct{}) <-c }) + + t.Run("order test", func(t *testing.T) { + err := s.Connect(context.Background()) + assert.NoError(t, err) + + s.OnOrderUpdate(func(order types.Order) { + t.Log("got update", order) + }) + c := make(chan struct{}) + <-c + }) } func TestStream_parseWebSocketEvent(t *testing.T) { diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index a1a0d71345..096e975dda 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -80,6 +81,7 @@ type TopicType string const ( TopicTypeOrderBook TopicType = "orderbook" TopicTypeWallet TopicType = "wallet" + TopicTypeOrder TopicType = "order" ) type DataType string @@ -201,3 +203,9 @@ type WalletEvent struct { MarginCollateral bool `json:"marginCollateral"` } `json:"coin"` } + +type OrderEvent struct { + bybitapi.Order + + Category bybitapi.Category `json:"category"` +} From 33b3d0ff57903e8c5022a5f9bdb36b2940dac5c6 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 11:48:06 +0800 Subject: [PATCH 1324/1392] types: use consistent receiver for MarginSettings --- pkg/types/margin.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/types/margin.go b/pkg/types/margin.go index 9d68049ec7..a546f2d694 100644 --- a/pkg/types/margin.go +++ b/pkg/types/margin.go @@ -119,8 +119,8 @@ type MarginSettings struct { IsolatedMarginSymbol string } -func (e MarginSettings) GetMarginSettings() MarginSettings { - return e +func (e *MarginSettings) GetMarginSettings() MarginSettings { + return *e } func (e *MarginSettings) UseMargin() { From c7845477b409c4234b0fbbe6fffddcf1fe352c09 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 11:58:36 +0800 Subject: [PATCH 1325/1392] deposit2transfer: remove binance spot struct field --- pkg/strategy/deposit2transfer/strategy.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index fea9e5a811..fb5ab03ec2 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -75,7 +75,7 @@ func (s *Strategy) Validate() error { } func (s *Strategy) InstanceID() string { - return fmt.Sprintf("%s", ID) + return fmt.Sprintf("%s-%s", ID, s.Assets) } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { @@ -83,11 +83,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se var ok bool - s.binanceSpot, ok = session.Exchange.(*binance.Exchange) - if !ok { - return errNotBinanceExchange - } - s.marginTransferService, ok = session.Exchange.(marginTransferService) if !ok { return errMarginTransferNotSupport From 241ce657c302279f6b9dccc502f4400e7155e9c0 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 12:01:30 +0800 Subject: [PATCH 1326/1392] binance: remove isMargin check --- pkg/exchange/binance/exchange.go | 4 ---- pkg/strategy/deposit2transfer/transfer.go | 1 - 2 files changed, 5 deletions(-) delete mode 100644 pkg/strategy/deposit2transfer/transfer.go diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 8f5a9217bf..3e62068c6c 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -379,10 +379,6 @@ func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) e // // to call this method, you must set the IsMargin = true func (e *Exchange) TransferMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { - if !e.IsMargin { - return errors.New("you can not operate margin transfer on a non-margin session") - } - if e.IsIsolatedMargin { return e.transferIsolatedMarginAccountAsset(ctx, asset, amount, io) } diff --git a/pkg/strategy/deposit2transfer/transfer.go b/pkg/strategy/deposit2transfer/transfer.go deleted file mode 100644 index 7e8a8df125..0000000000 --- a/pkg/strategy/deposit2transfer/transfer.go +++ /dev/null @@ -1 +0,0 @@ -package deposit2transfer From 423cb27288172bf5ceb4de5a80888f23ecf00bab Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 12:08:14 +0800 Subject: [PATCH 1327/1392] deposit2transfer: add more log messages --- pkg/strategy/deposit2transfer/strategy.go | 31 +++++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index fb5ab03ec2..19aff8a17b 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -47,11 +47,9 @@ type Strategy struct { binanceSpot *binance.Exchange marginTransferService marginTransferService - depositHistoryService types.ExchangeTransferService - lastDeposit *types.Deposit - + session *bbgo.ExchangeSession watchingDeposits map[string]types.Deposit mu sync.Mutex } @@ -79,6 +77,7 @@ func (s *Strategy) InstanceID() string { } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.session = session s.watchingDeposits = make(map[string]types.Deposit) var ok bool @@ -107,6 +106,8 @@ func (s *Strategy) tickWatcher(ctx context.Context, interval time.Duration) { case <-ticker.C: for _, asset := range s.Assets { + account := s.session.Account + succeededDeposits, err := s.scanDepositHistory(ctx, asset, 4*time.Hour) if err != nil { log.WithError(err).Errorf("unable to scan deposit history") @@ -116,8 +117,21 @@ func (s *Strategy) tickWatcher(ctx context.Context, interval time.Duration) { for _, d := range succeededDeposits { log.Infof("found succeeded deposit: %+v", d) - bbgo.Notify("Found succeeded deposit %s %s, transferring asset into the margin account", d.Amount.String(), d.Asset) - if err2 := s.marginTransferService.TransferMarginAccountAsset(ctx, d.Asset, d.Amount, types.TransferIn); err2 != nil { + bal, ok := account.Balance(d.Asset) + if !ok { + log.Errorf("unexpected error: %s balance not found", d.Asset) + continue + } + + log.Infof("%s balance: %+v", d.Asset, bal) + + amount := fixedpoint.Min(bal.Available, d.Amount) + + bbgo.Notify("Found succeeded deposit %s %s, transferring %s %s into the margin account", + d.Amount.String(), d.Asset, + amount.String(), d.Asset) + + if err2 := s.marginTransferService.TransferMarginAccountAsset(ctx, d.Asset, amount, types.TransferIn); err2 != nil { log.WithError(err2).Errorf("unable to transfer deposit asset into the margin account") } } @@ -145,6 +159,8 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio defer s.mu.Lock() for _, deposit := range deposits { + log.Infof("scanning deposit: %+v", deposit) + if deposit.Asset != asset { continue } @@ -156,9 +172,11 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio switch deposit.Status { case types.DepositSuccess: // ignore all deposits that are already success + log.Infof("ignored succeess deposit: %s", deposit.TransactionID) continue case types.DepositCredited, types.DepositPending: + log.Infof("adding pending deposit: %s", deposit.TransactionID) s.watchingDeposits[deposit.TransactionID] = deposit } } @@ -167,8 +185,11 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio var succeededDeposits []types.Deposit for _, deposit := range deposits { if deposit.Status == types.DepositSuccess { + log.Infof("found pending -> success deposit: %+v", deposit) + current, required := deposit.GetCurrentConfirmation() if required > 0 && deposit.UnlockConfirm > 0 && current < deposit.UnlockConfirm { + log.Infof("deposit %s unlock confirm %d is not reached, current: %d, required: %d, skip this round", deposit.TransactionID, deposit.UnlockConfirm, current, required) continue } From 29727c12beab8bef68b82e11923094d08c8f9cd1 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 12:14:14 +0800 Subject: [PATCH 1328/1392] add deposit2transfer config --- config/deposit2transfer.yaml | 12 ++++++++++++ pkg/strategy/deposit2transfer/strategy.go | 5 ----- 2 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 config/deposit2transfer.yaml diff --git a/config/deposit2transfer.yaml b/config/deposit2transfer.yaml new file mode 100644 index 0000000000..2f4cef67de --- /dev/null +++ b/config/deposit2transfer.yaml @@ -0,0 +1,12 @@ +--- +## deposit2transfer scans the deposit history and then transfer the deposited assets into your margin account +## currently only cross-margin is supported. +exchangeStrategies: +- on: binance + deposit2transfer: + ## interval is the deposit history scanning interval + interval: 1m + + ## assets are the assets you want to transfer into the margin account + assets: + - BTC diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index 19aff8a17b..dd47ab5f69 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -11,7 +11,6 @@ import ( "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/exchange/binance" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -24,8 +23,6 @@ const ID = "deposit2transfer" var log = logrus.WithField("strategy", ID) -var errNotBinanceExchange = errors.New("not binance exchange, currently only support binance exchange") - var errMarginTransferNotSupport = errors.New("exchange session does not support margin transfer") var errDepositHistoryNotSupport = errors.New("exchange session does not support deposit history query") @@ -44,8 +41,6 @@ type Strategy struct { Interval types.Interval `json:"interval"` - binanceSpot *binance.Exchange - marginTransferService marginTransferService depositHistoryService types.ExchangeTransferService From 073c4562fd236b804db9752dd01ffe61fb36ea5e Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 12:23:17 +0800 Subject: [PATCH 1329/1392] deposit2transfer: refactor deposit check and add more logs --- pkg/strategy/deposit2transfer/strategy.go | 58 ++++++++++++++--------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index dd47ab5f69..5f60b1ded6 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -87,6 +87,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return errDepositHistoryNotSupport } + go s.tickWatcher(ctx, s.Interval.Duration()) + return nil } @@ -94,42 +96,52 @@ func (s *Strategy) tickWatcher(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() + s.checkDeposits(ctx) for { select { case <-ctx.Done(): return case <-ticker.C: - for _, asset := range s.Assets { - account := s.session.Account + s.checkDeposits(ctx) + } + } +} + +func (s *Strategy) checkDeposits(ctx context.Context) { + for _, asset := range s.Assets { + log.Infof("checking %s deposits...", asset) - succeededDeposits, err := s.scanDepositHistory(ctx, asset, 4*time.Hour) - if err != nil { - log.WithError(err).Errorf("unable to scan deposit history") - continue - } + account := s.session.Account + succeededDeposits, err := s.scanDepositHistory(ctx, asset, 4*time.Hour) + if err != nil { + log.WithError(err).Errorf("unable to scan deposit history") + return + } - for _, d := range succeededDeposits { - log.Infof("found succeeded deposit: %+v", d) + if len(succeededDeposits) == 0 { + log.Infof("no %s deposit found", asset) + } - bal, ok := account.Balance(d.Asset) - if !ok { - log.Errorf("unexpected error: %s balance not found", d.Asset) - continue - } + for _, d := range succeededDeposits { + log.Infof("found succeeded deposit: %+v", d) + + bal, ok := account.Balance(d.Asset) + if !ok { + log.Errorf("unexpected error: %s balance not found", d.Asset) + return + } - log.Infof("%s balance: %+v", d.Asset, bal) + log.Infof("%s balance: %+v", d.Asset, bal) - amount := fixedpoint.Min(bal.Available, d.Amount) + amount := fixedpoint.Min(bal.Available, d.Amount) - bbgo.Notify("Found succeeded deposit %s %s, transferring %s %s into the margin account", - d.Amount.String(), d.Asset, - amount.String(), d.Asset) + bbgo.Notify("Found succeeded deposit %s %s, transferring %s %s into the margin account", + d.Amount.String(), d.Asset, + amount.String(), d.Asset) - if err2 := s.marginTransferService.TransferMarginAccountAsset(ctx, d.Asset, amount, types.TransferIn); err2 != nil { - log.WithError(err2).Errorf("unable to transfer deposit asset into the margin account") - } - } + if err2 := s.marginTransferService.TransferMarginAccountAsset(ctx, d.Asset, amount, types.TransferIn); err2 != nil { + log.WithError(err2).Errorf("unable to transfer deposit asset into the margin account") } } } From 4a28843a0a0cfbb6ff6292b9f80f3b1155d7d7f5 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 12:38:23 +0800 Subject: [PATCH 1330/1392] deposit2transfer: fix mutex lock --- config/deposit2transfer.yaml | 2 +- pkg/strategy/deposit2transfer/strategy.go | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/config/deposit2transfer.yaml b/config/deposit2transfer.yaml index 2f4cef67de..94cc742d20 100644 --- a/config/deposit2transfer.yaml +++ b/config/deposit2transfer.yaml @@ -9,4 +9,4 @@ exchangeStrategies: ## assets are the assets you want to transfer into the margin account assets: - - BTC + - USDT diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index 5f60b1ded6..b5bf94938f 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -39,7 +39,7 @@ type Strategy struct { Assets []string `json:"assets"` - Interval types.Interval `json:"interval"` + Interval types.Duration `json:"interval"` marginTransferService marginTransferService depositHistoryService types.ExchangeTransferService @@ -56,8 +56,8 @@ func (s *Strategy) ID() string { func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {} func (s *Strategy) Defaults() error { - if s.Interval == "" { - s.Interval = types.Interval1m + if s.Interval == 0 { + s.Interval = types.Duration(5 * time.Minute) } return nil @@ -87,7 +87,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return errDepositHistoryNotSupport } - go s.tickWatcher(ctx, s.Interval.Duration()) + session.UserDataStream.OnStart(func() { + go s.tickWatcher(ctx, s.Interval.Duration()) + }) return nil } @@ -97,6 +99,7 @@ func (s *Strategy) tickWatcher(ctx context.Context, interval time.Duration) { defer ticker.Stop() s.checkDeposits(ctx) + for { select { case <-ctx.Done(): @@ -121,6 +124,7 @@ func (s *Strategy) checkDeposits(ctx context.Context) { if len(succeededDeposits) == 0 { log.Infof("no %s deposit found", asset) + continue } for _, d := range succeededDeposits { @@ -163,10 +167,10 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio }) s.mu.Lock() - defer s.mu.Lock() + defer s.mu.Unlock() for _, deposit := range deposits { - log.Infof("scanning deposit: %+v", deposit) + log.Infof("checking deposit: %+v", deposit) if deposit.Asset != asset { continue @@ -177,9 +181,10 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio s.watchingDeposits[deposit.TransactionID] = deposit } else { switch deposit.Status { + case types.DepositSuccess: // ignore all deposits that are already success - log.Infof("ignored succeess deposit: %s", deposit.TransactionID) + log.Infof("ignored succeess deposit: %s %+v", deposit.TransactionID, deposit) continue case types.DepositCredited, types.DepositPending: From ece8cacd9ea0a2fcada8fdd3d8ed15c8bbbba905 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 12:38:59 +0800 Subject: [PATCH 1331/1392] deposit2transfer: use watchingDeposits instead of just deposits --- pkg/strategy/deposit2transfer/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index b5bf94938f..6bfec0a654 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -195,7 +195,7 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio } var succeededDeposits []types.Deposit - for _, deposit := range deposits { + for _, deposit := range s.watchingDeposits { if deposit.Status == types.DepositSuccess { log.Infof("found pending -> success deposit: %+v", deposit) From 5460ebdbf4fbcf7a9592b38c07ea527a0a26be0c Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 12:49:05 +0800 Subject: [PATCH 1332/1392] max: add margin transfer request --- .../max/maxapi/v3/margin_transfer_request.go | 41 +++++ .../v3/margin_transfer_request_requestgen.go | 173 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 pkg/exchange/max/maxapi/v3/margin_transfer_request.go create mode 100644 pkg/exchange/max/maxapi/v3/margin_transfer_request_requestgen.go diff --git a/pkg/exchange/max/maxapi/v3/margin_transfer_request.go b/pkg/exchange/max/maxapi/v3/margin_transfer_request.go new file mode 100644 index 0000000000..0042223412 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/margin_transfer_request.go @@ -0,0 +1,41 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func (s *Client) NewMarginTransferRequest() *MarginTransferRequest { + return &MarginTransferRequest{client: s.Client} +} + +type MarginTransferSide string + +const ( + MarginTransferSideIn MarginTransferSide = "in" + MarginTransferSideOut MarginTransferSide = "out" +) + +type MarginTransferResponse struct { + Sn string `json:"sn"` + Side MarginTransferSide `json:"side"` + Currency string `json:"currency"` + Amount fixedpoint.Value `json:"amount"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + State string `json:"state"` +} + +//go:generate PostRequest -url "/api/v3/wallet/m/transfer" -type MarginTransferRequest -responseType .MarginTransferResponse +type MarginTransferRequest struct { + client requestgen.AuthenticatedAPIClient + + currency string `param:"currency,required"` + amount string `param:"amount"` + side string `param:"side"` +} diff --git a/pkg/exchange/max/maxapi/v3/margin_transfer_request_requestgen.go b/pkg/exchange/max/maxapi/v3/margin_transfer_request_requestgen.go new file mode 100644 index 0000000000..382e321537 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/margin_transfer_request_requestgen.go @@ -0,0 +1,173 @@ +// Code generated by "requestgen -method POST -url /api/v3/wallet/m/transfer -type MarginTransferRequest -responseType .MarginTransferResponse"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (m *MarginTransferRequest) Currency(currency string) *MarginTransferRequest { + m.currency = currency + return m +} + +func (m *MarginTransferRequest) Amount(amount string) *MarginTransferRequest { + m.amount = amount + return m +} + +func (m *MarginTransferRequest) Side(side string) *MarginTransferRequest { + m.side = side + return m +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (m *MarginTransferRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (m *MarginTransferRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check currency field -> json key currency + currency := m.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency + // check amount field -> json key amount + amount := m.amount + + // assign parameter of amount + params["amount"] = amount + // check side field -> json key side + side := m.side + + // assign parameter of side + params["side"] = side + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (m *MarginTransferRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := m.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if m.isVarSlice(_v) { + m.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (m *MarginTransferRequest) GetParametersJSON() ([]byte, error) { + params, err := m.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (m *MarginTransferRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (m *MarginTransferRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (m *MarginTransferRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (m *MarginTransferRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (m *MarginTransferRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := m.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (m *MarginTransferRequest) Do(ctx context.Context) (*MarginTransferResponse, error) { + + params, err := m.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/v3/wallet/m/transfer" + + req, err := m.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := m.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse MarginTransferResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} From 25298720d02a4b9d2ce6fe0f1d7c338f6a44fdfa Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 13:16:11 +0800 Subject: [PATCH 1333/1392] max: implement TransferMarginAccountAsset on max --- pkg/exchange/max/exchange.go | 10 +++++ pkg/exchange/max/margin.go | 43 +++++++++++++++++++ .../max/maxapi/v3/margin_transfer_request.go | 8 ++-- .../v3/margin_transfer_request_requestgen.go | 13 +++++- 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 pkg/exchange/max/margin.go diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 33671bf7ea..1912a3ffc5 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -1051,3 +1051,13 @@ func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { _, ok := SupportedIntervals[interval] return ok } + +func logResponse(resp interface{}, err error, req interface{}) error { + if err != nil { + log.WithError(err).Errorf("%T: error %+v", req, resp) + return err + } + + log.Infof("%T: response: %+v", req, resp) + return nil +} diff --git a/pkg/exchange/max/margin.go b/pkg/exchange/max/margin.go new file mode 100644 index 0000000000..d40c16189d --- /dev/null +++ b/pkg/exchange/max/margin.go @@ -0,0 +1,43 @@ +package max + +import ( + "context" + "errors" + "fmt" + + v3 "github.com/c9s/bbgo/pkg/exchange/max/maxapi/v3" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// TransferMarginAccountAsset transfers the asset into/out from the margin account +// +// types.TransferIn => Spot to Margin +// types.TransferOut => Margin to Spot +// +// to call this method, you must set the IsMargin = true +func (e *Exchange) TransferMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { + if e.IsIsolatedMargin { + return errors.New("isolated margin is not supported") + } + + return e.transferCrossMarginAccountAsset(ctx, asset, amount, io) +} + +// transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account +func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { + req := e.v3margin.NewMarginTransferRequest() + req.Currency(toLocalCurrency(asset)) + req.Amount(amount.String()) + + if io == types.TransferIn { + req.Side(v3.MarginTransferSideIn) + } else if io == types.TransferOut { + req.Side(v3.MarginTransferSideOut) + } else { + return fmt.Errorf("unexpected transfer direction: %d given", io) + } + + resp, err := req.Do(ctx) + return logResponse(resp, err, req) +} diff --git a/pkg/exchange/max/maxapi/v3/margin_transfer_request.go b/pkg/exchange/max/maxapi/v3/margin_transfer_request.go index 0042223412..e9f8be094d 100644 --- a/pkg/exchange/max/maxapi/v3/margin_transfer_request.go +++ b/pkg/exchange/max/maxapi/v3/margin_transfer_request.go @@ -11,7 +11,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -func (s *Client) NewMarginTransferRequest() *MarginTransferRequest { +func (s *MarginService) NewMarginTransferRequest() *MarginTransferRequest { return &MarginTransferRequest{client: s.Client} } @@ -35,7 +35,7 @@ type MarginTransferResponse struct { type MarginTransferRequest struct { client requestgen.AuthenticatedAPIClient - currency string `param:"currency,required"` - amount string `param:"amount"` - side string `param:"side"` + currency string `param:"currency,required"` + amount string `param:"amount"` + side MarginTransferSide `param:"side"` } diff --git a/pkg/exchange/max/maxapi/v3/margin_transfer_request_requestgen.go b/pkg/exchange/max/maxapi/v3/margin_transfer_request_requestgen.go index 382e321537..da668af007 100644 --- a/pkg/exchange/max/maxapi/v3/margin_transfer_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/margin_transfer_request_requestgen.go @@ -21,7 +21,7 @@ func (m *MarginTransferRequest) Amount(amount string) *MarginTransferRequest { return m } -func (m *MarginTransferRequest) Side(side string) *MarginTransferRequest { +func (m *MarginTransferRequest) Side(side MarginTransferSide) *MarginTransferRequest { m.side = side return m } @@ -60,6 +60,17 @@ func (m *MarginTransferRequest) GetParameters() (map[string]interface{}, error) // check side field -> json key side side := m.side + // TEMPLATE check-valid-values + switch side { + case MarginTransferSideIn, MarginTransferSideOut: + params["side"] = side + + default: + return nil, fmt.Errorf("side value %v is invalid", side) + + } + // END TEMPLATE check-valid-values + // assign parameter of side params["side"] = side From b27395f6f4adde9e96d6e74e6ddd3f15b03c9ff9 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 8 Aug 2023 14:11:19 +0800 Subject: [PATCH 1334/1392] pkg/exchange: avoiding GC panic caused by a rapid creation/removal slice of pointers --- pkg/exchange/bybit/stream.go | 16 ++++++++-------- pkg/exchange/bybit/stream_callbacks.go | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index e388722798..9af2bff141 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -33,8 +33,8 @@ type Stream struct { types.StandardStream bookEventCallbacks []func(e BookEvent) - walletEventCallbacks []func(e []*WalletEvent) - orderEventCallbacks []func(e []*OrderEvent) + walletEventCallbacks []func(e []WalletEvent) + orderEventCallbacks []func(e []OrderEvent) } func NewStream(key, secret string) *Stream { @@ -77,10 +77,10 @@ func (s *Stream) dispatchEvent(event interface{}) { case *BookEvent: s.EmitBookEvent(*e) - case []*WalletEvent: + case []WalletEvent: s.EmitWalletEvent(e) - case []*OrderEvent: + case []OrderEvent: s.EmitOrderEvent(e) } } @@ -110,11 +110,11 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { return &book, nil case TopicTypeWallet: - var wallets []*WalletEvent + var wallets []WalletEvent return wallets, json.Unmarshal(e.WebSocketTopicEvent.Data, &wallets) case TopicTypeOrder: - var orders []*OrderEvent + var orders []OrderEvent return orders, json.Unmarshal(e.WebSocketTopicEvent.Data, &orders) } @@ -239,7 +239,7 @@ func (s *Stream) handleBookEvent(e BookEvent) { } } -func (s *Stream) handleWalletEvent(events []*WalletEvent) { +func (s *Stream) handleWalletEvent(events []WalletEvent) { bm := types.BalanceMap{} for _, event := range events { if event.AccountType != AccountTypeSpot { @@ -258,7 +258,7 @@ func (s *Stream) handleWalletEvent(events []*WalletEvent) { s.StandardStream.EmitBalanceSnapshot(bm) } -func (s *Stream) handleOrderEvent(events []*OrderEvent) { +func (s *Stream) handleOrderEvent(events []OrderEvent) { for _, event := range events { if event.Category != bybitapi.CategorySpot { return diff --git a/pkg/exchange/bybit/stream_callbacks.go b/pkg/exchange/bybit/stream_callbacks.go index 79b1bd7be0..33e7516ff6 100644 --- a/pkg/exchange/bybit/stream_callbacks.go +++ b/pkg/exchange/bybit/stream_callbacks.go @@ -14,21 +14,21 @@ func (s *Stream) EmitBookEvent(e BookEvent) { } } -func (s *Stream) OnWalletEvent(cb func(e []*WalletEvent)) { +func (s *Stream) OnWalletEvent(cb func(e []WalletEvent)) { s.walletEventCallbacks = append(s.walletEventCallbacks, cb) } -func (s *Stream) EmitWalletEvent(e []*WalletEvent) { +func (s *Stream) EmitWalletEvent(e []WalletEvent) { for _, cb := range s.walletEventCallbacks { cb(e) } } -func (s *Stream) OnOrderEvent(cb func(e []*OrderEvent)) { +func (s *Stream) OnOrderEvent(cb func(e []OrderEvent)) { s.orderEventCallbacks = append(s.orderEventCallbacks, cb) } -func (s *Stream) EmitOrderEvent(e []*OrderEvent) { +func (s *Stream) EmitOrderEvent(e []OrderEvent) { for _, cb := range s.orderEventCallbacks { cb(e) } From 4ed402b775f3aaa4c3a5e4cb313e8264407da199 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Aug 2023 20:51:09 +0800 Subject: [PATCH 1335/1392] max: update deposit states and add more fields to deposit --- pkg/exchange/max/convert.go | 18 +++++++++--------- pkg/exchange/max/exchange.go | 5 +++-- pkg/exchange/max/maxapi/account.go | 29 +++++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index b6ea59ba2e..0fb6c7462f 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -262,24 +262,24 @@ func toGlobalTradeV2(t max.Trade) (*types.Trade, error) { }, nil } -func toGlobalDepositStatus(a string) types.DepositStatus { +func toGlobalDepositStatus(a max.DepositState) types.DepositStatus { switch a { - case "submitting", "submitted", "checking": - return types.DepositPending - case "accepted": - return types.DepositSuccess + case max.DepositStateSubmitting, max.DepositStateSubmitted, max.DepositStatePending, max.DepositStateChecking: + return types.DepositPending - case "rejected": + case max.DepositStateRejected: return types.DepositRejected - case "canceled": + case max.DepositStateCancelled: return types.DepositCancelled - case "suspect", "refunded": - + case max.DepositStateAccepted: + return types.DepositSuccess } + // other states goes to this + // max.DepositStateSuspect, max.DepositStateSuspended return types.DepositStatus(a) } diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 1912a3ffc5..d49eb19923 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -781,10 +781,11 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, Time: types.Time(d.CreatedAt), Amount: d.Amount, Asset: toGlobalCurrency(d.Currency), - Address: "", // not supported - AddressTag: "", // not supported + Address: d.Address, // not supported + AddressTag: "", // not supported TransactionID: d.TxID, Status: toGlobalDepositStatus(d.State), + Confirmation: "", }) } diff --git a/pkg/exchange/max/maxapi/account.go b/pkg/exchange/max/maxapi/account.go index 91d98ebacf..17d4e29d47 100644 --- a/pkg/exchange/max/maxapi/account.go +++ b/pkg/exchange/max/maxapi/account.go @@ -106,14 +106,31 @@ func (c *RestClient) NewGetAccountsRequest() *GetAccountsRequest { return &GetAccountsRequest{client: c} } +type DepositState string + +const ( + DepositStateSubmitting DepositState = "submitting" + DepositStateCancelled DepositState = "cancelled" + DepositStateSubmitted DepositState = "submitted" + DepositStatePending DepositState = "pending" + DepositStateSuspect DepositState = "suspect" + DepositStateRejected DepositState = "rejected" + DepositStateSuspended DepositState = "suspended" + DepositStateAccepted DepositState = "accepted" + DepositStateChecking DepositState = "checking" +) + type Deposit struct { - Currency string `json:"currency"` + Currency string `json:"currency"` // "eth" CurrencyVersion string `json:"currency_version"` // "eth" + NetworkProtocol string `json:"network_protocol"` // "ethereum-erc20" Amount fixedpoint.Value `json:"amount"` Fee fixedpoint.Value `json:"fee"` TxID string `json:"txid"` - State string `json:"state"` + State DepositState `json:"state"` + Status string `json:"status"` Confirmations int64 `json:"confirmations"` + Address string `json:"to_address"` // 0x5c7d23d516f120d322fc7b116386b7e491739138 CreatedAt types.MillisecondTimestamp `json:"created_at"` UpdatedAt types.MillisecondTimestamp `json:"updated_at"` } @@ -135,6 +152,14 @@ func (c *RestClient) NewGetDepositHistoryRequest() *GetDepositHistoryRequest { } } +// submitted -> accepted -> processing -> sent -> confirmed +type WithdrawState string + +const ( + WithdrawStateSubmitting WithdrawState = "submitting" + WithdrawStateConfirmed WithdrawState = "confirmed" +) + type Withdraw struct { UUID string `json:"uuid"` Currency string `json:"currency"` From 8c22863334fb128d96de498874c5d8998ddd6476 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 9 Aug 2023 12:39:19 +0800 Subject: [PATCH 1336/1392] pkg/exchange: mv BalanceEvent to bybitapi and rename to WalletBalances --- .../bybitapi/get_wallet_balances_request.go | 62 +++++++++++++++++++ pkg/exchange/bybit/bybitapi/types.go | 4 ++ pkg/exchange/bybit/convert.go | 18 ++++++ pkg/exchange/bybit/stream.go | 25 ++------ pkg/exchange/bybit/stream_callbacks.go | 8 ++- pkg/exchange/bybit/types.go | 61 ------------------ 6 files changed, 94 insertions(+), 84 deletions(-) create mode 100644 pkg/exchange/bybit/bybitapi/get_wallet_balances_request.go diff --git a/pkg/exchange/bybit/bybitapi/get_wallet_balances_request.go b/pkg/exchange/bybit/bybitapi/get_wallet_balances_request.go new file mode 100644 index 0000000000..c883c50b4f --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_wallet_balances_request.go @@ -0,0 +1,62 @@ +package bybitapi + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type WalletBalances struct { + AccountType AccountType `json:"accountType"` + AccountIMRate fixedpoint.Value `json:"accountIMRate"` + AccountMMRate fixedpoint.Value `json:"accountMMRate"` + TotalEquity fixedpoint.Value `json:"totalEquity"` + TotalWalletBalance fixedpoint.Value `json:"totalWalletBalance"` + TotalMarginBalance fixedpoint.Value `json:"totalMarginBalance"` + TotalAvailableBalance fixedpoint.Value `json:"totalAvailableBalance"` + TotalPerpUPL fixedpoint.Value `json:"totalPerpUPL"` + TotalInitialMargin fixedpoint.Value `json:"totalInitialMargin"` + TotalMaintenanceMargin fixedpoint.Value `json:"totalMaintenanceMargin"` + // Account LTV: account total borrowed size / (account total equity + account total borrowed size). + // In non-unified mode & unified (inverse) & unified (isolated_margin), the field will be returned as an empty string. + AccountLTV fixedpoint.Value `json:"accountLTV"` + Coins []struct { + Coin string `json:"coin"` + // Equity of current coin + Equity fixedpoint.Value `json:"equity"` + // UsdValue of current coin. If this coin cannot be collateral, then it is 0 + UsdValue fixedpoint.Value `json:"usdValue"` + // WalletBalance of current coin + WalletBalance fixedpoint.Value `json:"walletBalance"` + // Free available balance for Spot wallet. This is a unique field for Normal SPOT + Free fixedpoint.Value + // Locked balance for Spot wallet. This is a unique field for Normal SPOT + Locked fixedpoint.Value + // Available amount to withdraw of current coin + AvailableToWithdraw fixedpoint.Value `json:"availableToWithdraw"` + // Available amount to borrow of current coin + AvailableToBorrow fixedpoint.Value `json:"availableToBorrow"` + // Borrow amount of current coin + BorrowAmount fixedpoint.Value `json:"borrowAmount"` + // Accrued interest + AccruedInterest fixedpoint.Value `json:"accruedInterest"` + // Pre-occupied margin for order. For portfolio margin mode, it returns "" + TotalOrderIM fixedpoint.Value `json:"totalOrderIM"` + // Sum of initial margin of all positions + Pre-occupied liquidation fee. For portfolio margin mode, it returns "" + TotalPositionIM fixedpoint.Value `json:"totalPositionIM"` + // Sum of maintenance margin for all positions. For portfolio margin mode, it returns "" + TotalPositionMM fixedpoint.Value `json:"totalPositionMM"` + // Unrealised P&L + UnrealisedPnl fixedpoint.Value `json:"unrealisedPnl"` + // Cumulative Realised P&L + CumRealisedPnl fixedpoint.Value `json:"cumRealisedPnl"` + // Bonus. This is a unique field for UNIFIED account + Bonus fixedpoint.Value `json:"bonus"` + // Whether it can be used as a margin collateral currency (platform) + // - When marginCollateral=false, then collateralSwitch is meaningless + // - This is a unique field for UNIFIED account + CollateralSwitch bool `json:"collateralSwitch"` + // Whether the collateral is turned on by user (user) + // - When marginCollateral=true, then collateralSwitch is meaningful + // - This is a unique field for UNIFIED account + MarginCollateral bool `json:"marginCollateral"` + } `json:"coin"` +} diff --git a/pkg/exchange/bybit/bybitapi/types.go b/pkg/exchange/bybit/bybitapi/types.go index 7685d54253..d54e54d63d 100644 --- a/pkg/exchange/bybit/bybitapi/types.go +++ b/pkg/exchange/bybit/bybitapi/types.go @@ -87,3 +87,7 @@ const ( TimeInForceIOC TimeInForce = "IOC" TimeInForceFOK TimeInForce = "FOK" ) + +type AccountType string + +const AccountTypeSpot AccountType = "SPOT" diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index c78f6dc1ba..9f665ecec0 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -265,3 +265,21 @@ func v3ToGlobalTrade(trade v3.Trade) (*types.Trade, error) { IsIsolated: false, }, nil } + +func toGlobalBalanceMap(events []bybitapi.WalletBalances) types.BalanceMap { + bm := types.BalanceMap{} + for _, event := range events { + if event.AccountType != bybitapi.AccountTypeSpot { + continue + } + + for _, obj := range event.Coins { + bm[obj.Coin] = types.Balance{ + Currency: obj.Coin, + Available: obj.Free, + Locked: obj.Locked, + } + } + } + return bm +} diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 9af2bff141..dcaa158fe2 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -33,7 +33,7 @@ type Stream struct { types.StandardStream bookEventCallbacks []func(e BookEvent) - walletEventCallbacks []func(e []WalletEvent) + walletEventCallbacks []func(e []bybitapi.WalletBalances) orderEventCallbacks []func(e []OrderEvent) } @@ -77,7 +77,7 @@ func (s *Stream) dispatchEvent(event interface{}) { case *BookEvent: s.EmitBookEvent(*e) - case []WalletEvent: + case []bybitapi.WalletBalances: s.EmitWalletEvent(e) case []OrderEvent: @@ -110,7 +110,7 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { return &book, nil case TopicTypeWallet: - var wallets []WalletEvent + var wallets []bybitapi.WalletBalances return wallets, json.Unmarshal(e.WebSocketTopicEvent.Data, &wallets) case TopicTypeOrder: @@ -239,23 +239,8 @@ func (s *Stream) handleBookEvent(e BookEvent) { } } -func (s *Stream) handleWalletEvent(events []WalletEvent) { - bm := types.BalanceMap{} - for _, event := range events { - if event.AccountType != AccountTypeSpot { - return - } - - for _, obj := range event.Coins { - bm[obj.Coin] = types.Balance{ - Currency: obj.Coin, - Available: obj.Free, - Locked: obj.Locked, - } - } - } - - s.StandardStream.EmitBalanceSnapshot(bm) +func (s *Stream) handleWalletEvent(events []bybitapi.WalletBalances) { + s.StandardStream.EmitBalanceSnapshot(toGlobalBalanceMap(events)) } func (s *Stream) handleOrderEvent(events []OrderEvent) { diff --git a/pkg/exchange/bybit/stream_callbacks.go b/pkg/exchange/bybit/stream_callbacks.go index 33e7516ff6..2669dcfa7c 100644 --- a/pkg/exchange/bybit/stream_callbacks.go +++ b/pkg/exchange/bybit/stream_callbacks.go @@ -2,7 +2,9 @@ package bybit -import () +import ( + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" +) func (s *Stream) OnBookEvent(cb func(e BookEvent)) { s.bookEventCallbacks = append(s.bookEventCallbacks, cb) @@ -14,11 +16,11 @@ func (s *Stream) EmitBookEvent(e BookEvent) { } } -func (s *Stream) OnWalletEvent(cb func(e []WalletEvent)) { +func (s *Stream) OnWalletEvent(cb func(e []bybitapi.WalletBalances)) { s.walletEventCallbacks = append(s.walletEventCallbacks, cb) } -func (s *Stream) EmitWalletEvent(e []WalletEvent) { +func (s *Stream) EmitWalletEvent(e []bybitapi.WalletBalances) { for _, cb := range s.walletEventCallbacks { cb(e) } diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index 096e975dda..8046630b83 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -143,67 +143,6 @@ func getTopicType(topic string) TopicType { return TopicType(slice[0]) } -type AccountType string - -const AccountTypeSpot AccountType = "SPOT" - -type WalletEvent struct { - AccountType AccountType `json:"accountType"` - AccountIMRate fixedpoint.Value `json:"accountIMRate"` - AccountMMRate fixedpoint.Value `json:"accountMMRate"` - TotalEquity fixedpoint.Value `json:"totalEquity"` - TotalWalletBalance fixedpoint.Value `json:"totalWalletBalance"` - TotalMarginBalance fixedpoint.Value `json:"totalMarginBalance"` - TotalAvailableBalance fixedpoint.Value `json:"totalAvailableBalance"` - TotalPerpUPL fixedpoint.Value `json:"totalPerpUPL"` - TotalInitialMargin fixedpoint.Value `json:"totalInitialMargin"` - TotalMaintenanceMargin fixedpoint.Value `json:"totalMaintenanceMargin"` - // Account LTV: account total borrowed size / (account total equity + account total borrowed size). - // In non-unified mode & unified (inverse) & unified (isolated_margin), the field will be returned as an empty string. - AccountLTV fixedpoint.Value `json:"accountLTV"` - Coins []struct { - Coin string `json:"coin"` - // Equity of current coin - Equity fixedpoint.Value `json:"equity"` - // UsdValue of current coin. If this coin cannot be collateral, then it is 0 - UsdValue fixedpoint.Value `json:"usdValue"` - // WalletBalance of current coin - WalletBalance fixedpoint.Value `json:"walletBalance"` - // Free available balance for Spot wallet. This is a unique field for Normal SPOT - Free fixedpoint.Value - // Locked balance for Spot wallet. This is a unique field for Normal SPOT - Locked fixedpoint.Value - // Available amount to withdraw of current coin - AvailableToWithdraw fixedpoint.Value `json:"availableToWithdraw"` - // Available amount to borrow of current coin - AvailableToBorrow fixedpoint.Value `json:"availableToBorrow"` - // Borrow amount of current coin - BorrowAmount fixedpoint.Value `json:"borrowAmount"` - // Accrued interest - AccruedInterest fixedpoint.Value `json:"accruedInterest"` - // Pre-occupied margin for order. For portfolio margin mode, it returns "" - TotalOrderIM fixedpoint.Value `json:"totalOrderIM"` - // Sum of initial margin of all positions + Pre-occupied liquidation fee. For portfolio margin mode, it returns "" - TotalPositionIM fixedpoint.Value `json:"totalPositionIM"` - // Sum of maintenance margin for all positions. For portfolio margin mode, it returns "" - TotalPositionMM fixedpoint.Value `json:"totalPositionMM"` - // Unrealised P&L - UnrealisedPnl fixedpoint.Value `json:"unrealisedPnl"` - // Cumulative Realised P&L - CumRealisedPnl fixedpoint.Value `json:"cumRealisedPnl"` - // Bonus. This is a unique field for UNIFIED account - Bonus fixedpoint.Value `json:"bonus"` - // Whether it can be used as a margin collateral currency (platform) - // - When marginCollateral=false, then collateralSwitch is meaningless - // - This is a unique field for UNIFIED account - CollateralSwitch bool `json:"collateralSwitch"` - // Whether the collateral is turned on by user (user) - // - When marginCollateral=true, then collateralSwitch is meaningful - // - This is a unique field for UNIFIED account - MarginCollateral bool `json:"marginCollateral"` - } `json:"coin"` -} - type OrderEvent struct { bybitapi.Order From dfead5ebed93ca995b1a9abf733c2bec9c6e48c2 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 9 Aug 2023 12:50:30 +0800 Subject: [PATCH 1337/1392] pkg/exchange: add query account balance api --- pkg/exchange/bybit/bybitapi/client_test.go | 7 + .../bybitapi/get_wallet_balances_request.go | 30 +++ .../get_wallet_balances_request_requestgen.go | 176 ++++++++++++++++++ pkg/exchange/bybit/exchange.go | 19 +- 4 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 pkg/exchange/bybit/bybitapi/get_wallet_balances_request_requestgen.go diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index 0daf9eb2be..cf6a61215e 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -155,4 +155,11 @@ func TestClient(t *testing.T) { assert.NoError(t, err) t.Logf("apiResp: %#v", orderResp) }) + + t.Run("GetWalletBalancesRequest", func(t *testing.T) { + req := client.NewGetWalletBalancesRequest().Coin("BTC") + apiResp, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %+v", apiResp) + }) } diff --git a/pkg/exchange/bybit/bybitapi/get_wallet_balances_request.go b/pkg/exchange/bybit/bybitapi/get_wallet_balances_request.go index c883c50b4f..d2f07fe9c2 100644 --- a/pkg/exchange/bybit/bybitapi/get_wallet_balances_request.go +++ b/pkg/exchange/bybit/bybitapi/get_wallet_balances_request.go @@ -1,9 +1,18 @@ package bybitapi import ( + "github.com/c9s/requestgen" + "github.com/c9s/bbgo/pkg/fixedpoint" ) +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +type WalletBalancesResponse struct { + List []WalletBalances `json:"list"` +} + type WalletBalances struct { AccountType AccountType `json:"accountType"` AccountIMRate fixedpoint.Value `json:"accountIMRate"` @@ -60,3 +69,24 @@ type WalletBalances struct { MarginCollateral bool `json:"marginCollateral"` } `json:"coin"` } + +//go:generate GetRequest -url "/v5/account/wallet-balance" -type GetWalletBalancesRequest -responseDataType .WalletBalancesResponse +type GetWalletBalancesRequest struct { + client requestgen.AuthenticatedAPIClient + + // Account type + // - Unified account: UNIFIED (trade spot/linear/options), CONTRACT(trade inverse) + // - Normal account: CONTRACT, SPOT + accountType AccountType `param:"accountType,query" validValues:"SPOT"` + // Coin name + // - If not passed, it returns non-zero asset info + // - You can pass multiple coins to query, separated by comma. USDT,USDC + coin *string `param:"coin,query"` +} + +func (c *RestClient) NewGetWalletBalancesRequest() *GetWalletBalancesRequest { + return &GetWalletBalancesRequest{ + client: c, + accountType: AccountTypeSpot, + } +} diff --git a/pkg/exchange/bybit/bybitapi/get_wallet_balances_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_wallet_balances_request_requestgen.go new file mode 100644 index 0000000000..14ae4029cb --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_wallet_balances_request_requestgen.go @@ -0,0 +1,176 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/account/wallet-balance -type GetWalletBalancesRequest -responseDataType .WalletBalancesResponse"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetWalletBalancesRequest) AccountType(accountType AccountType) *GetWalletBalancesRequest { + g.accountType = accountType + return g +} + +func (g *GetWalletBalancesRequest) Coin(coin string) *GetWalletBalancesRequest { + g.coin = &coin + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetWalletBalancesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check accountType field -> json key accountType + accountType := g.accountType + + // TEMPLATE check-valid-values + switch accountType { + case "SPOT": + params["accountType"] = accountType + + default: + return nil, fmt.Errorf("accountType value %v is invalid", accountType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of accountType + params["accountType"] = accountType + // check coin field -> json key coin + if g.coin != nil { + coin := *g.coin + + // assign parameter of coin + params["coin"] = coin + } else { + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetWalletBalancesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetWalletBalancesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetWalletBalancesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetWalletBalancesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetWalletBalancesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetWalletBalancesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetWalletBalancesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetWalletBalancesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetWalletBalancesRequest) Do(ctx context.Context) (*WalletBalancesResponse, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v5/account/wallet-balance" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data WalletBalancesResponse + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 76c0c2edfd..c622179a06 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -25,10 +25,10 @@ const ( // https://bybit-exchange.github.io/docs/zh-TW/v5/rate-limit // sharedRateLimiter indicates that the API belongs to the public API. // -// The default order limiter apply 2 requests per second and a 2 initial bucket -// this includes QueryMarkets, QueryTicker +// The default order limiter apply 3 requests per second and a 2 initial bucket +// this includes QueryMarkets, QueryTicker, QueryAccountBalances var ( - sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2) + sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/3), 2) tradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5) orderRateLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) closedOrderQueryLimiter = rate.NewLimiter(rate.Every(time.Second), 1) @@ -389,6 +389,19 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type return trades, nil } +func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { + if err := sharedRateLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("query account balances rate limiter wait error: %w", err) + } + + req := e.client.NewGetWalletBalancesRequest() + accounts, err := req.Do(ctx) + if err != nil { + return nil, err + } + + return toGlobalBalanceMap(accounts.List), nil +} func (e *Exchange) NewStream() types.Stream { return NewStream(e.key, e.secret) } From 65b06ff4012f41c2129b26e42f8bf83b59683493 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 9 Aug 2023 14:05:26 +0800 Subject: [PATCH 1338/1392] pkg/exchange: add query account function --- pkg/exchange/bybit/exchange.go | 20 ++++++++++++++++++++ pkg/exchange/okex/exchange.go | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index c622179a06..6998f5407f 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -12,6 +12,7 @@ import ( "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" v3 "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi/v3" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -36,6 +37,8 @@ var ( log = logrus.WithFields(logrus.Fields{ "exchange": "bybit", }) + + _ types.ExchangeAccountService = &Exchange{} ) type Exchange struct { @@ -389,6 +392,23 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type return trades, nil } +func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { + balanceMap, err := e.QueryAccountBalances(ctx) + if err != nil { + return nil, err + } + acct := &types.Account{ + AccountType: types.AccountTypeSpot, + // MakerFeeRate bybit doesn't support global maker fee rate. + MakerFeeRate: fixedpoint.Zero, + // TakerFeeRate bybit doesn't support global taker fee rate. + TakerFeeRate: fixedpoint.Zero, + } + acct.UpdateBalances(balanceMap) + + return acct, nil +} + func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { if err := sharedRateLimiter.Wait(ctx); err != nil { return nil, fmt.Errorf("query account balances rate limiter wait error: %w", err) diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 0ad66181f0..e810f54a76 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -142,7 +142,7 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { } var account = types.Account{ - AccountType: "SPOT", + AccountType: types.AccountTypeSpot, } var balanceMap = toGlobalBalance(accountBalance) From 1c5d2dc7591a82d781d55f032d4eb26d139fbe3e Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Wed, 9 Aug 2023 15:05:26 +0800 Subject: [PATCH 1339/1392] add QueryOrder in okex exchange.go --- go.sum | 1 + pkg/exchange/okex/convert.go | 29 ++++++++------- pkg/exchange/okex/exchange.go | 46 ++++++++++++++++++++++++ pkg/exchange/okex/okexapi/client_test.go | 31 ++++++++++++++++ pkg/exchange/okex/okexapi/trade.go | 2 +- 5 files changed, 95 insertions(+), 14 deletions(-) diff --git a/go.sum b/go.sum index cdc1af5feb..b39b946630 100644 --- a/go.sum +++ b/go.sum @@ -1000,6 +1000,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20190226202314-149afe6ec0b6/go.mod h1:jevfED4GnIEnJrWW55YmY9DMhajHcnkqVnEXmEtMyNI= gonum.org/v1/gonum v0.0.0-20190902003836-43865b531bee/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 968544729b..2977c12c21 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -18,6 +18,7 @@ func toGlobalSymbol(symbol string) string { } // //go:generate sh -c "echo \"package okex\nvar spotSymbolMap = map[string]string{\n\" $(curl -s -L 'https://okex.com/api/v5/public/instruments?instType=SPOT' | jq -r '.data[] | \"\\(.instId | sub(\"-\" ; \"\") | tojson ): \\( .instId | tojson),\n\"') \"\n}\" > symbols.go" +// //go:generate go run gensymbols.go func toLocalSymbol(symbol string) string { if s, ok := spotSymbolMap[symbol]; ok { @@ -223,20 +224,20 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) return orders, nil } -func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) { +func toGlobalOrderStatus(state okexapi.OrderState) types.OrderStatus { switch state { case okexapi.OrderStateCanceled: - return types.OrderStatusCanceled, nil + return types.OrderStatusCanceled case okexapi.OrderStateLive: - return types.OrderStatusNew, nil + return types.OrderStatusNew case okexapi.OrderStatePartiallyFilled: - return types.OrderStatusPartiallyFilled, nil + return types.OrderStatusPartiallyFilled case okexapi.OrderStateFilled: - return types.OrderStatusFilled, nil + return types.OrderStatusFilled } - return "", fmt.Errorf("unknown or unsupported okex order state: %s", state) + return types.OrderStatus(state) } func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { @@ -255,20 +256,22 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType) } -func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { +func toGlobalOrderType(orderType okexapi.OrderType) types.OrderType { switch orderType { case okexapi.OrderTypeMarket: - return types.OrderTypeMarket, nil + return types.OrderTypeMarket case okexapi.OrderTypeLimit: - return types.OrderTypeLimit, nil + return types.OrderTypeLimit case okexapi.OrderTypePostOnly: - return types.OrderTypeLimitMaker, nil - + return types.OrderTypeLimitMaker case okexapi.OrderTypeFOK: + return types.OrderTypeFillOrKill case okexapi.OrderTypeIOC: - + return types.OrderTypeIOC + default: + log.Errorf("unsupported order type: %v", orderType) + return "" } - return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType) } func toLocalInterval(src string) string { diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 0ad66181f0..1214ed5276 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -339,3 +339,49 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type return klines, nil } + +func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { + if len(q.Symbol) == 0 { + return nil, errors.New("okex.QueryOrder: InstrumentID is required parameter") + } + if len(q.OrderID) == 0 && len(q.ClientOrderID) == 0 { + return nil, errors.New("okex.QueryOrder: ordId or clOrdId is required parameter") + } + req := e.client.TradeService.NewGetOrderDetailsRequest() + req.InstrumentID(q.Symbol). + OrderID(q.OrderID). + ClientOrderID(q.ClientOrderID) + orderID, err := strconv.ParseInt(q.OrderID, 10, 64) + if err != nil { + return nil, err + } + + var order *okexapi.OrderDetails + order, err = req.Do(ctx) + + if err != nil { + return nil, err + } + + timeInForce := types.TimeInForceFOK + if order.OrderType == okexapi.OrderTypeIOC { + timeInForce = types.TimeInForceIOC + } + return &types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: order.ClientOrderID, + Symbol: order.InstrumentID, + Side: types.SideType(order.Side), + Type: toGlobalOrderType(order.OrderType), + Quantity: order.Quantity, + Price: order.Price, + TimeInForce: types.TimeInForce(timeInForce), + }, + Exchange: types.ExchangeOKEx, + OrderID: uint64(orderID), + Status: toGlobalOrderStatus(order.State), + ExecutedQuantity: order.FilledQuantity, + CreationTime: types.Time(order.CreationTime), + UpdateTime: types.Time(order.UpdateTime), + }, nil +} diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index a5abe42109..04fc69e1d6 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -74,3 +74,34 @@ func TestClient_PlaceOrderRequest(t *testing.T) { assert.NotEmpty(t, order) t.Logf("order: %+v", order) // Right now account has no money } + +func TestClient_GetPendingOrderRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + srv := &TradeService{client: client} + req := srv.NewGetPendingOrderRequest() + odr_type := []string{string(OrderTypeLimit), string(OrderTypeIOC)} + + pending_order, err := req. + InstrumentID("XTZ-BTC"). + OrderTypes(odr_type). + Do(ctx) + assert.NoError(t, err) + assert.Empty(t, pending_order) + t.Logf("order: %+v", pending_order) +} + +func TestClient_GetOrderDetailsRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + srv := &TradeService{client: client} + req := srv.NewGetOrderDetailsRequest() + + orderDetail, err := req. + InstrumentID("BTC-USDT"). + OrderID("xxx-test-order-id"). + Do(ctx) + assert.Error(t, err) // Right now account has no orders + assert.Empty(t, orderDetail) + t.Logf("err: %+v", err) +} diff --git a/pkg/exchange/okex/okexapi/trade.go b/pkg/exchange/okex/okexapi/trade.go index 7adc37c986..8a814057b6 100644 --- a/pkg/exchange/okex/okexapi/trade.go +++ b/pkg/exchange/okex/okexapi/trade.go @@ -363,7 +363,7 @@ func (r *GetOrderDetailsRequest) Do(ctx context.Context) (*OrderDetails, error) } if len(orderResponse.Data) == 0 { - return nil, errors.New("order create error") + return nil, errors.New("get order details error") } return &orderResponse.Data[0], nil From 6103a9350feab3fbcc5b9e05c8832fbc06ead94c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Aug 2023 15:54:28 +0800 Subject: [PATCH 1340/1392] deposit2transfer: add lastAssetDepositTimes for immediate success deposits --- pkg/strategy/deposit2transfer/strategy.go | 35 ++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index 6bfec0a654..ac86739b0a 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -47,6 +47,8 @@ type Strategy struct { session *bbgo.ExchangeSession watchingDeposits map[string]types.Deposit mu sync.Mutex + + lastAssetDepositTimes map[string]time.Time } func (s *Strategy) ID() string { @@ -74,6 +76,7 @@ func (s *Strategy) InstanceID() string { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { s.session = session s.watchingDeposits = make(map[string]types.Deposit) + s.lastAssetDepositTimes = make(map[string]time.Time) var ok bool @@ -161,7 +164,7 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio return nil, err } - // sort the recent deposit records in descending order + // sort the recent deposit records in ascending order sort.Slice(deposits, func(i, j int) bool { return deposits[i].Time.Time().Before(deposits[j].Time.Time()) }) @@ -183,9 +186,16 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio switch deposit.Status { case types.DepositSuccess: - // ignore all deposits that are already success - log.Infof("ignored succeess deposit: %s %+v", deposit.TransactionID, deposit) - continue + if depositTime, ok := s.lastAssetDepositTimes[asset]; ok { + // if it's newer than the latest deposit time, then we just add it the monitoring list + if deposit.Time.After(depositTime) { + log.Infof("adding new success deposit: %s", deposit.TransactionID) + s.watchingDeposits[deposit.TransactionID] = deposit + } + } else { + // ignore all initial deposit history that are already success + log.Infof("ignored succeess deposit: %s %+v", deposit.TransactionID, deposit) + } case types.DepositCredited, types.DepositPending: log.Infof("adding pending deposit: %s", deposit.TransactionID) @@ -194,6 +204,15 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio } } + if len(deposits) > 0 { + lastDeposit := deposits[len(deposits)-1] + if lastTime, ok := s.lastAssetDepositTimes[asset]; ok { + s.lastAssetDepositTimes[asset] = later(lastDeposit.Time.Time(), lastTime) + } else { + s.lastAssetDepositTimes[asset] = lastDeposit.Time.Time() + } + } + var succeededDeposits []types.Deposit for _, deposit := range s.watchingDeposits { if deposit.Status == types.DepositSuccess { @@ -212,3 +231,11 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio return succeededDeposits, nil } + +func later(a, b time.Time) time.Time { + if a.After(b) { + return a + } + + return b +} From e9d0ce5bbfe505286899e595dac3d34aff031ad2 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 9 Aug 2023 11:41:04 +0800 Subject: [PATCH 1341/1392] pkg/exchage: support k line rest api --- pkg/exchange/bybit/bybitapi/client_test.go | 11 + .../bybit/bybitapi/get_k_lines_request.go | 107 ++++++++ .../get_k_lines_request_requestgen.go | 237 ++++++++++++++++++ .../bybitapi/get_k_lines_request_test.go | 175 +++++++++++++ pkg/exchange/bybit/bybitapi/types.go | 36 +++ pkg/exchange/bybit/bybitapi/types_test.go | 41 +++ pkg/exchange/bybit/convert.go | 45 ++++ pkg/exchange/bybit/convert_test.go | 85 +++++++ pkg/exchange/bybit/exchange.go | 70 +++++- 9 files changed, 806 insertions(+), 1 deletion(-) create mode 100644 pkg/exchange/bybit/bybitapi/get_k_lines_request.go create mode 100644 pkg/exchange/bybit/bybitapi/get_k_lines_request_requestgen.go create mode 100644 pkg/exchange/bybit/bybitapi/get_k_lines_request_test.go create mode 100644 pkg/exchange/bybit/bybitapi/types_test.go diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index cf6a61215e..8156dd2c57 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -5,6 +5,7 @@ import ( "os" "strconv" "testing" + "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -162,4 +163,14 @@ func TestClient(t *testing.T) { assert.NoError(t, err) t.Logf("apiResp: %+v", apiResp) }) + + t.Run("GetKLinesRequest", func(t *testing.T) { + startTime := time.Date(2023, 8, 8, 9, 28, 0, 0, time.UTC) + endTime := time.Date(2023, 8, 8, 9, 45, 0, 0, time.UTC) + req := client.NewGetKLinesRequest(). + Symbol("BTCUSDT").Interval("15").StartTime(startTime).EndTime(endTime) + apiResp, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %+v", apiResp.List) + }) } diff --git a/pkg/exchange/bybit/bybitapi/get_k_lines_request.go b/pkg/exchange/bybit/bybitapi/get_k_lines_request.go new file mode 100644 index 0000000000..de04b72d36 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_k_lines_request.go @@ -0,0 +1,107 @@ +package bybitapi + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +type IntervalSign string + +const ( + IntervalSignDay IntervalSign = "D" + IntervalSignWeek IntervalSign = "W" + IntervalSignMonth IntervalSign = "M" +) + +type KLinesResponse struct { + Symbol string `json:"symbol"` + // An string array of individual candle + // Sort in reverse by startTime + List []KLine `json:"list"` + Category Category `json:"category"` +} + +type KLine struct { + // list[0]: startTime, Start time of the candle (ms) + StartTime types.MillisecondTimestamp + // list[1]: openPrice + Open fixedpoint.Value + // list[2]: highPrice + High fixedpoint.Value + // list[3]: lowPrice + Low fixedpoint.Value + // list[4]: closePrice + Close fixedpoint.Value + // list[5]: volume, Trade volume. Unit of contract: pieces of contract. Unit of spot: quantity of coins + Volume fixedpoint.Value + // list[6]: turnover, Turnover. Unit of figure: quantity of quota coin + TurnOver fixedpoint.Value +} + +const KLinesArrayLen = 7 + +func (k *KLine) UnmarshalJSON(data []byte) error { + var jsonArr []json.RawMessage + err := json.Unmarshal(data, &jsonArr) + if err != nil { + return fmt.Errorf("failed to unmarshal jsonRawMessage: %v, err: %w", string(data), err) + } + if len(jsonArr) != KLinesArrayLen { + return fmt.Errorf("unexpected K Lines array length: %d, exp: %d", len(jsonArr), KLinesArrayLen) + } + + err = json.Unmarshal(jsonArr[0], &k.StartTime) + if err != nil { + return fmt.Errorf("failed to unmarshal resp index 0: %v, err: %w", string(jsonArr[0]), err) + } + + values := make([]fixedpoint.Value, len(jsonArr)-1) + for i, jsonRaw := range jsonArr[1:] { + err = json.Unmarshal(jsonRaw, &values[i]) + if err != nil { + return fmt.Errorf("failed to unmarshal resp index %d: %v, err: %w", i+1, string(jsonRaw), err) + } + } + k.Open = values[0] + k.High = values[1] + k.Low = values[2] + k.Close = values[3] + k.Volume = values[4] + k.TurnOver = values[5] + + return nil +} + +//go:generate GetRequest -url "/v5/market/kline" -type GetKLinesRequest -responseDataType .KLinesResponse +type GetKLinesRequest struct { + client requestgen.APIClient + + category Category `param:"category,query" validValues:"spot"` + symbol string `param:"symbol,query"` + // Kline interval. + // - 1,3,5,15,30,60,120,240,360,720: minute + // - D: day + // - M: month + // - W: week + interval string `param:"interval,query" validValues:"1,3,5,15,30,60,120,240,360,720,D,W,M"` + startTime *time.Time `param:"start,query,milliseconds"` + endTime *time.Time `param:"end,query,milliseconds"` + // Limit for data size per page. [1, 1000]. Default: 200 + limit *uint64 `param:"limit,query"` +} + +func (c *RestClient) NewGetKLinesRequest() *GetKLinesRequest { + return &GetKLinesRequest{ + client: c, + category: CategorySpot, + } +} diff --git a/pkg/exchange/bybit/bybitapi/get_k_lines_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_k_lines_request_requestgen.go new file mode 100644 index 0000000000..d6b3d06c3f --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_k_lines_request_requestgen.go @@ -0,0 +1,237 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/market/kline -type GetKLinesRequest -responseDataType .KLinesResponse"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetKLinesRequest) Category(category Category) *GetKLinesRequest { + g.category = category + return g +} + +func (g *GetKLinesRequest) Symbol(symbol string) *GetKLinesRequest { + g.symbol = symbol + return g +} + +func (g *GetKLinesRequest) Interval(interval string) *GetKLinesRequest { + g.interval = interval + return g +} + +func (g *GetKLinesRequest) StartTime(startTime time.Time) *GetKLinesRequest { + g.startTime = &startTime + return g +} + +func (g *GetKLinesRequest) EndTime(endTime time.Time) *GetKLinesRequest { + g.endTime = &endTime + return g +} + +func (g *GetKLinesRequest) Limit(limit uint64) *GetKLinesRequest { + g.limit = &limit + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetKLinesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check category field -> json key category + category := g.category + + // TEMPLATE check-valid-values + switch category { + case "spot": + params["category"] = category + + default: + return nil, fmt.Errorf("category value %v is invalid", category) + + } + // END TEMPLATE check-valid-values + + // assign parameter of category + params["category"] = category + // check symbol field -> json key symbol + symbol := g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + // check interval field -> json key interval + interval := g.interval + + // TEMPLATE check-valid-values + switch interval { + case "1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "W", "M": + params["interval"] = interval + + default: + return nil, fmt.Errorf("interval value %v is invalid", interval) + + } + // END TEMPLATE check-valid-values + + // assign parameter of interval + params["interval"] = interval + // check startTime field -> json key start + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["start"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key end + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["end"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetKLinesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetKLinesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetKLinesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetKLinesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetKLinesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetKLinesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetKLinesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetKLinesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetKLinesRequest) Do(ctx context.Context) (*KLinesResponse, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v5/market/kline" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data KLinesResponse + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bybit/bybitapi/get_k_lines_request_test.go b/pkg/exchange/bybit/bybitapi/get_k_lines_request_test.go new file mode 100644 index 0000000000..05a739c580 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_k_lines_request_test.go @@ -0,0 +1,175 @@ +package bybitapi + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func TestKLinesResponse_UnmarshalJSON(t *testing.T) { + t.Run("succeeds", func(t *testing.T) { + data := `{ + "symbol": "BTCUSDT", + "category": "spot", + "list": [ + [ + "1670608800000", + "17071", + "17073", + "17027", + "17055.5", + "268611", + "15.74462667" + ], + [ + "1670605200000", + "17071.5", + "17071.5", + "17061", + "17071", + "4177", + "0.24469757" + ] + ] + }` + + expRes := &KLinesResponse{ + Symbol: "BTCUSDT", + List: []KLine{ + { + StartTime: types.NewMillisecondTimestampFromInt(1670608800000), + Open: fixedpoint.NewFromFloat(17071), + High: fixedpoint.NewFromFloat(17073), + Low: fixedpoint.NewFromFloat(17027), + Close: fixedpoint.NewFromFloat(17055.5), + Volume: fixedpoint.NewFromFloat(268611), + TurnOver: fixedpoint.NewFromFloat(15.74462667), + }, + { + StartTime: types.NewMillisecondTimestampFromInt(1670605200000), + Open: fixedpoint.NewFromFloat(17071.5), + High: fixedpoint.NewFromFloat(17071.5), + Low: fixedpoint.NewFromFloat(17061), + Close: fixedpoint.NewFromFloat(17071), + Volume: fixedpoint.NewFromFloat(4177), + TurnOver: fixedpoint.NewFromFloat(0.24469757), + }, + }, + Category: CategorySpot, + } + + kline := &KLinesResponse{} + err := json.Unmarshal([]byte(data), kline) + assert.NoError(t, err) + assert.Equal(t, expRes, kline) + }) + + t.Run("unexpected length", func(t *testing.T) { + data := `{ + "symbol": "BTCUSDT", + "category": "spot", + "list": [ + [ + "1670608800000", + "17071", + "17073", + "17027", + "17055.5", + "268611" + ] + ] + }` + kline := &KLinesResponse{} + err := json.Unmarshal([]byte(data), kline) + assert.Equal(t, fmt.Errorf("unexpected K Lines array length: 6, exp: %d", KLinesArrayLen), err) + }) + + t.Run("unexpected json array", func(t *testing.T) { + klineJson := `{}` + + data := fmt.Sprintf(`{ + "symbol": "BTCUSDT", + "category": "spot", + "list": [%s] + }`, klineJson) + + var jsonArr []json.RawMessage + expErr := json.Unmarshal([]byte(klineJson), &jsonArr) + assert.Error(t, expErr) + + kline := &KLinesResponse{} + err := json.Unmarshal([]byte(data), kline) + assert.Equal(t, fmt.Errorf("failed to unmarshal jsonRawMessage: %v, err: %w", klineJson, expErr), err) + }) + + t.Run("unexpected json 0", func(t *testing.T) { + klineJson := ` + [ + "a", + "17071.5", + "17071.5", + "17061", + "17071", + "4177", + "0.24469757" + ] + ` + + data := fmt.Sprintf(`{ + "symbol": "BTCUSDT", + "category": "spot", + "list": [%s] + }`, klineJson) + + var jsonArr []json.RawMessage + err := json.Unmarshal([]byte(klineJson), &jsonArr) + assert.NoError(t, err) + + timestamp := types.MillisecondTimestamp{} + expErr := json.Unmarshal(jsonArr[0], ×tamp) + assert.NoError(t, err) + + kline := &KLinesResponse{} + err = json.Unmarshal([]byte(data), kline) + assert.Equal(t, fmt.Errorf("failed to unmarshal resp index 0: %v, err: %w", string(jsonArr[0]), expErr), err) + }) + + t.Run("unexpected json 1", func(t *testing.T) { + // TODO: fix panic + t.Skip("test will result in a panic, skip it") + klineJson := ` + [ + "1670608800000", + "a", + "17071.5", + "17061", + "17071", + "4177", + "0.24469757" + ] + ` + + data := fmt.Sprintf(`{ + "symbol": "BTCUSDT", + "category": "spot", + "list": [%s] + }`, klineJson) + + var jsonArr []json.RawMessage + err := json.Unmarshal([]byte(klineJson), &jsonArr) + assert.NoError(t, err) + + var value fixedpoint.Value + expErr := json.Unmarshal(jsonArr[1], &value) + assert.NoError(t, err) + + kline := &KLinesResponse{} + err = json.Unmarshal([]byte(data), kline) + assert.Equal(t, fmt.Errorf("failed to unmarshal resp index 1: %v, err: %w", string(jsonArr[1]), expErr), err) + }) +} diff --git a/pkg/exchange/bybit/bybitapi/types.go b/pkg/exchange/bybit/bybitapi/types.go index d54e54d63d..509b51ded1 100644 --- a/pkg/exchange/bybit/bybitapi/types.go +++ b/pkg/exchange/bybit/bybitapi/types.go @@ -1,5 +1,41 @@ package bybitapi +import "github.com/c9s/bbgo/pkg/types" + +var ( + SupportedIntervals = map[types.Interval]int{ + types.Interval1m: 1 * 60, + types.Interval3m: 3 * 60, + types.Interval5m: 5 * 60, + types.Interval15m: 15 * 60, + types.Interval30m: 30 * 60, + types.Interval1h: 60 * 60, + types.Interval2h: 60 * 60 * 2, + types.Interval4h: 60 * 60 * 4, + types.Interval6h: 60 * 60 * 6, + types.Interval12h: 60 * 60 * 12, + types.Interval1d: 60 * 60 * 24, + types.Interval1w: 60 * 60 * 24 * 7, + types.Interval1mo: 60 * 60 * 24 * 30, + } + + ToGlobalInterval = map[string]types.Interval{ + "1": types.Interval1m, + "3": types.Interval3m, + "5": types.Interval5m, + "15": types.Interval15m, + "30": types.Interval30m, + "60": types.Interval1h, + "120": types.Interval2h, + "240": types.Interval4h, + "360": types.Interval6h, + "720": types.Interval12h, + "D": types.Interval1d, + "W": types.Interval1w, + "M": types.Interval1mo, + } +) + type Category string const ( diff --git a/pkg/exchange/bybit/bybitapi/types_test.go b/pkg/exchange/bybit/bybitapi/types_test.go new file mode 100644 index 0000000000..ba39b3d848 --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/types_test.go @@ -0,0 +1,41 @@ +package bybitapi + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" +) + +func Test_SupportedIntervals(t *testing.T) { + assert.Equal(t, SupportedIntervals[types.Interval1m], 60) + assert.Equal(t, SupportedIntervals[types.Interval3m], 180) + assert.Equal(t, SupportedIntervals[types.Interval5m], 300) + assert.Equal(t, SupportedIntervals[types.Interval15m], 15*60) + assert.Equal(t, SupportedIntervals[types.Interval30m], 30*60) + assert.Equal(t, SupportedIntervals[types.Interval1h], 60*60) + assert.Equal(t, SupportedIntervals[types.Interval2h], 60*60*2) + assert.Equal(t, SupportedIntervals[types.Interval4h], 60*60*4) + assert.Equal(t, SupportedIntervals[types.Interval6h], 60*60*6) + assert.Equal(t, SupportedIntervals[types.Interval12h], 60*60*12) + assert.Equal(t, SupportedIntervals[types.Interval1d], 60*60*24) + assert.Equal(t, SupportedIntervals[types.Interval1w], 60*60*24*7) + assert.Equal(t, SupportedIntervals[types.Interval1mo], 60*60*24*30) +} + +func Test_ToGlobalInterval(t *testing.T) { + assert.Equal(t, ToGlobalInterval["1"], types.Interval1m) + assert.Equal(t, ToGlobalInterval["3"], types.Interval3m) + assert.Equal(t, ToGlobalInterval["5"], types.Interval5m) + assert.Equal(t, ToGlobalInterval["15"], types.Interval15m) + assert.Equal(t, ToGlobalInterval["30"], types.Interval30m) + assert.Equal(t, ToGlobalInterval["60"], types.Interval1h) + assert.Equal(t, ToGlobalInterval["120"], types.Interval2h) + assert.Equal(t, ToGlobalInterval["240"], types.Interval4h) + assert.Equal(t, ToGlobalInterval["360"], types.Interval6h) + assert.Equal(t, ToGlobalInterval["720"], types.Interval12h) + assert.Equal(t, ToGlobalInterval["D"], types.Interval1d) + assert.Equal(t, ToGlobalInterval["W"], types.Interval1w) + assert.Equal(t, ToGlobalInterval["M"], types.Interval1mo) +} diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index 9f665ecec0..19043ad23c 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -283,3 +283,48 @@ func toGlobalBalanceMap(events []bybitapi.WalletBalances) types.BalanceMap { } return bm } + +func toLocalInterval(interval types.Interval) (string, error) { + if _, found := bybitapi.SupportedIntervals[interval]; !found { + return "", fmt.Errorf("interval not supported: %s", interval) + } + + switch interval { + + case types.Interval1d: + return string(bybitapi.IntervalSignDay), nil + + case types.Interval1w: + return string(bybitapi.IntervalSignWeek), nil + + case types.Interval1mo: + return string(bybitapi.IntervalSignMonth), nil + + default: + return fmt.Sprintf("%d", interval.Minutes()), nil + + } +} + +func toGlobalKLines(symbol string, interval types.Interval, klines []bybitapi.KLine) []types.KLine { + gKLines := make([]types.KLine, len(klines)) + for i, kline := range klines { + endTime := types.Time(kline.StartTime.Time().Add(interval.Duration())) + gKLines[i] = types.KLine{ + Exchange: types.ExchangeBybit, + Symbol: symbol, + StartTime: types.Time(kline.StartTime), + EndTime: endTime, + Interval: interval, + Open: kline.Open, + Close: kline.Close, + High: kline.High, + Low: kline.Low, + Volume: kline.Volume, + QuoteVolume: kline.TurnOver, + // Bybit doesn't support close flag in REST API + Closed: false, + } + } + return gKLines +} diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index e6eb5b0491..d2dab16320 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -459,3 +459,88 @@ func Test_toGlobalTrade(t *testing.T) { assert.NoError(t, err) assert.Equal(t, res, &exp) } + +func Test_toGlobalKLines(t *testing.T) { + symbol := "BTCUSDT" + interval := types.Interval15m + + resp := bybitapi.KLinesResponse{ + Symbol: symbol, + List: []bybitapi.KLine{ + /* + [ + { + "StartTime": "2023-08-08 17:30:00 +0800 CST", + "OpenPrice": 29045.3, + "HighPrice": 29228.56, + "LowPrice": 29045.3, + "ClosePrice": 29228.56, + "Volume": 9.265593, + "TurnOver": 270447.43520753 + }, + { + "StartTime": "2023-08-08 17:15:00 +0800 CST", + "OpenPrice": 29167.33, + "HighPrice": 29229.08, + "LowPrice": 29000, + "ClosePrice": 29045.3, + "Volume": 9.295508, + "TurnOver": 270816.87513775 + } + ] + */ + { + StartTime: types.NewMillisecondTimestampFromInt(1691486100000), + Open: fixedpoint.NewFromFloat(29045.3), + High: fixedpoint.NewFromFloat(29228.56), + Low: fixedpoint.NewFromFloat(29045.3), + Close: fixedpoint.NewFromFloat(29228.56), + Volume: fixedpoint.NewFromFloat(9.265593), + TurnOver: fixedpoint.NewFromFloat(270447.43520753), + }, + { + StartTime: types.NewMillisecondTimestampFromInt(1691487000000), + Open: fixedpoint.NewFromFloat(29167.33), + High: fixedpoint.NewFromFloat(29229.08), + Low: fixedpoint.NewFromFloat(29000), + Close: fixedpoint.NewFromFloat(29045.3), + Volume: fixedpoint.NewFromFloat(9.295508), + TurnOver: fixedpoint.NewFromFloat(270816.87513775), + }, + }, + Category: bybitapi.CategorySpot, + } + + expKlines := []types.KLine{ + { + Exchange: types.ExchangeBybit, + Symbol: resp.Symbol, + StartTime: types.Time(resp.List[0].StartTime.Time()), + EndTime: types.Time(resp.List[0].StartTime.Time().Add(interval.Duration())), + Interval: interval, + Open: fixedpoint.NewFromFloat(29045.3), + Close: fixedpoint.NewFromFloat(29228.56), + High: fixedpoint.NewFromFloat(29228.56), + Low: fixedpoint.NewFromFloat(29045.3), + Volume: fixedpoint.NewFromFloat(9.265593), + QuoteVolume: fixedpoint.NewFromFloat(270447.43520753), + Closed: false, + }, + { + Exchange: types.ExchangeBybit, + Symbol: resp.Symbol, + StartTime: types.Time(resp.List[1].StartTime.Time()), + EndTime: types.Time(resp.List[1].StartTime.Time().Add(interval.Duration())), + Interval: interval, + Open: fixedpoint.NewFromFloat(29167.33), + Close: fixedpoint.NewFromFloat(29045.3), + High: fixedpoint.NewFromFloat(29229.08), + Low: fixedpoint.NewFromFloat(29000), + Volume: fixedpoint.NewFromFloat(9.295508), + QuoteVolume: fixedpoint.NewFromFloat(270816.87513775), + Closed: false, + }, + } + + assert.Equal(t, toGlobalKLines(symbol, interval, resp.List), expKlines) +} diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 6998f5407f..9629585898 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -19,6 +19,7 @@ import ( const ( maxOrderIdLen = 36 defaultQueryLimit = 50 + defaultKLineLimit = 1000 halfYearDuration = 6 * 30 * 24 * time.Hour ) @@ -38,7 +39,12 @@ var ( "exchange": "bybit", }) - _ types.ExchangeAccountService = &Exchange{} + _ types.ExchangeAccountService = &Exchange{} + _ types.ExchangeMarketDataService = &Exchange{} + _ types.CustomIntervalProvider = &Exchange{} + _ types.ExchangeMinimal = &Exchange{} + _ types.ExchangeTradeService = &Exchange{} + _ types.Exchange = &Exchange{} ) type Exchange struct { @@ -422,6 +428,68 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, return toGlobalBalanceMap(accounts.List), nil } + +/* +QueryKLines queries for historical klines (also known as candles/candlesticks). Charts are returned in groups based +on the requested interval. + +A k-line's start time is inclusive, but end time is not(startTime + interval - 1 millisecond). +e.q. 15m interval k line can be represented as 00:00:00.000 ~ 00:14:59.999 +*/ +func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { + req := e.client.NewGetKLinesRequest().Symbol(symbol) + intervalStr, err := toLocalInterval(interval) + if err != nil { + return nil, err + } + req.Interval(intervalStr) + + limit := uint64(options.Limit) + if limit > defaultKLineLimit || limit <= 0 { + log.Debugf("limtit is exceeded or zero, update to %d, got: %d", defaultKLineLimit, options.Limit) + limit = defaultKLineLimit + } + req.Limit(limit) + + if options.StartTime != nil { + req.StartTime(*options.StartTime) + } + + if options.EndTime != nil { + req.EndTime(*options.EndTime) + } + + if err := sharedRateLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("query klines rate limiter wait error: %w", err) + } + + resp, err := req.Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to call k line, err: %w", err) + } + + if resp.Category != bybitapi.CategorySpot { + return nil, fmt.Errorf("unexpected category: %s", resp.Category) + } + + if resp.Symbol != symbol { + return nil, fmt.Errorf("unexpected symbol: %s, exp: %s", resp.Category, symbol) + } + + kLines := toGlobalKLines(symbol, interval, resp.List) + return types.SortKLinesAscending(kLines), nil + +} + +func (e *Exchange) SupportedInterval() map[types.Interval]int { + return bybitapi.SupportedIntervals +} + +func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { + _, ok := bybitapi.SupportedIntervals[interval] + return ok +} + func (e *Exchange) NewStream() types.Stream { return NewStream(e.key, e.secret) } From 4cee22ce31aa96c2f513f831aa842afd9d5e466d Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 9 Aug 2023 11:41:16 +0800 Subject: [PATCH 1342/1392] pkg/exchage: support k line websocket event --- pkg/exchange/bybit/stream.go | 62 +++++++++++++-- pkg/exchange/bybit/stream_callbacks.go | 10 +++ pkg/exchange/bybit/stream_test.go | 103 +++++++++++++++++++++++-- pkg/exchange/bybit/types.go | 62 +++++++++++++++ pkg/exchange/bybit/types_test.go | 71 +++++++++++++++++ 5 files changed, 297 insertions(+), 11 deletions(-) diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index dcaa158fe2..1fe90298db 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -34,6 +34,7 @@ type Stream struct { bookEventCallbacks []func(e BookEvent) walletEventCallbacks []func(e []bybitapi.WalletBalances) + kLineEventCallbacks []func(e KLineEvent) orderEventCallbacks []func(e []OrderEvent) } @@ -52,6 +53,7 @@ func NewStream(key, secret string) *Stream { stream.OnConnect(stream.handlerConnect) stream.OnBookEvent(stream.handleBookEvent) + stream.OnKLineEvent(stream.handleKLineEvent) stream.OnWalletEvent(stream.handleWalletEvent) stream.OnOrderEvent(stream.handleOrderEvent) return stream @@ -80,6 +82,9 @@ func (s *Stream) dispatchEvent(event interface{}) { case []bybitapi.WalletBalances: s.EmitWalletEvent(e) + case *KLineEvent: + s.EmitKLineEvent(*e) + case []OrderEvent: s.EmitOrderEvent(e) } @@ -99,6 +104,7 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { case e.IsTopic(): switch getTopicType(e.Topic) { + case TopicTypeOrderBook: var book BookEvent err = json.Unmarshal(e.WebSocketTopicEvent.Data, &book) @@ -109,6 +115,20 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { book.Type = e.WebSocketTopicEvent.Type return &book, nil + case TopicTypeKLine: + var kLines []KLine + err = json.Unmarshal(e.WebSocketTopicEvent.Data, &kLines) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal data into KLine: %+v, : %w", string(e.WebSocketTopicEvent.Data), err) + } + + symbol, err := getSymbolFromTopic(e.Topic) + if err != nil { + return nil, err + } + + return &KLineEvent{KLines: kLines, Symbol: symbol, Type: e.WebSocketTopicEvent.Type}, nil + case TopicTypeWallet: var wallets []bybitapi.WalletBalances return wallets, json.Unmarshal(e.WebSocketTopicEvent.Data, &wallets) @@ -165,7 +185,7 @@ func (s *Stream) handlerConnect() { var topics []string for _, subscription := range s.Subscriptions { - topic, err := convertSubscription(subscription) + topic, err := s.convertSubscription(subscription) if err != nil { log.WithError(err).Errorf("subscription convert error") continue @@ -213,17 +233,27 @@ func (s *Stream) handlerConnect() { } } -func convertSubscription(s types.Subscription) (string, error) { - switch s.Channel { +func (s *Stream) convertSubscription(sub types.Subscription) (string, error) { + switch sub.Channel { + case types.BookChannel: depth := types.DepthLevel1 - if len(s.Options.Depth) > 0 && s.Options.Depth == types.DepthLevel50 { + if len(sub.Options.Depth) > 0 && sub.Options.Depth == types.DepthLevel50 { depth = types.DepthLevel50 } - return genTopic(TopicTypeOrderBook, depth, s.Symbol), nil + return genTopic(TopicTypeOrderBook, depth, sub.Symbol), nil + + case types.KLineChannel: + interval, err := toLocalInterval(sub.Options.Interval) + if err != nil { + return "", err + } + + return genTopic(TopicTypeKLine, interval, sub.Symbol), nil + } - return "", fmt.Errorf("unsupported stream channel: %s", s.Channel) + return "", fmt.Errorf("unsupported stream channel: %s", sub.Channel) } func (s *Stream) handleBookEvent(e BookEvent) { @@ -257,3 +287,23 @@ func (s *Stream) handleOrderEvent(events []OrderEvent) { s.StandardStream.EmitOrderUpdate(*gOrder) } } + +func (s *Stream) handleKLineEvent(klineEvent KLineEvent) { + if klineEvent.Type != DataTypeSnapshot { + return + } + + for _, event := range klineEvent.KLines { + kline, err := event.toGlobalKLine(klineEvent.Symbol) + if err != nil { + log.WithError(err).Error("failed to convert to global k line") + continue + } + + if kline.Closed { + s.EmitKLineClosed(kline) + } else { + s.EmitKLine(kline) + } + } +} diff --git a/pkg/exchange/bybit/stream_callbacks.go b/pkg/exchange/bybit/stream_callbacks.go index 2669dcfa7c..0c7df8042b 100644 --- a/pkg/exchange/bybit/stream_callbacks.go +++ b/pkg/exchange/bybit/stream_callbacks.go @@ -26,6 +26,16 @@ func (s *Stream) EmitWalletEvent(e []bybitapi.WalletBalances) { } } +func (s *Stream) OnKLineEvent(cb func(e KLineEvent)) { + s.kLineEventCallbacks = append(s.kLineEventCallbacks, cb) +} + +func (s *Stream) EmitKLineEvent(e KLineEvent) { + for _, cb := range s.kLineEventCallbacks { + cb(e) + } +} + func (s *Stream) OnOrderEvent(cb func(e []OrderEvent)) { s.orderEventCallbacks = append(s.orderEventCallbacks, cb) } diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go index 07ced5e43c..1c6a3badf0 100644 --- a/pkg/exchange/bybit/stream_test.go +++ b/pkg/exchange/bybit/stream_test.go @@ -2,6 +2,7 @@ package bybit import ( "context" + "errors" "fmt" "os" "strconv" @@ -76,6 +77,23 @@ func TestStream(t *testing.T) { c := make(chan struct{}) <-c }) + + t.Run("kline test", func(t *testing.T) { + s.Subscribe(types.KLineChannel, "BTCUSDT", types.SubscribeOptions{ + Interval: types.Interval30m, + Depth: "", + Speed: "", + }) + s.SetPublicOnly() + err := s.Connect(context.Background()) + assert.NoError(t, err) + + s.OnKLine(func(kline types.KLine) { + t.Log(kline) + }) + c := make(chan struct{}) + <-c + }) } func TestStream_parseWebSocketEvent(t *testing.T) { @@ -151,6 +169,80 @@ func TestStream_parseWebSocketEvent(t *testing.T) { }, *book) }) + t.Run("TopicTypeKLine with snapshot", func(t *testing.T) { + input := `{ + "topic": "kline.5.BTCUSDT", + "data": [ + { + "start": 1672324800000, + "end": 1672325099999, + "interval": "5", + "open": "16649.5", + "close": "16677", + "high": "16677", + "low": "16608", + "volume": "2.081", + "turnover": "34666.4005", + "confirm": false, + "timestamp": 1672324988882 + } + ], + "ts": 1672324988882, + "type": "snapshot" +}` + + res, err := s.parseWebSocketEvent([]byte(input)) + assert.NoError(t, err) + book, ok := res.(*KLineEvent) + assert.True(t, ok) + assert.Equal(t, KLineEvent{ + Symbol: "BTCUSDT", + Type: DataTypeSnapshot, + KLines: []KLine{ + { + StartTime: types.NewMillisecondTimestampFromInt(1672324800000), + EndTime: types.NewMillisecondTimestampFromInt(1672325099999), + Interval: "5", + OpenPrice: fixedpoint.NewFromFloat(16649.5), + ClosePrice: fixedpoint.NewFromFloat(16677), + HighPrice: fixedpoint.NewFromFloat(16677), + LowPrice: fixedpoint.NewFromFloat(16608), + Volume: fixedpoint.NewFromFloat(2.081), + Turnover: fixedpoint.NewFromFloat(34666.4005), + Confirm: false, + Timestamp: types.NewMillisecondTimestampFromInt(1672324988882), + }, + }, + }, *book) + }) + + t.Run("TopicTypeKLine with invalid topic", func(t *testing.T) { + input := `{ + "topic": "kline.5", + "data": [ + { + "start": 1672324800000, + "end": 1672325099999, + "interval": "5", + "open": "16649.5", + "close": "16677", + "high": "16677", + "low": "16608", + "volume": "2.081", + "turnover": "34666.4005", + "confirm": false, + "timestamp": 1672324988882 + } + ], + "ts": 1672324988882, + "type": "snapshot" +}` + + res, err := s.parseWebSocketEvent([]byte(input)) + assert.Equal(t, errors.New("unexpected topic: kline.5"), err) + assert.Nil(t, res) + }) + t.Run("Parse fails", func(t *testing.T) { input := `{ "topic":"orderbook.50.BTCUSDT", @@ -168,8 +260,9 @@ func TestStream_parseWebSocketEvent(t *testing.T) { } func Test_convertSubscription(t *testing.T) { + s := Stream{} t.Run("BookChannel.DepthLevel1", func(t *testing.T) { - res, err := convertSubscription(types.Subscription{ + res, err := s.convertSubscription(types.Subscription{ Symbol: "BTCUSDT", Channel: types.BookChannel, Options: types.SubscribeOptions{ @@ -180,7 +273,7 @@ func Test_convertSubscription(t *testing.T) { assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel1, "BTCUSDT"), res) }) t.Run("BookChannel. with default depth", func(t *testing.T) { - res, err := convertSubscription(types.Subscription{ + res, err := s.convertSubscription(types.Subscription{ Symbol: "BTCUSDT", Channel: types.BookChannel, }) @@ -188,7 +281,7 @@ func Test_convertSubscription(t *testing.T) { assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel1, "BTCUSDT"), res) }) t.Run("BookChannel.DepthLevel50", func(t *testing.T) { - res, err := convertSubscription(types.Subscription{ + res, err := s.convertSubscription(types.Subscription{ Symbol: "BTCUSDT", Channel: types.BookChannel, Options: types.SubscribeOptions{ @@ -199,7 +292,7 @@ func Test_convertSubscription(t *testing.T) { assert.Equal(t, genTopic(TopicTypeOrderBook, types.DepthLevel50, "BTCUSDT"), res) }) t.Run("BookChannel. not support depth, use default level 1", func(t *testing.T) { - res, err := convertSubscription(types.Subscription{ + res, err := s.convertSubscription(types.Subscription{ Symbol: "BTCUSDT", Channel: types.BookChannel, Options: types.SubscribeOptions{ @@ -211,7 +304,7 @@ func Test_convertSubscription(t *testing.T) { }) t.Run("unsupported channel", func(t *testing.T) { - res, err := convertSubscription(types.Subscription{ + res, err := s.convertSubscription(types.Subscription{ Symbol: "BTCUSDT", Channel: "unsupported", }) diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index 8046630b83..d817c8e7b5 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -82,6 +82,7 @@ const ( TopicTypeOrderBook TopicType = "orderbook" TopicTypeWallet TopicType = "wallet" TopicTypeOrder TopicType = "order" + TopicTypeKLine TopicType = "kline" ) type DataType string @@ -143,8 +144,69 @@ func getTopicType(topic string) TopicType { return TopicType(slice[0]) } +func getSymbolFromTopic(topic string) (string, error) { + slice := strings.Split(topic, topicSeparator) + if len(slice) != 3 { + return "", fmt.Errorf("unexpected topic: %s", topic) + } + return slice[2], nil +} + type OrderEvent struct { bybitapi.Order Category bybitapi.Category `json:"category"` } + +type KLineEvent struct { + KLines []KLine + + // internal use + // Type can be one of snapshot or delta. Copied from WebSocketTopicEvent.Type + Type DataType + // Symbol. Copied from WebSocketTopicEvent.Topic + Symbol string +} + +type KLine struct { + // The start timestamp (ms) + StartTime types.MillisecondTimestamp `json:"start"` + // The end timestamp (ms) + EndTime types.MillisecondTimestamp `json:"end"` + // Kline interval + Interval string `json:"interval"` + OpenPrice fixedpoint.Value `json:"open"` + ClosePrice fixedpoint.Value `json:"close"` + HighPrice fixedpoint.Value `json:"high"` + LowPrice fixedpoint.Value `json:"low"` + // Trade volume + Volume fixedpoint.Value `json:"volume"` + // Turnover. Unit of figure: quantity of quota coin + Turnover fixedpoint.Value `json:"turnover"` + // Weather the tick is ended or not + Confirm bool `json:"confirm"` + // The timestamp (ms) of the last matched order in the candle + Timestamp types.MillisecondTimestamp `json:"timestamp"` +} + +func (k *KLine) toGlobalKLine(symbol string) (types.KLine, error) { + interval, found := bybitapi.ToGlobalInterval[k.Interval] + if !found { + return types.KLine{}, fmt.Errorf("unexpected k line interval: %+v", k) + } + + return types.KLine{ + Exchange: types.ExchangeBybit, + Symbol: symbol, + StartTime: types.Time(k.StartTime.Time()), + EndTime: types.Time(k.EndTime.Time()), + Interval: interval, + Open: k.OpenPrice, + Close: k.ClosePrice, + High: k.HighPrice, + Low: k.LowPrice, + Volume: k.Volume, + QuoteVolume: k.Turnover, + Closed: k.Confirm, + }, nil +} diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go index 3332814cf8..22aec43bb5 100644 --- a/pkg/exchange/bybit/types_test.go +++ b/pkg/exchange/bybit/types_test.go @@ -386,3 +386,74 @@ func Test_getTopicName(t *testing.T) { exp := TopicTypeOrderBook assert.Equal(t, exp, getTopicType("orderbook.50.BTCUSDT")) } + +func Test_getSymbolFromTopic(t *testing.T) { + t.Run("succeeds", func(t *testing.T) { + exp := "BTCUSDT" + res, err := getSymbolFromTopic("kline.1.BTCUSDT") + assert.NoError(t, err) + assert.Equal(t, exp, res) + }) + + t.Run("unexpected topic", func(t *testing.T) { + res, err := getSymbolFromTopic("kline.1") + assert.Empty(t, res) + assert.Equal(t, err, fmt.Errorf("unexpected topic: kline.1")) + }) +} + +func TestKLine_toGlobalKLine(t *testing.T) { + t.Run("succeeds", func(t *testing.T) { + k := KLine{ + StartTime: types.NewMillisecondTimestampFromInt(1691486100000), + EndTime: types.NewMillisecondTimestampFromInt(1691487000000), + Interval: "1", + OpenPrice: fixedpoint.NewFromFloat(29045.3), + ClosePrice: fixedpoint.NewFromFloat(29228.56), + HighPrice: fixedpoint.NewFromFloat(29228.56), + LowPrice: fixedpoint.NewFromFloat(29045.3), + Volume: fixedpoint.NewFromFloat(9.265593), + Turnover: fixedpoint.NewFromFloat(270447.43520753), + Confirm: false, + Timestamp: types.NewMillisecondTimestampFromInt(1691486100000), + } + + gKline, err := k.toGlobalKLine("BTCUSDT") + assert.NoError(t, err) + + assert.Equal(t, types.KLine{ + Exchange: types.ExchangeBybit, + Symbol: "BTCUSDT", + StartTime: types.Time(k.StartTime.Time()), + EndTime: types.Time(k.EndTime.Time()), + Interval: types.Interval1m, + Open: fixedpoint.NewFromFloat(29045.3), + Close: fixedpoint.NewFromFloat(29228.56), + High: fixedpoint.NewFromFloat(29228.56), + Low: fixedpoint.NewFromFloat(29045.3), + Volume: fixedpoint.NewFromFloat(9.265593), + QuoteVolume: fixedpoint.NewFromFloat(270447.43520753), + Closed: false, + }, gKline) + }) + + t.Run("interval not supported", func(t *testing.T) { + k := KLine{ + StartTime: types.NewMillisecondTimestampFromInt(1691486100000), + EndTime: types.NewMillisecondTimestampFromInt(1691487000000), + Interval: "112", + OpenPrice: fixedpoint.NewFromFloat(29045.3), + ClosePrice: fixedpoint.NewFromFloat(29228.56), + HighPrice: fixedpoint.NewFromFloat(29228.56), + LowPrice: fixedpoint.NewFromFloat(29045.3), + Volume: fixedpoint.NewFromFloat(9.265593), + Turnover: fixedpoint.NewFromFloat(270447.43520753), + Confirm: false, + Timestamp: types.NewMillisecondTimestampFromInt(1691486100000), + } + + gKline, err := k.toGlobalKLine("BTCUSDT") + assert.Equal(t, fmt.Errorf("unexpected k line interval: %+v", &k), err) + assert.Equal(t, gKline, types.KLine{}) + }) +} From ace2c55a17166d0b88d9c76b0679ee49e5c06eeb Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 10 Aug 2023 15:02:30 +0800 Subject: [PATCH 1343/1392] exchange/bybit: add fee rate restful api --- pkg/exchange/bybit/bybitapi/client_test.go | 7 + .../bybit/bybitapi/get_fee_rates_request.go | 38 ++++ .../get_fee_rates_request_requestgen.go | 189 ++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 pkg/exchange/bybit/bybitapi/get_fee_rates_request.go create mode 100644 pkg/exchange/bybit/bybitapi/get_fee_rates_request_requestgen.go diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index 8156dd2c57..4a1403d38f 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -173,4 +173,11 @@ func TestClient(t *testing.T) { assert.NoError(t, err) t.Logf("apiResp: %+v", apiResp.List) }) + + t.Run("GetFeeRatesRequest", func(t *testing.T) { + req := client.NewGetFeeRatesRequest() + apiResp, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %+v", apiResp) + }) } diff --git a/pkg/exchange/bybit/bybitapi/get_fee_rates_request.go b/pkg/exchange/bybit/bybitapi/get_fee_rates_request.go new file mode 100644 index 0000000000..325316c9ac --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_fee_rates_request.go @@ -0,0 +1,38 @@ +package bybitapi + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +type FeeRates struct { + List []FeeRate `json:"list"` +} + +type FeeRate struct { + Symbol string `json:"symbol"` + TakerFeeRate fixedpoint.Value `json:"takerFeeRate"` + MakerFeeRate fixedpoint.Value `json:"makerFeeRate"` +} + +//go:generate GetRequest -url "/v5/account/fee-rate" -type GetFeeRatesRequest -responseDataType .FeeRates +type GetFeeRatesRequest struct { + client requestgen.AuthenticatedAPIClient + + category Category `param:"category,query" validValues:"spot"` + // Symbol name. Valid for linear, inverse, spot + symbol *string `param:"symbol,query"` + // Base coin. SOL, BTC, ETH. Valid for option + baseCoin *string `param:"baseCoin,query"` +} + +func (c *RestClient) NewGetFeeRatesRequest() *GetFeeRatesRequest { + return &GetFeeRatesRequest{ + client: c, + category: CategorySpot, + } +} diff --git a/pkg/exchange/bybit/bybitapi/get_fee_rates_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_fee_rates_request_requestgen.go new file mode 100644 index 0000000000..ac182c22ac --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_fee_rates_request_requestgen.go @@ -0,0 +1,189 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/account/fee-rate -type GetFeeRatesRequest -responseDataType .FeeRates"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetFeeRatesRequest) Category(category Category) *GetFeeRatesRequest { + g.category = category + return g +} + +func (g *GetFeeRatesRequest) Symbol(symbol string) *GetFeeRatesRequest { + g.symbol = &symbol + return g +} + +func (g *GetFeeRatesRequest) BaseCoin(baseCoin string) *GetFeeRatesRequest { + g.baseCoin = &baseCoin + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetFeeRatesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check category field -> json key category + category := g.category + + // TEMPLATE check-valid-values + switch category { + case "spot": + params["category"] = category + + default: + return nil, fmt.Errorf("category value %v is invalid", category) + + } + // END TEMPLATE check-valid-values + + // assign parameter of category + params["category"] = category + // check symbol field -> json key symbol + if g.symbol != nil { + symbol := *g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + } else { + } + // check baseCoin field -> json key baseCoin + if g.baseCoin != nil { + baseCoin := *g.baseCoin + + // assign parameter of baseCoin + params["baseCoin"] = baseCoin + } else { + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetFeeRatesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetFeeRatesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetFeeRatesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetFeeRatesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetFeeRatesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetFeeRatesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetFeeRatesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetFeeRatesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetFeeRatesRequest) Do(ctx context.Context) (*FeeRates, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v5/account/fee-rate" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data FeeRates + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} From 0b03336fb013f6fcae28276c05fb6587a708a557 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 10 Aug 2023 15:05:13 +0800 Subject: [PATCH 1344/1392] pkg/exchange: support GetFeeRates on bybit exchange --- pkg/exchange/bybit/exchange.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 9629585898..89a19a496d 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -27,10 +27,10 @@ const ( // https://bybit-exchange.github.io/docs/zh-TW/v5/rate-limit // sharedRateLimiter indicates that the API belongs to the public API. // -// The default order limiter apply 3 requests per second and a 2 initial bucket -// this includes QueryMarkets, QueryTicker, QueryAccountBalances +// The default order limiter apply 5 requests per second and a 5 initial bucket +// this includes QueryMarkets, QueryTicker, QueryAccountBalances, GetFeeRates var ( - sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/3), 2) + sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5) tradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5) orderRateLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) closedOrderQueryLimiter = rate.NewLimiter(rate.Every(time.Second), 1) @@ -490,6 +490,18 @@ func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { return ok } +func (e *Exchange) GetFeeRates(ctx context.Context) (bybitapi.FeeRates, error) { + if err := sharedRateLimiter.Wait(ctx); err != nil { + return bybitapi.FeeRates{}, fmt.Errorf("query fee rate limiter wait error: %w", err) + } + feeRates, err := e.client.NewGetFeeRatesRequest().Do(ctx) + if err != nil { + return bybitapi.FeeRates{}, fmt.Errorf("failed to get fee rates, err: %w", err) + } + + return *feeRates, nil +} + func (e *Exchange) NewStream() types.Stream { return NewStream(e.key, e.secret) } From 509f9ac8caf38ae950501bfec4f4804d4d4a9082 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 10 Aug 2023 15:08:31 +0800 Subject: [PATCH 1345/1392] pkg/types: add BeforeConnect hook function --- pkg/types/stream.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/types/stream.go b/pkg/types/stream.go index 6bcd5e39e2..668a12856e 100644 --- a/pkg/types/stream.go +++ b/pkg/types/stream.go @@ -44,6 +44,8 @@ type Dispatcher func(e interface{}) // HeartBeat keeps connection alive by sending the heartbeat packet. type HeartBeat func(ctxConn context.Context, conn *websocket.Conn, cancelConn context.CancelFunc) +type BeforeConnect func(ctx context.Context) error + //go:generate callbackgen -type StandardStream -interface type StandardStream struct { parser Parser @@ -111,6 +113,8 @@ type StandardStream struct { FuturesPositionSnapshotCallbacks []func(futuresPositions FuturesPositionMap) heartBeat HeartBeat + + beforeConnect BeforeConnect } type StandardStreamEmitter interface { @@ -306,6 +310,11 @@ func (s *StandardStream) Reconnect() { // Connect starts the stream and create the websocket connection func (s *StandardStream) Connect(ctx context.Context) error { + if s.beforeConnect != nil { + if err := s.beforeConnect(ctx); err != nil { + return err + } + } err := s.DialAndConnect(ctx) if err != nil { return err @@ -432,6 +441,11 @@ func (s *StandardStream) SetHeartBeat(fn HeartBeat) { s.heartBeat = fn } +// SetBeforeConnect sets the custom hook function before connect +func (s *StandardStream) SetBeforeConnect(fn BeforeConnect) { + s.beforeConnect = fn +} + type Depth string const ( From affff325992ed972129511574cd0c27079fbf69c Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 10 Aug 2023 15:11:06 +0800 Subject: [PATCH 1346/1392] pkg/exchange: get fee rate before connect --- pkg/exchange/bybit/exchange.go | 4 +- pkg/exchange/bybit/mocks/stream.go | 67 ++++++++++++++++ pkg/exchange/bybit/stream.go | 72 +++++++++++++++-- pkg/exchange/bybit/stream_test.go | 124 ++++++++++++++++++++++++++++- pkg/exchange/bybit/types_test.go | 11 ++- 5 files changed, 266 insertions(+), 12 deletions(-) create mode 100644 pkg/exchange/bybit/mocks/stream.go diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 89a19a496d..01ba110ff6 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -490,7 +490,7 @@ func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { return ok } -func (e *Exchange) GetFeeRates(ctx context.Context) (bybitapi.FeeRates, error) { +func (e *Exchange) GetAllFeeRates(ctx context.Context) (bybitapi.FeeRates, error) { if err := sharedRateLimiter.Wait(ctx); err != nil { return bybitapi.FeeRates{}, fmt.Errorf("query fee rate limiter wait error: %w", err) } @@ -503,5 +503,5 @@ func (e *Exchange) GetFeeRates(ctx context.Context) (bybitapi.FeeRates, error) { } func (e *Exchange) NewStream() types.Stream { - return NewStream(e.key, e.secret) + return NewStream(e.key, e.secret, e) } diff --git a/pkg/exchange/bybit/mocks/stream.go b/pkg/exchange/bybit/mocks/stream.go new file mode 100644 index 0000000000..df6c225fef --- /dev/null +++ b/pkg/exchange/bybit/mocks/stream.go @@ -0,0 +1,67 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/c9s/bbgo/pkg/exchange/bybit (interfaces: MarketInfoProvider) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + bybitapi "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + types "github.com/c9s/bbgo/pkg/types" + gomock "github.com/golang/mock/gomock" +) + +// MockMarketInfoProvider is a mock of MarketInfoProvider interface. +type MockMarketInfoProvider struct { + ctrl *gomock.Controller + recorder *MockMarketInfoProviderMockRecorder +} + +// MockMarketInfoProviderMockRecorder is the mock recorder for MockMarketInfoProvider. +type MockMarketInfoProviderMockRecorder struct { + mock *MockMarketInfoProvider +} + +// NewMockMarketInfoProvider creates a new mock instance. +func NewMockMarketInfoProvider(ctrl *gomock.Controller) *MockMarketInfoProvider { + mock := &MockMarketInfoProvider{ctrl: ctrl} + mock.recorder = &MockMarketInfoProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMarketInfoProvider) EXPECT() *MockMarketInfoProviderMockRecorder { + return m.recorder +} + +// GetFeeRates mocks base method. +func (m *MockMarketInfoProvider) GetFeeRates(arg0 context.Context) (bybitapi.FeeRates, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFeeRates", arg0) + ret0, _ := ret[0].(bybitapi.FeeRates) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFeeRates indicates an expected call of GetFeeRates. +func (mr *MockMarketInfoProviderMockRecorder) GetFeeRates(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeeRates", reflect.TypeOf((*MockMarketInfoProvider)(nil).GetFeeRates), arg0) +} + +// QueryMarkets mocks base method. +func (m *MockMarketInfoProvider) QueryMarkets(arg0 context.Context) (types.MarketMap, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryMarkets", arg0) + ret0, _ := ret[0].(types.MarketMap) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryMarkets indicates an expected call of QueryMarkets. +func (mr *MockMarketInfoProviderMockRecorder) QueryMarkets(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryMarkets", reflect.TypeOf((*MockMarketInfoProvider)(nil).QueryMarkets), arg0) +} diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 1fe90298db..571e10af61 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -27,31 +27,43 @@ var ( wsAuthRequest = 10 * time.Second ) +//go:generate mockgen -destination=mocks/stream.go -package=mocks . MarketInfoProvider +type MarketInfoProvider interface { + GetFeeRates(ctx context.Context) (bybitapi.FeeRates, error) + QueryMarkets(ctx context.Context) (types.MarketMap, error) +} + //go:generate callbackgen -type Stream type Stream struct { - key, secret string types.StandardStream + key, secret string + marketProvider MarketInfoProvider + // TODO: update the fee rate at 7:00 am UTC; rotation required. + symbolFeeDetails map[string]*symbolFeeDetail + bookEventCallbacks []func(e BookEvent) walletEventCallbacks []func(e []bybitapi.WalletBalances) kLineEventCallbacks []func(e KLineEvent) orderEventCallbacks []func(e []OrderEvent) } -func NewStream(key, secret string) *Stream { +func NewStream(key, secret string, marketProvider MarketInfoProvider) *Stream { stream := &Stream{ StandardStream: types.NewStandardStream(), // pragma: allowlist nextline secret - key: key, - secret: secret, + key: key, + secret: secret, + marketProvider: marketProvider, } stream.SetEndpointCreator(stream.createEndpoint) stream.SetParser(stream.parseWebSocketEvent) stream.SetDispatcher(stream.dispatchEvent) stream.SetHeartBeat(stream.ping) - + stream.SetBeforeConnect(stream.getAllFeeRates) stream.OnConnect(stream.handlerConnect) + stream.OnBookEvent(stream.handleBookEvent) stream.OnKLineEvent(stream.handleKLineEvent) stream.OnWalletEvent(stream.handleWalletEvent) @@ -307,3 +319,53 @@ func (s *Stream) handleKLineEvent(klineEvent KLineEvent) { } } } + +type symbolFeeDetail struct { + bybitapi.FeeRate + + BaseCoin string + QuoteCoin string +} + +// getAllFeeRates retrieves all fee rates from the Bybit API and then fetches markets to ensure the base coin and quote coin +// are correct. +func (e *Stream) getAllFeeRates(ctx context.Context) error { + feeRates, err := e.marketProvider.GetFeeRates(ctx) + if err != nil { + return fmt.Errorf("failed to call get fee rates: %w", err) + } + + symbolMap := map[string]*symbolFeeDetail{} + for _, f := range feeRates.List { + if _, found := symbolMap[f.Symbol]; !found { + symbolMap[f.Symbol] = &symbolFeeDetail{FeeRate: f} + } + } + + mkts, err := e.marketProvider.QueryMarkets(ctx) + if err != nil { + return fmt.Errorf("failed to get markets: %w", err) + } + + // update base coin, quote coin into symbolFeeDetail + for _, mkt := range mkts { + feeRate, found := symbolMap[mkt.Symbol] + if !found { + continue + } + + feeRate.BaseCoin = mkt.BaseCurrency + feeRate.QuoteCoin = mkt.QuoteCurrency + } + + // remove trading pairs that are not present in spot market. + for k, v := range symbolMap { + if len(v.BaseCoin) == 0 || len(v.QuoteCoin) == 0 { + log.Debugf("related market not found: %s, skipping the associated trade", k) + delete(symbolMap, k) + } + } + + e.symbolFeeDetails = symbolMap + return nil +} diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go index 1c6a3badf0..08fc301f0e 100644 --- a/pkg/exchange/bybit/stream_test.go +++ b/pkg/exchange/bybit/stream_test.go @@ -8,8 +8,11 @@ import ( "strconv" "testing" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" + "github.com/c9s/bbgo/pkg/exchange/bybit/mocks" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/testutil" "github.com/c9s/bbgo/pkg/types" @@ -26,7 +29,9 @@ func getTestClientOrSkip(t *testing.T) *Stream { return nil } - return NewStream(key, secret) + exchange, err := New(key, secret) + assert.NoError(t, err) + return NewStream(key, secret, exchange) } func TestStream(t *testing.T) { @@ -312,3 +317,120 @@ func Test_convertSubscription(t *testing.T) { assert.Equal(t, "", res) }) } + +func TestStream_getFeeRate(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + unknownErr := errors.New("unknown err") + + t.Run("succeeds", func(t *testing.T) { + mockMarketProvider := mocks.NewMockMarketInfoProvider(mockCtrl) + s := &Stream{ + marketProvider: mockMarketProvider, + } + + ctx := context.Background() + feeRates := bybitapi.FeeRates{ + List: []bybitapi.FeeRate{ + { + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.001), + }, + { + Symbol: "ETHUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.001), + }, + { + Symbol: "OPTIONCOIN", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.001), + }, + }, + } + + mkts := types.MarketMap{ + "BTCUSDT": types.Market{ + Symbol: "BTCUSDT", + QuoteCurrency: "USDT", + BaseCurrency: "BTC", + }, + "ETHUSDT": types.Market{ + Symbol: "ETHUSDT", + QuoteCurrency: "USDT", + BaseCurrency: "ETH", + }, + } + + mockMarketProvider.EXPECT().GetFeeRates(ctx).Return(feeRates, nil).Times(1) + mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(mkts, nil).Times(1) + + expFeeRates := map[string]*symbolFeeDetail{ + "BTCUSDT": { + FeeRate: feeRates.List[0], + BaseCoin: "BTC", + QuoteCoin: "USDT", + }, + "ETHUSDT": { + FeeRate: feeRates.List[1], + BaseCoin: "ETH", + QuoteCoin: "USDT", + }, + } + err := s.getFeeRate(ctx) + assert.NoError(t, err) + assert.Equal(t, expFeeRates, s.symbolFeeDetails) + }) + + t.Run("failed to query markets", func(t *testing.T) { + mockMarketProvider := mocks.NewMockMarketInfoProvider(mockCtrl) + s := &Stream{ + marketProvider: mockMarketProvider, + } + + ctx := context.Background() + feeRates := bybitapi.FeeRates{ + List: []bybitapi.FeeRate{ + { + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.001), + }, + { + Symbol: "ETHUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.001), + }, + { + Symbol: "OPTIONCOIN", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.001), + }, + }, + } + + mockMarketProvider.EXPECT().GetFeeRates(ctx).Return(feeRates, nil).Times(1) + mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(nil, unknownErr).Times(1) + + err := s.getFeeRate(ctx) + assert.Equal(t, fmt.Errorf("failed to get markets: %w", unknownErr), err) + assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails) + }) + + t.Run("failed to get fee rates", func(t *testing.T) { + mockMarketProvider := mocks.NewMockMarketInfoProvider(mockCtrl) + s := &Stream{ + marketProvider: mockMarketProvider, + } + + ctx := context.Background() + + mockMarketProvider.EXPECT().GetFeeRates(ctx).Return(bybitapi.FeeRates{}, unknownErr).Times(1) + + err := s.getFeeRate(ctx) + assert.Equal(t, fmt.Errorf("failed to call get fee rates: %w", unknownErr), err) + assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails) + }) +} diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go index 22aec43bb5..c7f41d3e68 100644 --- a/pkg/exchange/bybit/types_test.go +++ b/pkg/exchange/bybit/types_test.go @@ -2,17 +2,20 @@ package bybit import ( "fmt" + "strconv" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) func Test_parseWebSocketEvent(t *testing.T) { t.Run("[public] PingEvent without req id", func(t *testing.T) { - s := NewStream("", "") + s := NewStream("", "", nil) msg := `{"success":true,"ret_msg":"pong","conn_id":"a806f6c4-3608-4b6d-a225-9f5da975bc44","op":"ping"}` raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) @@ -33,7 +36,7 @@ func Test_parseWebSocketEvent(t *testing.T) { }) t.Run("[public] PingEvent with req id", func(t *testing.T) { - s := NewStream("", "") + s := NewStream("", "", nil) msg := `{"success":true,"ret_msg":"pong","conn_id":"a806f6c4-3608-4b6d-a225-9f5da975bc44","req_id":"b26704da-f5af-44c2-bdf7-935d6739e1a0","op":"ping"}` raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) @@ -55,7 +58,7 @@ func Test_parseWebSocketEvent(t *testing.T) { }) t.Run("[private] PingEvent without req id", func(t *testing.T) { - s := NewStream("", "") + s := NewStream("", "", nil) msg := `{"op":"pong","args":["1690884539181"],"conn_id":"civn4p1dcjmtvb69ome0-yrt1"}` raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) @@ -75,7 +78,7 @@ func Test_parseWebSocketEvent(t *testing.T) { }) t.Run("[private] PingEvent with req id", func(t *testing.T) { - s := NewStream("", "") + s := NewStream("", "", nil) msg := `{"req_id":"78d36b57-a142-47b7-9143-5843df77d44d","op":"pong","args":["1690884539181"],"conn_id":"civn4p1dcjmtvb69ome0-yrt1"}` raw, err := s.parseWebSocketEvent([]byte(msg)) assert.NoError(t, err) From 54e7065d8aa152e8b9c59ded45c3ccb0ec832f11 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 10 Aug 2023 15:13:09 +0800 Subject: [PATCH 1347/1392] pkg/exchange: implement trade event --- pkg/exchange/bybit/mocks/stream.go | 12 +- pkg/exchange/bybit/stream.go | 32 +- pkg/exchange/bybit/stream_callbacks.go | 10 + pkg/exchange/bybit/stream_test.go | 23 +- pkg/exchange/bybit/types.go | 145 +++++++++ pkg/exchange/bybit/types_test.go | 392 +++++++++++++++++++++++++ 6 files changed, 600 insertions(+), 14 deletions(-) diff --git a/pkg/exchange/bybit/mocks/stream.go b/pkg/exchange/bybit/mocks/stream.go index df6c225fef..9d20878e85 100644 --- a/pkg/exchange/bybit/mocks/stream.go +++ b/pkg/exchange/bybit/mocks/stream.go @@ -36,19 +36,19 @@ func (m *MockMarketInfoProvider) EXPECT() *MockMarketInfoProviderMockRecorder { return m.recorder } -// GetFeeRates mocks base method. -func (m *MockMarketInfoProvider) GetFeeRates(arg0 context.Context) (bybitapi.FeeRates, error) { +// GetAllFeeRates mocks base method. +func (m *MockMarketInfoProvider) GetAllFeeRates(arg0 context.Context) (bybitapi.FeeRates, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFeeRates", arg0) + ret := m.ctrl.Call(m, "GetAllFeeRates", arg0) ret0, _ := ret[0].(bybitapi.FeeRates) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetFeeRates indicates an expected call of GetFeeRates. -func (mr *MockMarketInfoProviderMockRecorder) GetFeeRates(arg0 interface{}) *gomock.Call { +// GetAllFeeRates indicates an expected call of GetAllFeeRates. +func (mr *MockMarketInfoProviderMockRecorder) GetAllFeeRates(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeeRates", reflect.TypeOf((*MockMarketInfoProvider)(nil).GetFeeRates), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllFeeRates", reflect.TypeOf((*MockMarketInfoProvider)(nil).GetAllFeeRates), arg0) } // QueryMarkets mocks base method. diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 571e10af61..2359030f9b 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -29,7 +29,7 @@ var ( //go:generate mockgen -destination=mocks/stream.go -package=mocks . MarketInfoProvider type MarketInfoProvider interface { - GetFeeRates(ctx context.Context) (bybitapi.FeeRates, error) + GetAllFeeRates(ctx context.Context) (bybitapi.FeeRates, error) QueryMarkets(ctx context.Context) (types.MarketMap, error) } @@ -46,6 +46,7 @@ type Stream struct { walletEventCallbacks []func(e []bybitapi.WalletBalances) kLineEventCallbacks []func(e KLineEvent) orderEventCallbacks []func(e []OrderEvent) + tradeEventCallbacks []func(e []TradeEvent) } func NewStream(key, secret string, marketProvider MarketInfoProvider) *Stream { @@ -68,6 +69,7 @@ func NewStream(key, secret string, marketProvider MarketInfoProvider) *Stream { stream.OnKLineEvent(stream.handleKLineEvent) stream.OnWalletEvent(stream.handleWalletEvent) stream.OnOrderEvent(stream.handleOrderEvent) + stream.OnTradeEvent(stream.handleTradeEvent) return stream } @@ -99,6 +101,10 @@ func (s *Stream) dispatchEvent(event interface{}) { case []OrderEvent: s.EmitOrderEvent(e) + + case []TradeEvent: + s.EmitTradeEvent(e) + } } @@ -149,6 +155,10 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { var orders []OrderEvent return orders, json.Unmarshal(e.WebSocketTopicEvent.Data, &orders) + case TopicTypeTrade: + var trades []TradeEvent + return trades, json.Unmarshal(e.WebSocketTopicEvent.Data, &trades) + } } @@ -237,6 +247,7 @@ func (s *Stream) handlerConnect() { Args: []string{ string(TopicTypeWallet), string(TopicTypeOrder), + string(TopicTypeTrade), }, }); err != nil { log.WithError(err).Error("failed to send subscription request") @@ -320,6 +331,23 @@ func (s *Stream) handleKLineEvent(klineEvent KLineEvent) { } } +func (s *Stream) handleTradeEvent(events []TradeEvent) { + for _, event := range events { + feeRate, found := s.symbolFeeDetails[event.Symbol] + if !found { + log.Warnf("unexpected symbol found, fee rate not supported, symbol: %s", event.Symbol) + continue + } + + gTrade, err := event.toGlobalTrade(*feeRate) + if err != nil { + log.WithError(err).Errorf("unable to convert: %+v", event) + continue + } + s.StandardStream.EmitTradeUpdate(*gTrade) + } +} + type symbolFeeDetail struct { bybitapi.FeeRate @@ -330,7 +358,7 @@ type symbolFeeDetail struct { // getAllFeeRates retrieves all fee rates from the Bybit API and then fetches markets to ensure the base coin and quote coin // are correct. func (e *Stream) getAllFeeRates(ctx context.Context) error { - feeRates, err := e.marketProvider.GetFeeRates(ctx) + feeRates, err := e.marketProvider.GetAllFeeRates(ctx) if err != nil { return fmt.Errorf("failed to call get fee rates: %w", err) } diff --git a/pkg/exchange/bybit/stream_callbacks.go b/pkg/exchange/bybit/stream_callbacks.go index 0c7df8042b..fcd1f68595 100644 --- a/pkg/exchange/bybit/stream_callbacks.go +++ b/pkg/exchange/bybit/stream_callbacks.go @@ -45,3 +45,13 @@ func (s *Stream) EmitOrderEvent(e []OrderEvent) { cb(e) } } + +func (s *Stream) OnTradeEvent(cb func(e []TradeEvent)) { + s.tradeEventCallbacks = append(s.tradeEventCallbacks, cb) +} + +func (s *Stream) EmitTradeEvent(e []TradeEvent) { + for _, cb := range s.tradeEventCallbacks { + cb(e) + } +} diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go index 08fc301f0e..195c6fea2f 100644 --- a/pkg/exchange/bybit/stream_test.go +++ b/pkg/exchange/bybit/stream_test.go @@ -99,6 +99,17 @@ func TestStream(t *testing.T) { c := make(chan struct{}) <-c }) + + t.Run("trade test", func(t *testing.T) { + err := s.Connect(context.Background()) + assert.NoError(t, err) + + s.OnTradeUpdate(func(trade types.Trade) { + t.Log("got update", trade) + }) + c := make(chan struct{}) + <-c + }) } func TestStream_parseWebSocketEvent(t *testing.T) { @@ -364,7 +375,7 @@ func TestStream_getFeeRate(t *testing.T) { }, } - mockMarketProvider.EXPECT().GetFeeRates(ctx).Return(feeRates, nil).Times(1) + mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(feeRates, nil).Times(1) mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(mkts, nil).Times(1) expFeeRates := map[string]*symbolFeeDetail{ @@ -379,7 +390,7 @@ func TestStream_getFeeRate(t *testing.T) { QuoteCoin: "USDT", }, } - err := s.getFeeRate(ctx) + err := s.getAllFeeRates(ctx) assert.NoError(t, err) assert.Equal(t, expFeeRates, s.symbolFeeDetails) }) @@ -411,10 +422,10 @@ func TestStream_getFeeRate(t *testing.T) { }, } - mockMarketProvider.EXPECT().GetFeeRates(ctx).Return(feeRates, nil).Times(1) + mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(feeRates, nil).Times(1) mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(nil, unknownErr).Times(1) - err := s.getFeeRate(ctx) + err := s.getAllFeeRates(ctx) assert.Equal(t, fmt.Errorf("failed to get markets: %w", unknownErr), err) assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails) }) @@ -427,9 +438,9 @@ func TestStream_getFeeRate(t *testing.T) { ctx := context.Background() - mockMarketProvider.EXPECT().GetFeeRates(ctx).Return(bybitapi.FeeRates{}, unknownErr).Times(1) + mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(bybitapi.FeeRates{}, unknownErr).Times(1) - err := s.getFeeRate(ctx) + err := s.getAllFeeRates(ctx) assert.Equal(t, fmt.Errorf("failed to call get fee rates: %w", unknownErr), err) assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails) }) diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index d817c8e7b5..6b62f6563e 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -3,6 +3,7 @@ package bybit import ( "encoding/json" "fmt" + "strconv" "strings" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" @@ -83,6 +84,7 @@ const ( TopicTypeWallet TopicType = "wallet" TopicTypeOrder TopicType = "order" TopicTypeKLine TopicType = "kline" + TopicTypeTrade TopicType = "execution" ) type DataType string @@ -210,3 +212,146 @@ func (k *KLine) toGlobalKLine(symbol string) (types.KLine, error) { Closed: k.Confirm, }, nil } + +type TradeEvent struct { + // linear and inverse order id format: 42f4f364-82e1-49d3-ad1d-cd8cf9aa308d (UUID format) + // spot: 1468264727470772736 (only numbers) + // we only use spot trading. + OrderId string `json:"orderId"` + OrderLinkId string `json:"orderLinkId"` + Category bybitapi.Category `json:"category"` + Symbol string `json:"symbol"` + ExecId string `json:"execId"` + ExecPrice fixedpoint.Value `json:"execPrice"` + ExecQty fixedpoint.Value `json:"execQty"` + + // Is maker order. true: maker, false: taker + IsMaker bool `json:"isMaker"` + // Paradigm block trade ID + BlockTradeId string `json:"blockTradeId"` + // Order type. Market,Limit + OrderType bybitapi.OrderType `json:"orderType"` + // Side. Buy,Sell + Side bybitapi.Side `json:"side"` + // Executed timestamp(ms) + ExecTime types.MillisecondTimestamp `json:"execTime"` + // Closed position size + ClosedSize fixedpoint.Value `json:"closedSize"` + + /* The following parameters do not support SPOT trading. */ + // Executed trading fee. You can get spot fee currency instruction here. Normal spot is not supported + ExecFee fixedpoint.Value `json:"execFee"` + // Executed type. Normal spot is not supported + ExecType string `json:"execType"` + // Executed order value. Normal spot is not supported + ExecValue fixedpoint.Value `json:"execValue"` + // Trading fee rate. Normal spot is not supported + FeeRate fixedpoint.Value `json:"feeRate"` + // The remaining qty not executed. Normal spot is not supported + LeavesQty fixedpoint.Value `json:"leavesQty"` + // Order price. Normal spot is not supported + OrderPrice fixedpoint.Value `json:"orderPrice"` + // Order qty. Normal spot is not supported + OrderQty fixedpoint.Value `json:"orderQty"` + // Stop order type. If the order is not stop order, any type is not returned. Normal spot is not supported + StopOrderType string `json:"stopOrderType"` + // Whether to borrow. Unified spot only. 0: false, 1: true. . Normal spot is not supported, always 0 + IsLeverage string `json:"isLeverage"` + // Implied volatility of mark price. Valid for option + MarkIv string `json:"markIv"` + // The mark price of the symbol when executing. Valid for option + MarkPrice fixedpoint.Value `json:"markPrice"` + // The index price of the symbol when executing. Valid for option + IndexPrice fixedpoint.Value `json:"indexPrice"` + // The underlying price of the symbol when executing. Valid for option + UnderlyingPrice fixedpoint.Value `json:"underlyingPrice"` + // Implied volatility. Valid for option + TradeIv string `json:"tradeIv"` +} + +func (t *TradeEvent) toGlobalTrade(symbolFee symbolFeeDetail) (*types.Trade, error) { + if t.Category != bybitapi.CategorySpot { + return nil, fmt.Errorf("unexected category: %s", t.Category) + } + + side, err := toGlobalSideType(t.Side) + if err != nil { + return nil, err + } + + orderIdNum, err := strconv.ParseUint(t.OrderId, 10, 64) + if err != nil { + return nil, fmt.Errorf("unexpected order id: %s, err: %w", t.OrderId, err) + } + + execIdNum, err := strconv.ParseUint(t.ExecId, 10, 64) + if err != nil { + return nil, fmt.Errorf("unexpected exec id: %s, err: %w", t.ExecId, err) + } + + trade := &types.Trade{ + ID: execIdNum, + OrderID: orderIdNum, + Exchange: types.ExchangeBybit, + Price: t.ExecPrice, + Quantity: t.ExecQty, + QuoteQuantity: t.ExecPrice.Mul(t.ExecQty), + Symbol: t.Symbol, + Side: side, + IsBuyer: side == types.SideTypeBuy, + IsMaker: t.IsMaker, + Time: types.Time(t.ExecTime), + Fee: fixedpoint.Zero, + FeeCurrency: "", + } + trade.FeeCurrency, trade.Fee = calculateFee(*t, symbolFee) + return trade, nil +} + +// CalculateFee given isMaker to get the fee currency and fee. +// https://bybit-exchange.github.io/docs/v5/enum#spot-fee-currency-instruction +// +// with the example of BTCUSDT: +// +// Is makerFeeRate positive? +// +// - TRUE +// Side = Buy -> base currency (BTC) +// Side = Sell -> quote currency (USDT) +// +// - FALSE +// IsMakerOrder = TRUE +// -> Side = Buy -> quote currency (USDT) +// -> Side = Sell -> base currency (BTC) +// +// IsMakerOrder = FALSE +// -> Side = Buy -> base currency (BTC) +// -> Side = Sell -> quote currency (USDT) +func calculateFee(t TradeEvent, feeDetail symbolFeeDetail) (string, fixedpoint.Value) { + if feeDetail.MakerFeeRate.Sign() > 0 || !t.IsMaker { + if t.Side == bybitapi.SideBuy { + return feeDetail.BaseCoin, baseCoinAsFee(t, feeDetail) + } + return feeDetail.QuoteCoin, quoteCoinAsFee(t, feeDetail) + } + + if t.Side == bybitapi.SideBuy { + return feeDetail.QuoteCoin, quoteCoinAsFee(t, feeDetail) + } + return feeDetail.BaseCoin, baseCoinAsFee(t, feeDetail) +} + +func baseCoinAsFee(t TradeEvent, feeDetail symbolFeeDetail) fixedpoint.Value { + if t.IsMaker { + return feeDetail.MakerFeeRate.Mul(t.ExecQty) + } + return feeDetail.TakerFeeRate.Mul(t.ExecQty) +} + +func quoteCoinAsFee(t TradeEvent, feeDetail symbolFeeDetail) fixedpoint.Value { + baseFee := t.ExecPrice.Mul(t.ExecQty) + if t.IsMaker { + return feeDetail.MakerFeeRate.Mul(baseFee) + } + return feeDetail.TakerFeeRate.Mul(baseFee) +} diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go index c7f41d3e68..d3ae97643d 100644 --- a/pkg/exchange/bybit/types_test.go +++ b/pkg/exchange/bybit/types_test.go @@ -460,3 +460,395 @@ func TestKLine_toGlobalKLine(t *testing.T) { assert.Equal(t, gKline, types.KLine{}) }) } + +func TestTradeEvent_toGlobalTrade(t *testing.T) { + /* + { + "category":"spot", + "symbol":"BTCUSDT", + "execFee":"", + "execId":"2100000000032905730", + "execPrice":"28829.7600", + "execQty":"0.002289", + "execType":"", + "execValue":"", + "isMaker":false, + "feeRate":"", + "tradeIv":"", + "markIv":"", + "blockTradeId":"", + "markPrice":"", + "indexPrice":"", + "underlyingPrice":"", + "leavesQty":"", + "orderId":"1482125285219500288", + "orderLinkId":"1691419101980", + "orderPrice":"", + "orderQty":"", + "orderType":"", + "stopOrderType":"", + "side":"Buy", + "execTime":"1691419102282", + "isLeverage":"0" + } + */ + t.Run("succeeds", func(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + qty := fixedpoint.NewFromFloat(0.002289) + price := fixedpoint.NewFromFloat(28829.7600) + timeNow := time.Now().Truncate(time.Second) + expTrade := &types.Trade{ + ID: 2100000000032905730, + OrderID: 1482125285219500288, + Exchange: types.ExchangeBybit, + Price: price, + Quantity: qty, + QuoteQuantity: qty.Mul(price), + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + IsMaker: false, + Time: types.Time(timeNow), + Fee: symbolFee.FeeRate.TakerFeeRate.Mul(qty), + FeeCurrency: "BTC", + } + tradeEvent := TradeEvent{ + OrderId: fmt.Sprintf("%d", expTrade.OrderID), + OrderLinkId: "1691419101980", + Category: "spot", + Symbol: fmt.Sprintf("%s", expTrade.Symbol), + ExecId: fmt.Sprintf("%d", expTrade.ID), + ExecPrice: expTrade.Price, + ExecQty: expTrade.Quantity, + IsMaker: false, + BlockTradeId: "", + OrderType: "", + Side: bybitapi.SideBuy, + ExecTime: types.MillisecondTimestamp(timeNow), + ClosedSize: fixedpoint.NewFromInt(0), + ExecFee: fixedpoint.NewFromInt(0), + ExecType: "", + ExecValue: fixedpoint.NewFromInt(0), + FeeRate: fixedpoint.NewFromInt(0), + LeavesQty: fixedpoint.NewFromInt(0), + OrderPrice: fixedpoint.NewFromInt(0), + OrderQty: fixedpoint.NewFromInt(0), + StopOrderType: "", + IsLeverage: "0", + MarkIv: "", + MarkPrice: fixedpoint.NewFromInt(0), + IndexPrice: fixedpoint.NewFromInt(0), + UnderlyingPrice: fixedpoint.NewFromInt(0), + TradeIv: "", + } + + actualTrade, err := tradeEvent.toGlobalTrade(symbolFee) + assert.NoError(t, err) + assert.Equal(t, expTrade, actualTrade) + }) + + t.Run("unexpected category", func(t *testing.T) { + tradeEvent := TradeEvent{ + Category: "test-spot", + } + + actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{}) + assert.Equal(t, fmt.Errorf("unexected category: %s", tradeEvent.Category), err) + assert.Nil(t, actualTrade) + }) + + t.Run("unexpected side", func(t *testing.T) { + tradeEvent := TradeEvent{ + Category: "spot", + Side: bybitapi.Side("BOTH"), + } + + actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{}) + assert.Equal(t, fmt.Errorf("unexpected side: BOTH"), err) + assert.Nil(t, actualTrade) + }) + + t.Run("unexpected order id", func(t *testing.T) { + tradeEvent := TradeEvent{ + Category: "spot", + Side: bybitapi.SideBuy, + OrderId: "ABCD3123", + } + + _, nerr := strconv.ParseUint(tradeEvent.OrderId, 10, 64) + actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{}) + assert.Equal(t, fmt.Errorf("unexpected order id: %s, err: %w", tradeEvent.OrderId, nerr), err) + assert.Nil(t, actualTrade) + }) + + t.Run("unexpected exec id", func(t *testing.T) { + tradeEvent := TradeEvent{ + Category: "spot", + Side: bybitapi.SideBuy, + OrderId: "3123", + ExecId: "ABC3123", + } + + _, nerr := strconv.ParseUint(tradeEvent.ExecId, 10, 64) + actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{}) + assert.Equal(t, fmt.Errorf("unexpected exec id: %s, err: %w", tradeEvent.ExecId, nerr), err) + assert.Nil(t, actualTrade) + }) +} + +func TestTradeEvent_CalculateFee(t *testing.T) { + t.Run("maker fee positive, maker, buyer", func(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + + qty := fixedpoint.NewFromFloat(0.010000) + price := fixedpoint.NewFromFloat(28830.8100) + trade := &TradeEvent{ + ExecPrice: price, + ExecQty: qty, + IsMaker: true, + Side: bybitapi.SideBuy, + } + + feeCurrency, fee := calculateFee(*trade, symbolFee) + assert.Equal(t, feeCurrency, "BTC") + assert.Equal(t, fee, qty.Mul(symbolFee.FeeRate.MakerFeeRate)) + }) + + t.Run("maker fee positive, maker, seller", func(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + + qty := fixedpoint.NewFromFloat(0.010000) + price := fixedpoint.NewFromFloat(28830.8099) + trade := &TradeEvent{ + ExecPrice: price, + ExecQty: qty, + IsMaker: true, + Side: bybitapi.SideSell, + } + + feeCurrency, fee := calculateFee(*trade, symbolFee) + assert.Equal(t, feeCurrency, "USDT") + assert.Equal(t, fee, qty.Mul(price).Mul(symbolFee.FeeRate.MakerFeeRate)) + }) + + t.Run("maker fee positive, taker, buyer", func(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + + qty := fixedpoint.NewFromFloat(0.010000) + price := fixedpoint.NewFromFloat(28830.8100) + trade := &TradeEvent{ + ExecPrice: price, + ExecQty: qty, + IsMaker: false, + Side: bybitapi.SideBuy, + } + + feeCurrency, fee := calculateFee(*trade, symbolFee) + assert.Equal(t, feeCurrency, "BTC") + assert.Equal(t, fee, qty.Mul(symbolFee.FeeRate.TakerFeeRate)) + }) + + t.Run("maker fee positive, taker, seller", func(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + + qty := fixedpoint.NewFromFloat(0.010000) + price := fixedpoint.NewFromFloat(28830.8099) + trade := &TradeEvent{ + ExecPrice: price, + ExecQty: qty, + IsMaker: false, + Side: bybitapi.SideSell, + } + + feeCurrency, fee := calculateFee(*trade, symbolFee) + assert.Equal(t, feeCurrency, "USDT") + assert.Equal(t, fee, qty.Mul(price).Mul(symbolFee.FeeRate.TakerFeeRate)) + }) + + t.Run("maker fee negative, maker, buyer", func(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(-0.001), + MakerFeeRate: fixedpoint.NewFromFloat(-0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + + qty := fixedpoint.NewFromFloat(0.002289) + price := fixedpoint.NewFromFloat(28829.7600) + trade := &TradeEvent{ + ExecPrice: price, + ExecQty: qty, + IsMaker: true, + Side: bybitapi.SideBuy, + } + + feeCurrency, fee := calculateFee(*trade, symbolFee) + assert.Equal(t, feeCurrency, "USDT") + assert.Equal(t, fee, qty.Mul(price).Mul(symbolFee.FeeRate.MakerFeeRate)) + }) + + t.Run("maker fee negative, maker, seller", func(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(-0.001), + MakerFeeRate: fixedpoint.NewFromFloat(-0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + + qty := fixedpoint.NewFromFloat(0.002289) + price := fixedpoint.NewFromFloat(28829.7600) + trade := &TradeEvent{ + ExecPrice: price, + ExecQty: qty, + IsMaker: true, + Side: bybitapi.SideSell, + } + + feeCurrency, fee := calculateFee(*trade, symbolFee) + assert.Equal(t, feeCurrency, "BTC") + assert.Equal(t, fee, qty.Mul(symbolFee.FeeRate.MakerFeeRate)) + }) + + t.Run("maker fee negative, taker, buyer", func(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(-0.001), + MakerFeeRate: fixedpoint.NewFromFloat(-0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + + qty := fixedpoint.NewFromFloat(0.002289) + price := fixedpoint.NewFromFloat(28829.7600) + trade := &TradeEvent{ + ExecPrice: price, + ExecQty: qty, + IsMaker: false, + Side: bybitapi.SideBuy, + } + + feeCurrency, fee := calculateFee(*trade, symbolFee) + assert.Equal(t, feeCurrency, "BTC") + assert.Equal(t, fee, qty.Mul(symbolFee.FeeRate.TakerFeeRate)) + }) + + t.Run("maker fee negative, taker, seller", func(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(-0.001), + MakerFeeRate: fixedpoint.NewFromFloat(-0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + + qty := fixedpoint.NewFromFloat(0.002289) + price := fixedpoint.NewFromFloat(28829.7600) + trade := &TradeEvent{ + ExecPrice: price, + ExecQty: qty, + IsMaker: false, + Side: bybitapi.SideSell, + } + + feeCurrency, fee := calculateFee(*trade, symbolFee) + assert.Equal(t, feeCurrency, "USDT") + assert.Equal(t, fee, qty.Mul(price).Mul(symbolFee.FeeRate.TakerFeeRate)) + }) + +} + +func TestTradeEvent_baseCoinAsFee(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + qty := fixedpoint.NewFromFloat(0.002289) + price := fixedpoint.NewFromFloat(28829.7600) + trade := &TradeEvent{ + ExecPrice: price, + ExecQty: qty, + IsMaker: false, + } + assert.Equal(t, symbolFee.FeeRate.TakerFeeRate.Mul(qty), baseCoinAsFee(*trade, symbolFee)) + + trade.IsMaker = true + assert.Equal(t, symbolFee.FeeRate.MakerFeeRate.Mul(qty), baseCoinAsFee(*trade, symbolFee)) +} + +func TestTradeEvent_quoteCoinAsFee(t *testing.T) { + symbolFee := symbolFeeDetail{ + FeeRate: bybitapi.FeeRate{ + Symbol: "BTCUSDT", + TakerFeeRate: fixedpoint.NewFromFloat(0.001), + MakerFeeRate: fixedpoint.NewFromFloat(0.002), + }, + BaseCoin: "BTC", + QuoteCoin: "USDT", + } + qty := fixedpoint.NewFromFloat(0.002289) + price := fixedpoint.NewFromFloat(28829.7600) + trade := &TradeEvent{ + ExecPrice: price, + ExecQty: qty, + IsMaker: false, + } + assert.Equal(t, symbolFee.FeeRate.TakerFeeRate.Mul(qty.Mul(price)), quoteCoinAsFee(*trade, symbolFee)) + + trade.IsMaker = true + assert.Equal(t, symbolFee.FeeRate.MakerFeeRate.Mul(qty.Mul(price)), quoteCoinAsFee(*trade, symbolFee)) +} From ea5b45bfe44c3389cf418d004b0ded74edf712a6 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Fri, 11 Aug 2023 09:28:58 +0800 Subject: [PATCH 1348/1392] queryOrder() and test for it --- go.sum | 1 - pkg/exchange/okex/convert.go | 57 ++++++++++++++++++------ pkg/exchange/okex/exchange.go | 31 +++---------- pkg/exchange/okex/okexapi/client.go | 13 +++--- pkg/exchange/okex/okexapi/client_test.go | 24 +++++----- pkg/exchange/okex/query_order_test.go | 36 +++++++++++++++ 6 files changed, 106 insertions(+), 56 deletions(-) create mode 100644 pkg/exchange/okex/query_order_test.go diff --git a/go.sum b/go.sum index b39b946630..cdc1af5feb 100644 --- a/go.sum +++ b/go.sum @@ -1000,7 +1000,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20190226202314-149afe6ec0b6/go.mod h1:jevfED4GnIEnJrWW55YmY9DMhajHcnkqVnEXmEtMyNI= gonum.org/v1/gonum v0.0.0-20190902003836-43865b531bee/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 2977c12c21..175dcf731b 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -2,6 +2,7 @@ package okex import ( "fmt" + "hash/fnv" "regexp" "strconv" "strings" @@ -172,10 +173,7 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) side := types.SideType(strings.ToUpper(string(orderDetail.Side))) - orderType, err := toGlobalOrderType(orderDetail.OrderType) - if err != nil { - return orders, err - } + orderType := toGlobalOrderType(orderDetail.OrderType) timeInForce := types.TimeInForceGTC switch orderDetail.OrderType { @@ -186,10 +184,7 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) } - orderStatus, err := toGlobalOrderStatus(orderDetail.State) - if err != nil { - return orders, err - } + orderStatus := toGlobalOrderStatus(orderDetail.State) isWorking := false switch orderStatus { @@ -258,16 +253,21 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { func toGlobalOrderType(orderType okexapi.OrderType) types.OrderType { switch orderType { - case okexapi.OrderTypeMarket: + case okexapi.OrderTypeMarket, okexapi.OrderTypeMarketMakerProtection: return types.OrderTypeMarket + case okexapi.OrderTypeLimit: return types.OrderTypeLimit + case okexapi.OrderTypePostOnly: return types.OrderTypeLimitMaker - case okexapi.OrderTypeFOK: - return types.OrderTypeFillOrKill - case okexapi.OrderTypeIOC: - return types.OrderTypeIOC + + case okexapi.OrderTypeIOC, okexapi.OrderTypeFOK: + return types.OrderTypeMarket + + case okexapi.OrderTypeMarektMakerProtectionPostOnly: + return types.OrderTypeLimitMaker + default: log.Errorf("unsupported order type: %v", orderType) return "" @@ -280,3 +280,34 @@ func toLocalInterval(src string) string { return strings.ToUpper(w) }) } + +func hashStringID(s string) uint64 { + h := fnv.New64a() + h.Write([]byte(s)) + return h.Sum64() +} + +func toGlobalOrder(okexOrder *okexapi.OrderDetails, isMargin bool) (*types.Order, error) { + timeInForce := types.TimeInForceFOK + if okexOrder.OrderType == okexapi.OrderTypeIOC { + timeInForce = types.TimeInForceIOC + } + return &types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: okexOrder.ClientOrderID, + Symbol: okexOrder.InstrumentID, + Side: types.SideType(okexOrder.Side), + Type: toGlobalOrderType(okexOrder.OrderType), + Quantity: okexOrder.Quantity, + Price: okexOrder.Price, + TimeInForce: types.TimeInForce(timeInForce), + }, + Exchange: types.ExchangeOKEx, + OrderID: hashStringID(okexOrder.OrderID), + Status: toGlobalOrderStatus(okexOrder.State), + ExecutedQuantity: okexOrder.FilledQuantity, + CreationTime: types.Time(okexOrder.CreationTime), + UpdateTime: types.Time(okexOrder.UpdateTime), + IsMargin: isMargin, + }, nil +} diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 1214ed5276..02b48af646 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -351,37 +351,18 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O req.InstrumentID(q.Symbol). OrderID(q.OrderID). ClientOrderID(q.ClientOrderID) - orderID, err := strconv.ParseInt(q.OrderID, 10, 64) - if err != nil { - return nil, err - } var order *okexapi.OrderDetails - order, err = req.Do(ctx) + order, err := req.Do(ctx) if err != nil { return nil, err } - timeInForce := types.TimeInForceFOK - if order.OrderType == okexapi.OrderTypeIOC { - timeInForce = types.TimeInForceIOC + isMargin := false + if order.InstrumentType == string(okexapi.InstrumentTypeMARGIN) { + isMargin = true } - return &types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: order.ClientOrderID, - Symbol: order.InstrumentID, - Side: types.SideType(order.Side), - Type: toGlobalOrderType(order.OrderType), - Quantity: order.Quantity, - Price: order.Price, - TimeInForce: types.TimeInForce(timeInForce), - }, - Exchange: types.ExchangeOKEx, - OrderID: uint64(orderID), - Status: toGlobalOrderStatus(order.State), - ExecutedQuantity: order.FilledQuantity, - CreationTime: types.Time(order.CreationTime), - UpdateTime: types.Time(order.UpdateTime), - }, nil + + return toGlobalOrder(order, isMargin) } diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index 626e841603..1138fd41af 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -33,11 +33,13 @@ const ( type OrderType string const ( - OrderTypeMarket OrderType = "market" - OrderTypeLimit OrderType = "limit" - OrderTypePostOnly OrderType = "post_only" - OrderTypeFOK OrderType = "fok" - OrderTypeIOC OrderType = "ioc" + OrderTypeMarket OrderType = "market" + OrderTypeLimit OrderType = "limit" + OrderTypePostOnly OrderType = "post_only" + OrderTypeFOK OrderType = "fok" + OrderTypeIOC OrderType = "ioc" + OrderTypeMarketMakerProtection OrderType = "mmp" + OrderTypeMarektMakerProtectionPostOnly OrderType = "mmp_and_post_only" ) type InstrumentType string @@ -47,6 +49,7 @@ const ( InstrumentTypeSwap InstrumentType = "SWAP" InstrumentTypeFutures InstrumentType = "FUTURES" InstrumentTypeOption InstrumentType = "OPTION" + InstrumentTypeMARGIN InstrumentType = "MARGIN" ) type OrderState string diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index 04fc69e1d6..0f670d3348 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -63,16 +63,16 @@ func TestClient_PlaceOrderRequest(t *testing.T) { req := srv.NewPlaceOrderRequest() order, err := req. - InstrumentID("XTZ-BTC"). + InstrumentID("BTC-USDT"). TradeMode("cash"). - Side(SideTypeSell). + Side(SideTypeBuy). OrderType(OrderTypeLimit). - Price("0.001"). - Quantity("0.01"). + Price("15000"). + Quantity("0.0001"). Do(ctx) assert.NoError(t, err) assert.NotEmpty(t, order) - t.Logf("order: %+v", order) // Right now account has no money + t.Logf("place order: %+v", order) } func TestClient_GetPendingOrderRequest(t *testing.T) { @@ -83,12 +83,12 @@ func TestClient_GetPendingOrderRequest(t *testing.T) { odr_type := []string{string(OrderTypeLimit), string(OrderTypeIOC)} pending_order, err := req. - InstrumentID("XTZ-BTC"). + InstrumentID("BTC-USDT"). OrderTypes(odr_type). Do(ctx) assert.NoError(t, err) - assert.Empty(t, pending_order) - t.Logf("order: %+v", pending_order) + assert.NotEmpty(t, pending_order) + t.Logf("pending order: %+v", pending_order) } func TestClient_GetOrderDetailsRequest(t *testing.T) { @@ -99,9 +99,9 @@ func TestClient_GetOrderDetailsRequest(t *testing.T) { orderDetail, err := req. InstrumentID("BTC-USDT"). - OrderID("xxx-test-order-id"). + OrderID("609869603774656544"). Do(ctx) - assert.Error(t, err) // Right now account has no orders - assert.Empty(t, orderDetail) - t.Logf("err: %+v", err) + assert.NoError(t, err) + assert.NotEmpty(t, orderDetail) + t.Logf("order detail: %+v", orderDetail) } diff --git a/pkg/exchange/okex/query_order_test.go b/pkg/exchange/okex/query_order_test.go new file mode 100644 index 0000000000..3c32da40cf --- /dev/null +++ b/pkg/exchange/okex/query_order_test.go @@ -0,0 +1,36 @@ +package okex + +import ( + "context" + "os" + "testing" + + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +func Test_QueryOrder(t *testing.T) { + key := os.Getenv("OKEX_API_KEY") + secret := os.Getenv("OKEX_API_SECRET") + passphrase := os.Getenv("OKEX_API_PASSPHRASE") + if len(key) == 0 && len(secret) == 0 { + t.Skip("api key/secret are not configured") + return + } + if len(passphrase) == 0 { + t.Skip("passphrase are not configured") + return + } + + e := New(key, secret, passphrase) + + queryOrder := types.OrderQuery{ + Symbol: "BTC-USDT", + OrderID: "609869603774656544", + } + orderDetail, err := e.QueryOrder(context.Background(), queryOrder) + if assert.NoError(t, err) { + assert.NotEmpty(t, orderDetail) + } + t.Logf("order detail: %+v", orderDetail) +} From a74562ed31b0190d637aea043705d5a935714a63 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 11 Aug 2023 13:16:53 +0800 Subject: [PATCH 1349/1392] improve/profitStatsTracker: Add a parameter for window to sum up trades --- config/supertrend.yaml | 1 + pkg/report/profit_report.go | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index fe427a9b6e..e9b182aeca 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -119,5 +119,6 @@ exchangeStrategies: accumulatedProfitReport: profitMAWindow: 60 shortTermProfitWindow: 14 + accumulateTradeWindow: 30 tsvReportPath: res.tsv trackParameters: false diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index 8861117498..065a425011 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -25,6 +25,9 @@ type AccumulatedProfitReport struct { types.IntervalWindow + // ProfitMAWindow Accumulated profit SMA window + AccumulateTradeWindow int `json:"accumulateTradeWindow"` + // Accumulated profit accumulatedProfit fixedpoint.Value accumulatedProfitPerInterval *types.Float64Series @@ -119,7 +122,7 @@ func (r *AccumulatedProfitReport) CsvHeader() []string { "accumulatedFee", "winRatio", "profitFactor", - fmt.Sprintf("%s%d Trades", r.Interval, r.Window), + fmt.Sprintf("%s%d Trades", r.Interval, r.AccumulateTradeWindow), } for i := 0; i < len(r.strategyParameters); i++ { @@ -143,7 +146,7 @@ func (r *AccumulatedProfitReport) CsvRecords() [][]string { strconv.FormatFloat(r.accumulatedFeePerInterval.Last(i), 'f', 4, 64), strconv.FormatFloat(r.winRatioPerInterval.Last(i), 'f', 4, 64), strconv.FormatFloat(r.profitFactorPerInterval.Last(i), 'f', 4, 64), - strconv.FormatFloat(r.accumulatedTradesPerInterval.Last(i), 'f', 4, 64), + strconv.FormatFloat(r.accumulatedTradesPerInterval.Last(i)-r.accumulatedTradesPerInterval.Last(i+r.AccumulateTradeWindow), 'f', 4, 64), } for j := 0; j < len(r.strategyParameters); j++ { values = append(values, r.strategyParameters[j][1]) From 255718a54a83747fba890f27e6602557c3a18191 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 11 Aug 2023 19:03:16 +0800 Subject: [PATCH 1350/1392] deposit2transfer: apply rate limiter on checkDeposits --- pkg/strategy/deposit2transfer/strategy.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index ac86739b0a..f27529d5d4 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -9,6 +9,7 @@ import ( "time" "github.com/sirupsen/logrus" + "golang.org/x/time/rate" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -115,10 +116,11 @@ func (s *Strategy) tickWatcher(ctx context.Context, interval time.Duration) { } func (s *Strategy) checkDeposits(ctx context.Context) { + accountLimiter := rate.NewLimiter(rate.Every(3*time.Second), 1) + for _, asset := range s.Assets { log.Infof("checking %s deposits...", asset) - account := s.session.Account succeededDeposits, err := s.scanDepositHistory(ctx, asset, 4*time.Hour) if err != nil { log.WithError(err).Errorf("unable to scan deposit history") @@ -133,6 +135,17 @@ func (s *Strategy) checkDeposits(ctx context.Context) { for _, d := range succeededDeposits { log.Infof("found succeeded deposit: %+v", d) + if err2 := accountLimiter.Wait(ctx); err2 != nil { + log.WithError(err2).Errorf("rate limiter error") + return + } + + account, err2 := s.session.UpdateAccount(ctx) + if err2 != nil { + log.WithError(err2).Errorf("unable to update account") + continue + } + bal, ok := account.Balance(d.Asset) if !ok { log.Errorf("unexpected error: %s balance not found", d.Asset) From 187429081ffd7a55b02cc36ec21c63cb159c431f Mon Sep 17 00:00:00 2001 From: Edwin Date: Mon, 14 Aug 2023 18:06:14 +0800 Subject: [PATCH 1351/1392] pkg/exchange: fix orderId json tag --- pkg/exchange/bybit/bybitapi/cancel_order_request.go | 2 +- .../bybit/bybitapi/cancel_order_request_requestgen.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/bybit/bybitapi/cancel_order_request.go b/pkg/exchange/bybit/bybitapi/cancel_order_request.go index 144a086239..4ff7cb0a59 100644 --- a/pkg/exchange/bybit/bybitapi/cancel_order_request.go +++ b/pkg/exchange/bybit/bybitapi/cancel_order_request.go @@ -21,7 +21,7 @@ type CancelOrderRequest struct { // User customised order ID. Either orderId or orderLinkId is required orderLinkId string `param:"orderLinkId"` - orderId *string `param:"orderLinkId"` + orderId *string `param:"orderId"` // orderFilter default type is Order // tpsl order type are not currently supported orderFilter *string `param:"timeInForce" validValues:"Order"` diff --git a/pkg/exchange/bybit/bybitapi/cancel_order_request_requestgen.go b/pkg/exchange/bybit/bybitapi/cancel_order_request_requestgen.go index 6bda981ded..ad168ded99 100644 --- a/pkg/exchange/bybit/bybitapi/cancel_order_request_requestgen.go +++ b/pkg/exchange/bybit/bybitapi/cancel_order_request_requestgen.go @@ -77,12 +77,12 @@ func (p *CancelOrderRequest) GetParameters() (map[string]interface{}, error) { // assign parameter of orderLinkId params["orderLinkId"] = orderLinkId - // check orderId field -> json key orderLinkId + // check orderId field -> json key orderId if p.orderId != nil { orderId := *p.orderId // assign parameter of orderId - params["orderLinkId"] = orderId + params["orderId"] = orderId } else { } // check orderFilter field -> json key timeInForce From ed47d5064a15cb1322adc1cfa14658301f1c3870 Mon Sep 17 00:00:00 2001 From: Edwin Date: Mon, 14 Aug 2023 18:13:24 +0800 Subject: [PATCH 1352/1392] pkg/exchange: fix the order id check for cancel order --- pkg/exchange/bybit/exchange.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 01ba110ff6..c16de73a4a 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -262,11 +262,17 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err for _, order := range orders { req := e.client.NewCancelOrderRequest() + reqId := "" switch { + // use the OrderID first, then the ClientOrderID + case order.OrderID > 0: + req.OrderId(order.UUID) + reqId = order.UUID + case len(order.ClientOrderID) != 0: req.OrderLinkId(order.ClientOrderID) - case len(order.UUID) != 0 && order.OrderID != 0: - req.OrderId(order.UUID) + reqId = order.ClientOrderID + default: errs = multierr.Append( errs, @@ -286,8 +292,10 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err errs = multierr.Append(errs, fmt.Errorf("failed to cancel order id: %s, err: %w", order.ClientOrderID, err)) continue } - if res.OrderId != order.UUID || res.OrderLinkId != order.ClientOrderID { - errs = multierr.Append(errs, fmt.Errorf("unexpected order id, resp: %#v, order: %#v", res, order)) + + // sanity check + if res.OrderId != reqId && res.OrderLinkId != reqId { + errs = multierr.Append(errs, fmt.Errorf("order id mismatch, exp: %s, respOrderId: %s, respOrderLinkId: %s", reqId, res.OrderId, res.OrderLinkId)) continue } } From 3207a8227c35ce8ecb157ad455f52bf4adfe6404 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 15 Aug 2023 11:43:43 +0800 Subject: [PATCH 1353/1392] pkg/exchange: add QueryOrder api --- ..._request.go => get_open_orders_request.go} | 0 .../get_order_histories_request_requestgen.go | 13 +++++++ .../bybitapi/get_order_history_request.go | 7 ++-- pkg/exchange/bybit/exchange.go | 34 +++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) rename pkg/exchange/bybit/bybitapi/{get_open_order_request.go => get_open_orders_request.go} (100%) diff --git a/pkg/exchange/bybit/bybitapi/get_open_order_request.go b/pkg/exchange/bybit/bybitapi/get_open_orders_request.go similarity index 100% rename from pkg/exchange/bybit/bybitapi/get_open_order_request.go rename to pkg/exchange/bybit/bybitapi/get_open_orders_request.go diff --git a/pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go index 4fafe108b6..479d9eff40 100644 --- a/pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go +++ b/pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go @@ -28,6 +28,11 @@ func (g *GetOrderHistoriesRequest) OrderId(orderId string) *GetOrderHistoriesReq return g } +func (g *GetOrderHistoriesRequest) OrderLinkId(orderLinkId string) *GetOrderHistoriesRequest { + g.orderLinkId = &orderLinkId + return g +} + func (g *GetOrderHistoriesRequest) OrderFilter(orderFilter string) *GetOrderHistoriesRequest { g.orderFilter = &orderFilter return g @@ -93,6 +98,14 @@ func (g *GetOrderHistoriesRequest) GetQueryParameters() (url.Values, error) { params["orderId"] = orderId } else { } + // check orderLinkId field -> json key orderLinkId + if g.orderLinkId != nil { + orderLinkId := *g.orderLinkId + + // assign parameter of orderLinkId + params["orderLinkId"] = orderLinkId + } else { + } // check orderFilter field -> json key orderFilter if g.orderFilter != nil { orderFilter := *g.orderFilter diff --git a/pkg/exchange/bybit/bybitapi/get_order_history_request.go b/pkg/exchange/bybit/bybitapi/get_order_history_request.go index 7d46c8eff1..7731bc6afe 100644 --- a/pkg/exchange/bybit/bybitapi/get_order_history_request.go +++ b/pkg/exchange/bybit/bybitapi/get_order_history_request.go @@ -9,14 +9,15 @@ import ( //go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result //go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate GetRequest -url "/v5/order/history" -type GetOrderHistoriesRequest -responseDataType .OpenOrdersResponse +//go:generate GetRequest -url "/v5/order/history" -type GetOrderHistoriesRequest -responseDataType .OrdersResponse type GetOrderHistoriesRequest struct { client requestgen.AuthenticatedAPIClient category Category `param:"category,query" validValues:"spot"` - symbol *string `param:"symbol,query"` - orderId *string `param:"orderId,query"` + symbol *string `param:"symbol,query"` + orderId *string `param:"orderId,query"` + orderLinkId *string `param:"orderLinkId,query"` // orderFilter supports 3 types of Order: // 1. active order, 2. StopOrder: conditional order, 3. tpslOrder: spot TP/SL order // Normal spot: return Order active order by default diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 01ba110ff6..94f0ce4e4d 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -2,6 +2,7 @@ package bybit import ( "context" + "errors" "fmt" "strconv" "time" @@ -183,6 +184,39 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ return orders, nil } +func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { + if len(q.OrderID) == 0 && len(q.ClientOrderID) == 0 { + return nil, errors.New("one of OrderID/ClientOrderID is required parameter") + } + + if len(q.OrderID) != 0 && len(q.ClientOrderID) != 0 { + return nil, errors.New("only accept one parameter of OrderID/ClientOrderID") + } + + req := e.client.NewGetOrderHistoriesRequest() + if len(q.Symbol) != 0 { + req.Symbol(q.Symbol) + } + + if len(q.OrderID) != 0 { + req.OrderId(q.OrderID) + } + + if len(q.ClientOrderID) != 0 { + req.OrderLinkId(q.ClientOrderID) + } + + res, err := req.Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to query order, queryConfig: %+v, err: %w", q, err) + } + if len(res.List) != 1 { + return nil, fmt.Errorf("unexpected order length, queryConfig: %+v", q) + } + + return toGlobalOrder(res.List[0]) +} + func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { if len(order.Market.Symbol) == 0 { return nil, fmt.Errorf("order.Market.Symbol is required: %+v", order) From e4ebe1cffd7bdf8a7cefa77c43d82ae83fd2ae58 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 15 Aug 2023 14:18:36 +0800 Subject: [PATCH 1354/1392] pkg/exchange: supprot queryOrderTrades --- pkg/exchange/bybit/exchange.go | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index 94f0ce4e4d..a382469360 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -46,6 +46,7 @@ var ( _ types.ExchangeMinimal = &Exchange{} _ types.ExchangeTradeService = &Exchange{} _ types.Exchange = &Exchange{} + _ types.ExchangeOrderQueryService = &Exchange{} ) type Exchange struct { @@ -217,6 +218,45 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O return toGlobalOrder(res.List[0]) } +func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) (trades []types.Trade, err error) { + if len(q.ClientOrderID) != 0 { + log.Warn("!!!BYBIT EXCHANGE API NOTICE!!! Bybit does not support searching for trades using OrderClientId.") + } + + if len(q.OrderID) == 0 { + return nil, errors.New("orderID is required parameter") + } + req := e.v3client.NewGetTradesRequest().OrderId(q.OrderID) + + if len(q.Symbol) != 0 { + req.Symbol(q.Symbol) + } + + if err := tradeRateLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("trade rate limiter wait error: %w", err) + } + response, err := req.Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to query order trades, err: %w", err) + } + + var errs error + for _, trade := range response.List { + res, err := v3ToGlobalTrade(trade) + if err != nil { + errs = multierr.Append(errs, err) + continue + } + trades = append(trades, *res) + } + + if errs != nil { + return nil, errs + } + + return trades, nil +} + func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { if len(order.Market.Symbol) == 0 { return nil, fmt.Errorf("order.Market.Symbol is required: %+v", order) From 672a878194d52875b80fd029641a673af9bca8a1 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Tue, 15 Aug 2023 14:26:27 +0800 Subject: [PATCH 1355/1392] update toGlobalOrder by referencing toGlobalOrders --- pkg/exchange/okex/convert.go | 147 +++++++++++++++-------------------- 1 file changed, 64 insertions(+), 83 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 175dcf731b..7a008a2c23 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -2,7 +2,6 @@ package okex import ( "fmt" - "hash/fnv" "regexp" "strconv" "strings" @@ -166,73 +165,36 @@ func toGlobalTrades(orderDetails []okexapi.OrderDetails) ([]types.Trade, error) func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) { var orders []types.Order for _, orderDetail := range orderDetails { - orderID, err := strconv.ParseInt(orderDetail.OrderID, 10, 64) - if err != nil { - return orders, err + isMargin := false + if orderDetail.InstrumentType == string(okexapi.InstrumentTypeMARGIN) { + isMargin = true } - side := types.SideType(strings.ToUpper(string(orderDetail.Side))) - - orderType := toGlobalOrderType(orderDetail.OrderType) - - timeInForce := types.TimeInForceGTC - switch orderDetail.OrderType { - case okexapi.OrderTypeFOK: - timeInForce = types.TimeInForceFOK - case okexapi.OrderTypeIOC: - timeInForce = types.TimeInForceIOC - - } - - orderStatus := toGlobalOrderStatus(orderDetail.State) - - isWorking := false - switch orderStatus { - case types.OrderStatusNew, types.OrderStatusPartiallyFilled: - isWorking = true - + o, err := toGlobalOrder(&orderDetail, isMargin) + if err != nil { + log.WithError(err).Error("order convert error") + } else { + orders = append(orders, *o) } - - orders = append(orders, types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: orderDetail.ClientOrderID, - Symbol: toGlobalSymbol(orderDetail.InstrumentID), - Side: side, - Type: orderType, - Price: orderDetail.Price, - Quantity: orderDetail.Quantity, - StopPrice: fixedpoint.Zero, // not supported yet - TimeInForce: timeInForce, - }, - Exchange: types.ExchangeOKEx, - OrderID: uint64(orderID), - Status: orderStatus, - ExecutedQuantity: orderDetail.FilledQuantity, - IsWorking: isWorking, - CreationTime: types.Time(orderDetail.CreationTime), - UpdateTime: types.Time(orderDetail.UpdateTime), - IsMargin: false, - IsIsolated: false, - }) } return orders, nil } -func toGlobalOrderStatus(state okexapi.OrderState) types.OrderStatus { +func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) { switch state { case okexapi.OrderStateCanceled: - return types.OrderStatusCanceled + return types.OrderStatusCanceled, nil case okexapi.OrderStateLive: - return types.OrderStatusNew + return types.OrderStatusNew, nil case okexapi.OrderStatePartiallyFilled: - return types.OrderStatusPartiallyFilled + return types.OrderStatusPartiallyFilled, nil case okexapi.OrderStateFilled: - return types.OrderStatusFilled + return types.OrderStatusFilled, nil } - return types.OrderStatus(state) + return "", fmt.Errorf("unknown or unsupported okex order state: %s", state) } func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { @@ -251,27 +213,20 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType) } -func toGlobalOrderType(orderType okexapi.OrderType) types.OrderType { +func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { switch orderType { - case okexapi.OrderTypeMarket, okexapi.OrderTypeMarketMakerProtection: - return types.OrderTypeMarket + case okexapi.OrderTypeMarket: + return types.OrderTypeMarket, nil - case okexapi.OrderTypeLimit: - return types.OrderTypeLimit + case okexapi.OrderTypeLimit, okexapi.OrderTypeMarketMakerProtection: + return types.OrderTypeLimit, nil - case okexapi.OrderTypePostOnly: - return types.OrderTypeLimitMaker + case okexapi.OrderTypePostOnly, okexapi.OrderTypeIOC, okexapi.OrderTypeFOK, okexapi.OrderTypeMarektMakerProtectionPostOnly: + return types.OrderTypeLimitMaker, nil - case okexapi.OrderTypeIOC, okexapi.OrderTypeFOK: - return types.OrderTypeMarket - - case okexapi.OrderTypeMarektMakerProtectionPostOnly: - return types.OrderTypeLimitMaker - - default: - log.Errorf("unsupported order type: %v", orderType) - return "" } + + return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType) } func toLocalInterval(src string) string { @@ -281,33 +236,59 @@ func toLocalInterval(src string) string { }) } -func hashStringID(s string) uint64 { - h := fnv.New64a() - h.Write([]byte(s)) - return h.Sum64() -} - func toGlobalOrder(okexOrder *okexapi.OrderDetails, isMargin bool) (*types.Order, error) { - timeInForce := types.TimeInForceFOK - if okexOrder.OrderType == okexapi.OrderTypeIOC { + + orderID, err := strconv.ParseInt(okexOrder.OrderID, 10, 64) + if err != nil { + return &types.Order{}, err + } + + side := types.SideType(strings.ToUpper(string(okexOrder.Side))) + + orderType, err := toGlobalOrderType(okexOrder.OrderType) + if err != nil { + return &types.Order{}, err + } + + timeInForce := types.TimeInForceGTC + switch okexOrder.OrderType { + case okexapi.OrderTypeFOK: + timeInForce = types.TimeInForceFOK + case okexapi.OrderTypeIOC: timeInForce = types.TimeInForceIOC } + + orderStatus, err := toGlobalOrderStatus(okexOrder.State) + if err != nil { + return &types.Order{}, err + } + + isWorking := false + switch orderStatus { + case types.OrderStatusNew, types.OrderStatusPartiallyFilled: + isWorking = true + + } + return &types.Order{ SubmitOrder: types.SubmitOrder{ ClientOrderID: okexOrder.ClientOrderID, - Symbol: okexOrder.InstrumentID, - Side: types.SideType(okexOrder.Side), - Type: toGlobalOrderType(okexOrder.OrderType), - Quantity: okexOrder.Quantity, + Symbol: toGlobalSymbol(okexOrder.InstrumentID), + Side: side, + Type: orderType, Price: okexOrder.Price, - TimeInForce: types.TimeInForce(timeInForce), + Quantity: okexOrder.Quantity, + StopPrice: fixedpoint.Zero, // not supported yet + TimeInForce: timeInForce, }, Exchange: types.ExchangeOKEx, - OrderID: hashStringID(okexOrder.OrderID), - Status: toGlobalOrderStatus(okexOrder.State), + OrderID: uint64(orderID), + Status: orderStatus, ExecutedQuantity: okexOrder.FilledQuantity, + IsWorking: isWorking, CreationTime: types.Time(okexOrder.CreationTime), UpdateTime: types.Time(okexOrder.UpdateTime), IsMargin: isMargin, + IsIsolated: false, }, nil } From fb5cc41aeaab9e78d2219612d4602077e66b0704 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 15 Aug 2023 17:32:43 +0800 Subject: [PATCH 1356/1392] migrations: add bybit klines --- .../mysql/20230815173104_add_bybit_klines.sql | 10 +++++++ .../20230815173104_add_bybit_klines.sql | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 migrations/mysql/20230815173104_add_bybit_klines.sql create mode 100644 migrations/sqlite3/20230815173104_add_bybit_klines.sql diff --git a/migrations/mysql/20230815173104_add_bybit_klines.sql b/migrations/mysql/20230815173104_add_bybit_klines.sql new file mode 100644 index 0000000000..81f2c003ba --- /dev/null +++ b/migrations/mysql/20230815173104_add_bybit_klines.sql @@ -0,0 +1,10 @@ +-- +up +-- +begin +CREATE TABLE `bybit_klines` LIKE `binance_klines`; +-- +end + +-- +down + +-- +begin +DROP TABLE `bybit_klines`; +-- +end diff --git a/migrations/sqlite3/20230815173104_add_bybit_klines.sql b/migrations/sqlite3/20230815173104_add_bybit_klines.sql new file mode 100644 index 0000000000..7ad92fdac6 --- /dev/null +++ b/migrations/sqlite3/20230815173104_add_bybit_klines.sql @@ -0,0 +1,29 @@ +-- +up +-- +begin +CREATE TABLE `bybit_klines` +( + `gid` INTEGER PRIMARY KEY AUTOINCREMENT, + `exchange` VARCHAR(10) NOT NULL, + `start_time` DATETIME(3) NOT NULL, + `end_time` DATETIME(3) NOT NULL, + `interval` VARCHAR(3) NOT NULL, + `symbol` VARCHAR(7) NOT NULL, + `open` DECIMAL(16, 8) NOT NULL, + `high` DECIMAL(16, 8) NOT NULL, + `low` DECIMAL(16, 8) NOT NULL, + `close` DECIMAL(16, 8) NOT NULL DEFAULT 0.0, + `volume` DECIMAL(16, 8) NOT NULL DEFAULT 0.0, + `closed` BOOLEAN NOT NULL DEFAULT TRUE, + `last_trade_id` INT NOT NULL DEFAULT 0, + `num_trades` INT NOT NULL DEFAULT 0, + `quote_volume` DECIMAL NOT NULL DEFAULT 0.0, + `taker_buy_base_volume` DECIMAL NOT NULL DEFAULT 0.0, + `taker_buy_quote_volume` DECIMAL NOT NULL DEFAULT 0.0 +); +-- +end + +-- +down + +-- +begin +DROP TABLE bybit_klines; +-- +end From adf5805de5d4801fead14373cbf8d14a1268b3f1 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 15 Aug 2023 17:31:39 +0800 Subject: [PATCH 1357/1392] compile and update migration package --- .../mysql/20230815173104_add_bybit_klines.go | 34 +++++++++++++++++++ .../20230815173104_add_bybit_klines.go | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 pkg/migrations/mysql/20230815173104_add_bybit_klines.go create mode 100644 pkg/migrations/sqlite3/20230815173104_add_bybit_klines.go diff --git a/pkg/migrations/mysql/20230815173104_add_bybit_klines.go b/pkg/migrations/mysql/20230815173104_add_bybit_klines.go new file mode 100644 index 0000000000..dcaa29fa5b --- /dev/null +++ b/pkg/migrations/mysql/20230815173104_add_bybit_klines.go @@ -0,0 +1,34 @@ +package mysql + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upAddBybitKlines, downAddBybitKlines) + +} + +func upAddBybitKlines(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE TABLE `bybit_klines` LIKE `binance_klines`;") + if err != nil { + return err + } + + return err +} + +func downAddBybitKlines(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP TABLE `bybit_klines`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/sqlite3/20230815173104_add_bybit_klines.go b/pkg/migrations/sqlite3/20230815173104_add_bybit_klines.go new file mode 100644 index 0000000000..54afcd9ca6 --- /dev/null +++ b/pkg/migrations/sqlite3/20230815173104_add_bybit_klines.go @@ -0,0 +1,34 @@ +package sqlite3 + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upAddBybitKlines, downAddBybitKlines) + +} + +func upAddBybitKlines(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE TABLE `bybit_klines`\n(\n `gid` INTEGER PRIMARY KEY AUTOINCREMENT,\n `exchange` VARCHAR(10) NOT NULL,\n `start_time` DATETIME(3) NOT NULL,\n `end_time` DATETIME(3) NOT NULL,\n `interval` VARCHAR(3) NOT NULL,\n `symbol` VARCHAR(7) NOT NULL,\n `open` DECIMAL(16, 8) NOT NULL,\n `high` DECIMAL(16, 8) NOT NULL,\n `low` DECIMAL(16, 8) NOT NULL,\n `close` DECIMAL(16, 8) NOT NULL DEFAULT 0.0,\n `volume` DECIMAL(16, 8) NOT NULL DEFAULT 0.0,\n `closed` BOOLEAN NOT NULL DEFAULT TRUE,\n `last_trade_id` INT NOT NULL DEFAULT 0,\n `num_trades` INT NOT NULL DEFAULT 0,\n `quote_volume` DECIMAL NOT NULL DEFAULT 0.0,\n `taker_buy_base_volume` DECIMAL NOT NULL DEFAULT 0.0,\n `taker_buy_quote_volume` DECIMAL NOT NULL DEFAULT 0.0\n);") + if err != nil { + return err + } + + return err +} + +func downAddBybitKlines(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP TABLE bybit_klines;") + if err != nil { + return err + } + + return err +} From 252f4fbccc28c76a6e92bc82c25aa4e7e3031c92 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Aug 2023 12:02:18 +0800 Subject: [PATCH 1358/1392] deposit2transfer: call QuerySpotAccount for getting the spot balance --- pkg/exchange/max/exchange.go | 51 +++++++++++++++++++---- pkg/strategy/deposit2transfer/strategy.go | 31 ++++++++------ 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index d49eb19923..83b56f1edb 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -562,7 +562,7 @@ func (e *Exchange) getLaunchDate() (time.Time, error) { return time.Date(2018, time.June, 21, 0, 0, 0, 0, loc), nil } -func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { +func (e *Exchange) QuerySpotAccount(ctx context.Context) (*types.Account, error) { if err := e.accountQueryLimiter.Wait(ctx); err != nil { return nil, err } @@ -575,7 +575,6 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { // MAX returns the fee rate in the following format: // "maker_fee": 0.0005 -> 0.05% // "taker_fee": 0.0015 -> 0.15% - a := &types.Account{ AccountType: types.AccountTypeSpot, MarginLevel: fixedpoint.Zero, @@ -583,15 +582,48 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { TakerFeeRate: fixedpoint.NewFromFloat(vipLevel.Current.TakerFee), // 0.15% = 0.0015 } - balances, err := e.QueryAccountBalances(ctx) + balances, err := e.queryBalances(ctx, maxapi.WalletTypeSpot) if err != nil { return nil, err } a.UpdateBalances(balances) + return nil, nil +} + +func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { + if err := e.accountQueryLimiter.Wait(ctx); err != nil { + return nil, err + } + + vipLevel, err := e.client.NewGetVipLevelRequest().Do(ctx) + if err != nil { + return nil, err + } + + // MAX returns the fee rate in the following format: + // "maker_fee": 0.0005 -> 0.05% + // "taker_fee": 0.0015 -> 0.15% + + a := &types.Account{ + MarginLevel: fixedpoint.Zero, + MakerFeeRate: fixedpoint.NewFromFloat(vipLevel.Current.MakerFee), // 0.15% = 0.0015 + TakerFeeRate: fixedpoint.NewFromFloat(vipLevel.Current.TakerFee), // 0.15% = 0.0015 + } + if e.MarginSettings.IsMargin { a.AccountType = types.AccountTypeMargin + } else { + a.AccountType = types.AccountTypeSpot + } + balances, err := e.QueryAccountBalances(ctx) + if err != nil { + return nil, err + } + a.UpdateBalances(balances) + + if e.MarginSettings.IsMargin { req := e.v3client.NewGetMarginADRatioRequest() adRatio, err := req.Do(ctx) if err != nil { @@ -606,16 +638,21 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { } func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { - if err := e.accountQueryLimiter.Wait(ctx); err != nil { - return nil, err - } - walletType := maxapi.WalletTypeSpot if e.MarginSettings.IsMargin { walletType = maxapi.WalletTypeMargin } + return e.queryBalances(ctx, walletType) +} + +func (e *Exchange) queryBalances(ctx context.Context, walletType maxapi.WalletType) (types.BalanceMap, error) { + if err := e.accountQueryLimiter.Wait(ctx); err != nil { + return nil, err + } + req := e.v3client.NewGetWalletAccountsRequest(walletType) + accounts, err := req.Do(ctx) if err != nil { return nil, err diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index f27529d5d4..8be53b7013 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -20,6 +20,10 @@ type marginTransferService interface { TransferMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error } +type spotAccountQueryService interface { + QuerySpotAccount(ctx context.Context) (*types.Account, error) +} + const ID = "deposit2transfer" var log = logrus.WithField("strategy", ID) @@ -140,22 +144,23 @@ func (s *Strategy) checkDeposits(ctx context.Context) { return } - account, err2 := s.session.UpdateAccount(ctx) - if err2 != nil { - log.WithError(err2).Errorf("unable to update account") - continue - } + // we can't use the account from margin + amount := d.Amount + if service, ok := s.session.Exchange.(spotAccountQueryService); ok { + account, err2 := service.QuerySpotAccount(ctx) + if err2 != nil { + log.WithError(err2).Errorf("unable to update account") + continue + } - bal, ok := account.Balance(d.Asset) - if !ok { - log.Errorf("unexpected error: %s balance not found", d.Asset) - return + if bal, ok := account.Balance(d.Asset); ok { + log.Infof("%s balance: %+v", d.Asset, bal) + amount = fixedpoint.Min(bal.Available, amount) + } else { + log.Errorf("unexpected error: %s balance not found", d.Asset) + } } - log.Infof("%s balance: %+v", d.Asset, bal) - - amount := fixedpoint.Min(bal.Available, d.Amount) - bbgo.Notify("Found succeeded deposit %s %s, transferring %s %s into the margin account", d.Amount.String(), d.Asset, amount.String(), d.Asset) From 5cc09dfb9a5dbc090a8c17cf750e22e0750fabae Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Aug 2023 12:26:01 +0800 Subject: [PATCH 1359/1392] deposit2transfer: improve log format --- pkg/strategy/deposit2transfer/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index 8be53b7013..be59562aa4 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -154,7 +154,7 @@ func (s *Strategy) checkDeposits(ctx context.Context) { } if bal, ok := account.Balance(d.Asset); ok { - log.Infof("%s balance: %+v", d.Asset, bal) + log.Infof("spot account balance %s: %+v", d.Asset, bal) amount = fixedpoint.Min(bal.Available, amount) } else { log.Errorf("unexpected error: %s balance not found", d.Asset) From 6cfbb84bb5f77d7002c6fe62117a588180b21c6f Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 15 Aug 2023 11:03:21 +0800 Subject: [PATCH 1360/1392] pkg/exchange: fix order id check for the submitted orders --- pkg/exchange/bybit/exchange.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index c16de73a4a..a8abf0e1be 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -228,7 +228,9 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t if len(order.ClientOrderID) > maxOrderIdLen { return nil, fmt.Errorf("unexpected length of order id, got: %d", len(order.ClientOrderID)) } - req.OrderLinkId(order.ClientOrderID) + if len(order.ClientOrderID) > 0 { + req.OrderLinkId(order.ClientOrderID) + } if err := orderRateLimiter.Wait(ctx); err != nil { return nil, fmt.Errorf("place order rate limiter wait error: %w", err) @@ -238,11 +240,11 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t return nil, fmt.Errorf("failed to place order, order: %#v, err: %w", order, err) } - if len(res.OrderId) == 0 || res.OrderLinkId != order.ClientOrderID { + if len(res.OrderId) == 0 || (len(order.ClientOrderID) != 0 && res.OrderLinkId != order.ClientOrderID) { return nil, fmt.Errorf("unexpected order id, resp: %#v, order: %#v", res, order) } - ordersResp, err := e.client.NewGetOpenOrderRequest().OrderLinkId(res.OrderLinkId).Do(ctx) + ordersResp, err := e.client.NewGetOpenOrderRequest().OrderId(res.OrderId).Do(ctx) if err != nil { return nil, fmt.Errorf("failed to query order by client order id: %s, err: %w", res.OrderLinkId, err) } From dda3f25c6161c190f105c46ae8e3e2960ec579e7 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 17 Aug 2023 16:26:06 +0800 Subject: [PATCH 1361/1392] grid2,bbgo: refactor active order book and update order status when re-connected --- pkg/bbgo/activeorderbook.go | 67 ++++++++++++++++++++-------------- pkg/strategy/grid2/strategy.go | 35 ++++++++++++++++++ 2 files changed, 74 insertions(+), 28 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 8347d6aab7..fbd391a8da 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -244,33 +244,7 @@ func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) { return } - switch order.Status { - case types.OrderStatusFilled: - // make sure we have the order and we remove it - if b.Remove(order) { - b.EmitFilled(order) - } - b.C.Emit() - - case types.OrderStatusPartiallyFilled: - b.Update(order) - - case types.OrderStatusNew: - b.Update(order) - b.C.Emit() - - case types.OrderStatusCanceled, types.OrderStatusRejected: - if order.Status == types.OrderStatusCanceled { - b.EmitCanceled(order) - } - - log.Debugf("[ActiveOrderBook] order is %s, removing order %s", order.Status, order) - b.Remove(order) - b.C.Emit() - - default: - log.Warnf("unhandled order status: %s", order.Status) - } + b.Update(order) } func (b *ActiveOrderBook) Print() { @@ -288,7 +262,8 @@ func (b *ActiveOrderBook) Print() { } } -func (b *ActiveOrderBook) Update(orders ...types.Order) { +// update updates the order stores in the internal order storage +func (b *ActiveOrderBook) update(orders ...types.Order) { hasSymbol := len(b.Symbol) > 0 for _, order := range orders { if hasSymbol && b.Symbol != order.Symbol { @@ -299,6 +274,42 @@ func (b *ActiveOrderBook) Update(orders ...types.Order) { } } +// Update updates the order by the order status and emit the related events. +// When order is filled, the order will be removed from the internal order storage. +// When order is New or PartiallyFilled, the internal order will be updated according to the latest order update. +// When the order is cancelled, it will be removed from the internal order storage. +func (b *ActiveOrderBook) Update(order types.Order) { + switch order.Status { + case types.OrderStatusFilled: + // make sure we have the order and we remove it + if b.Remove(order) { + b.EmitFilled(order) + } + b.C.Emit() + + case types.OrderStatusPartiallyFilled: + b.update(order) + + case types.OrderStatusNew: + b.update(order) + b.C.Emit() + + case types.OrderStatusCanceled, types.OrderStatusRejected: + // TODO: note that orders transit to "canceled" may have partially filled + if order.Status == types.OrderStatusCanceled { + b.EmitCanceled(order) + } + + log.Debugf("[ActiveOrderBook] order is %s, removing order %s", order.Status, order) + b.Remove(order) + b.C.Emit() + + default: + log.Warnf("[ActiveOrderBook] unhandled order status: %s", order.Status) + } + +} + func (b *ActiveOrderBook) Add(orders ...types.Order) { hasSymbol := len(b.Symbol) > 0 diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 898868b196..1da04a3e01 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1951,6 +1951,10 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } }) + session.UserDataStream.OnConnect(func() { + s.handleConnect(ctx, session) + }) + // if TriggerPrice is zero, that means we need to open the grid when start up if s.TriggerPrice.IsZero() { // must call the openGrid method inside the OnStart callback because @@ -2112,3 +2116,34 @@ func (s *Strategy) newClientOrderID() string { } return "" } + +func (s *Strategy) handleConnect(ctx context.Context, session *bbgo.ExchangeSession) { + grid := s.getGrid() + if grid == nil { + return + } + + // TODO: move this logics into the active maker orders component, like activeOrders.Sync(ctx) + activeOrderBook := s.orderExecutor.ActiveMakerOrders() + activeOrders := activeOrderBook.Orders() + for _, o := range activeOrders { + + var updatedOrder *types.Order + + err := retry.GeneralBackoff(ctx, func() error { + var err error + updatedOrder, err = s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ + Symbol: o.Symbol, + OrderID: strconv.FormatUint(o.OrderID, 10), + }) + return err + }) + + if err != nil { + s.logger.WithError(err).Errorf("unable to query order") + return + } + + activeOrderBook.Update(*updatedOrder) + } +} From 2669c3a5dbf59bb7fdd3490ff467413472ff274b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 17 Aug 2023 16:28:42 +0800 Subject: [PATCH 1362/1392] bbgo: check order exists --- pkg/bbgo/activeorderbook.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index fbd391a8da..104c9f3497 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -233,17 +233,6 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange, } func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) { - hasSymbol := len(b.Symbol) > 0 - - if hasSymbol && order.Symbol != b.Symbol { - return - } - - if !b.orders.Exists(order.OrderID) { - b.pendingOrderUpdates.Add(order) - return - } - b.Update(order) } @@ -279,6 +268,17 @@ func (b *ActiveOrderBook) update(orders ...types.Order) { // When order is New or PartiallyFilled, the internal order will be updated according to the latest order update. // When the order is cancelled, it will be removed from the internal order storage. func (b *ActiveOrderBook) Update(order types.Order) { + hasSymbol := len(b.Symbol) > 0 + + if hasSymbol && order.Symbol != b.Symbol { + return + } + + if !b.orders.Exists(order.OrderID) { + b.pendingOrderUpdates.Add(order) + return + } + switch order.Status { case types.OrderStatusFilled: // make sure we have the order and we remove it From 1cadaf92651fc8d18594ff4a40242f71e146c676 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 17 Aug 2023 17:16:27 +0800 Subject: [PATCH 1363/1392] bbgo: add mutex lock to ActiveOrderBook --- pkg/bbgo/activeorderbook.go | 35 +++++++++++++++++++++++--------- pkg/bbgo/activeorderbook_test.go | 3 +-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 104c9f3497..5670146650 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "sort" + "sync" "time" "github.com/pkg/errors" @@ -32,6 +33,8 @@ type ActiveOrderBook struct { // sig is the order update signal // this signal will be emitted when a new order is added or removed. C sigchan.Chan + + mu sync.Mutex } func NewActiveOrderBook(symbol string) *ActiveOrderBook { @@ -57,7 +60,7 @@ func (b *ActiveOrderBook) BindStream(stream types.Stream) { } func (b *ActiveOrderBook) waitClear(ctx context.Context, order types.Order, waitTime, timeout time.Duration) (bool, error) { - if !b.Exists(order) { + if !b.orders.Exists(order.OrderID) { return true, nil } @@ -68,7 +71,7 @@ func (b *ActiveOrderBook) waitClear(ctx context.Context, order types.Order, wait case <-b.C: } - clear := !b.Exists(order) + clear := !b.orders.Exists(order.OrderID) select { case <-timeoutC: @@ -146,7 +149,7 @@ func (b *ActiveOrderBook) FastCancel(ctx context.Context, ex types.Exchange, ord } for _, o := range orders { - b.Remove(o) + b.orders.Remove(o.OrderID) } return nil } @@ -269,45 +272,53 @@ func (b *ActiveOrderBook) update(orders ...types.Order) { // When the order is cancelled, it will be removed from the internal order storage. func (b *ActiveOrderBook) Update(order types.Order) { hasSymbol := len(b.Symbol) > 0 - if hasSymbol && order.Symbol != b.Symbol { return } + b.mu.Lock() if !b.orders.Exists(order.OrderID) { b.pendingOrderUpdates.Add(order) + b.mu.Unlock() return } switch order.Status { case types.OrderStatusFilled: // make sure we have the order and we remove it - if b.Remove(order) { + removed := b.orders.Remove(order.OrderID) + b.mu.Unlock() + + if removed { b.EmitFilled(order) } b.C.Emit() case types.OrderStatusPartiallyFilled: b.update(order) + b.mu.Unlock() case types.OrderStatusNew: b.update(order) + b.mu.Unlock() + b.C.Emit() case types.OrderStatusCanceled, types.OrderStatusRejected: // TODO: note that orders transit to "canceled" may have partially filled + log.Debugf("[ActiveOrderBook] order is %s, removing order %s", order.Status, order) + b.orders.Remove(order.OrderID) + b.mu.Unlock() + if order.Status == types.OrderStatusCanceled { b.EmitCanceled(order) } - - log.Debugf("[ActiveOrderBook] order is %s, removing order %s", order.Status, order) - b.Remove(order) b.C.Emit() default: + b.mu.Unlock() log.Warnf("[ActiveOrderBook] unhandled order status: %s", order.Status) } - } func (b *ActiveOrderBook) Add(orders ...types.Order) { @@ -339,7 +350,7 @@ func (b *ActiveOrderBook) add(order types.Order) { // so, when it's not status=new, we should trigger order update handler if order.Status != types.OrderStatusNew { // emit the order update handle function to trigger callback - b.orderUpdateHandler(order) + b.Update(order) } } else { @@ -348,6 +359,8 @@ func (b *ActiveOrderBook) add(order types.Order) { } func (b *ActiveOrderBook) Exists(order types.Order) bool { + b.mu.Lock() + defer b.mu.Unlock() return b.orders.Exists(order.OrderID) } @@ -356,6 +369,8 @@ func (b *ActiveOrderBook) Get(orderID uint64) (types.Order, bool) { } func (b *ActiveOrderBook) Remove(order types.Order) bool { + b.mu.Lock() + defer b.mu.Unlock() return b.orders.Remove(order.OrderID) } diff --git a/pkg/bbgo/activeorderbook_test.go b/pkg/bbgo/activeorderbook_test.go index fdd4ea9e95..848e07bc9f 100644 --- a/pkg/bbgo/activeorderbook_test.go +++ b/pkg/bbgo/activeorderbook_test.go @@ -22,7 +22,7 @@ func TestActiveOrderBook_pendingOrders(t *testing.T) { // if we received filled order first // should be added to pending orders - ob.orderUpdateHandler(types.Order{ + ob.Update(types.Order{ OrderID: 99, SubmitOrder: types.SubmitOrder{ Symbol: "BTCUSDT", @@ -63,5 +63,4 @@ func TestActiveOrderBook_pendingOrders(t *testing.T) { }) assert.True(t, filled, "filled event should be fired") - } From c91861ca9adb6c799eabf3565a673d8e63f4bf35 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 17 Aug 2023 17:31:24 +0800 Subject: [PATCH 1364/1392] bbgo: add order update time check --- pkg/bbgo/activeorderbook.go | 25 +++++++++++-------------- pkg/strategy/grid2/strategy.go | 2 -- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 5670146650..ba0c0a6d9f 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -254,18 +254,6 @@ func (b *ActiveOrderBook) Print() { } } -// update updates the order stores in the internal order storage -func (b *ActiveOrderBook) update(orders ...types.Order) { - hasSymbol := len(b.Symbol) > 0 - for _, order := range orders { - if hasSymbol && b.Symbol != order.Symbol { - continue - } - - b.orders.Update(order) - } -} - // Update updates the order by the order status and emit the related events. // When order is filled, the order will be removed from the internal order storage. // When order is New or PartiallyFilled, the internal order will be updated according to the latest order update. @@ -283,6 +271,15 @@ func (b *ActiveOrderBook) Update(order types.Order) { return } + // if order update time is too old, skip it + if previousOrder, ok := b.orders.Get(order.OrderID); ok { + previousUpdateTime := previousOrder.UpdateTime.Time() + if !previousUpdateTime.IsZero() && order.UpdateTime.Before(previousUpdateTime) { + b.mu.Unlock() + return + } + } + switch order.Status { case types.OrderStatusFilled: // make sure we have the order and we remove it @@ -295,11 +292,11 @@ func (b *ActiveOrderBook) Update(order types.Order) { b.C.Emit() case types.OrderStatusPartiallyFilled: - b.update(order) + b.orders.Update(order) b.mu.Unlock() case types.OrderStatusNew: - b.update(order) + b.orders.Update(order) b.mu.Unlock() b.C.Emit() diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 1da04a3e01..e36c942d2c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -2127,9 +2127,7 @@ func (s *Strategy) handleConnect(ctx context.Context, session *bbgo.ExchangeSess activeOrderBook := s.orderExecutor.ActiveMakerOrders() activeOrders := activeOrderBook.Orders() for _, o := range activeOrders { - var updatedOrder *types.Order - err := retry.GeneralBackoff(ctx, func() error { var err error updatedOrder, err = s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ From 9105ebce786d70ce5a1180e23a9a59fcf0589522 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Aug 2023 12:51:15 +0800 Subject: [PATCH 1365/1392] deposit2transfer: fix err msg --- pkg/strategy/deposit2transfer/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/deposit2transfer/strategy.go b/pkg/strategy/deposit2transfer/strategy.go index be59562aa4..aad8ad0730 100644 --- a/pkg/strategy/deposit2transfer/strategy.go +++ b/pkg/strategy/deposit2transfer/strategy.go @@ -149,7 +149,7 @@ func (s *Strategy) checkDeposits(ctx context.Context) { if service, ok := s.session.Exchange.(spotAccountQueryService); ok { account, err2 := service.QuerySpotAccount(ctx) if err2 != nil { - log.WithError(err2).Errorf("unable to update account") + log.WithError(err2).Errorf("unable to query spot account") continue } From ed948b264252d15986b49df5e5d12b7b29df97a9 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 17 Aug 2023 17:42:54 +0800 Subject: [PATCH 1366/1392] max: fix QuerySpotAccount method return value --- pkg/exchange/max/exchange.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 83b56f1edb..0a894d4fa4 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -587,8 +587,7 @@ func (e *Exchange) QuerySpotAccount(ctx context.Context) (*types.Account, error) return nil, err } a.UpdateBalances(balances) - - return nil, nil + return a, nil } func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { From 3ad7075ace989e3ce2045d81063c9158f29a77f8 Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 18 Aug 2023 00:48:45 +0800 Subject: [PATCH 1367/1392] pkg/exchange: fix quantity coin unit --- .../bybit/bybitapi/get_open_orders_request.go | 101 +++--- pkg/exchange/bybit/bybitapi/types.go | 7 +- pkg/exchange/bybit/convert.go | 85 ++++- pkg/exchange/bybit/convert_test.go | 321 ++++++++++++++++-- 4 files changed, 423 insertions(+), 91 deletions(-) diff --git a/pkg/exchange/bybit/bybitapi/get_open_orders_request.go b/pkg/exchange/bybit/bybitapi/get_open_orders_request.go index 3634d005f6..3f01b717d1 100644 --- a/pkg/exchange/bybit/bybitapi/get_open_orders_request.go +++ b/pkg/exchange/bybit/bybitapi/get_open_orders_request.go @@ -16,47 +16,66 @@ type OrdersResponse struct { } type Order struct { - OrderId string `json:"orderId"` - OrderLinkId string `json:"orderLinkId"` - BlockTradeId string `json:"blockTradeId"` - Symbol string `json:"symbol"` - Price fixedpoint.Value `json:"price"` - Qty fixedpoint.Value `json:"qty"` - Side Side `json:"side"` - IsLeverage string `json:"isLeverage"` - PositionIdx int `json:"positionIdx"` - OrderStatus OrderStatus `json:"orderStatus"` - CancelType string `json:"cancelType"` - RejectReason string `json:"rejectReason"` - AvgPrice fixedpoint.Value `json:"avgPrice"` - LeavesQty fixedpoint.Value `json:"leavesQty"` - LeavesValue fixedpoint.Value `json:"leavesValue"` - CumExecQty fixedpoint.Value `json:"cumExecQty"` - CumExecValue fixedpoint.Value `json:"cumExecValue"` - CumExecFee fixedpoint.Value `json:"cumExecFee"` - TimeInForce TimeInForce `json:"timeInForce"` - OrderType OrderType `json:"orderType"` - StopOrderType string `json:"stopOrderType"` - OrderIv string `json:"orderIv"` - TriggerPrice fixedpoint.Value `json:"triggerPrice"` - TakeProfit fixedpoint.Value `json:"takeProfit"` - StopLoss fixedpoint.Value `json:"stopLoss"` - TpTriggerBy string `json:"tpTriggerBy"` - SlTriggerBy string `json:"slTriggerBy"` - TriggerDirection int `json:"triggerDirection"` - TriggerBy string `json:"triggerBy"` - LastPriceOnCreated string `json:"lastPriceOnCreated"` - ReduceOnly bool `json:"reduceOnly"` - CloseOnTrigger bool `json:"closeOnTrigger"` - SmpType string `json:"smpType"` - SmpGroup int `json:"smpGroup"` - SmpOrderId string `json:"smpOrderId"` - TpslMode string `json:"tpslMode"` - TpLimitPrice string `json:"tpLimitPrice"` - SlLimitPrice string `json:"slLimitPrice"` - PlaceType string `json:"placeType"` - CreatedTime types.MillisecondTimestamp `json:"createdTime"` - UpdatedTime types.MillisecondTimestamp `json:"updatedTime"` + OrderId string `json:"orderId"` + OrderLinkId string `json:"orderLinkId"` + Symbol string `json:"symbol"` + Side Side `json:"side"` + OrderStatus OrderStatus `json:"orderStatus"` + OrderType OrderType `json:"orderType"` + TimeInForce TimeInForce `json:"timeInForce"` + Price fixedpoint.Value `json:"price"` + CreatedTime types.MillisecondTimestamp `json:"createdTime"` + UpdatedTime types.MillisecondTimestamp `json:"updatedTime"` + + // Qty represents **quote coin** if order is market buy + Qty fixedpoint.Value `json:"qty"` + + // AvgPrice is supported in both RESTful API and WebSocket. + // + // For websocket must notice that: + // - Normal account is not supported. + // - For normal account USDT Perp and Inverse derivatives trades, if a partially filled order, and the + // final orderStatus is Cancelled, then avgPrice is "0" + AvgPrice fixedpoint.Value `json:"avgPrice"` + + // CumExecQty is supported in both RESTful API and WebSocket. + CumExecQty fixedpoint.Value `json:"cumExecQty"` + + // CumExecValue is supported in both RESTful API and WebSocket. + // However, it's **not** supported for **normal accounts** in RESTful API. + CumExecValue fixedpoint.Value `json:"cumExecValue"` + + // CumExecFee is supported in both RESTful API and WebSocket. + // However, it's **not** supported for **normal accounts** in RESTful API. + // For websocket normal spot, it is the execution fee per single fill. + CumExecFee fixedpoint.Value `json:"cumExecFee"` + + BlockTradeId string `json:"blockTradeId"` + IsLeverage string `json:"isLeverage"` + PositionIdx int `json:"positionIdx"` + CancelType string `json:"cancelType"` + RejectReason string `json:"rejectReason"` + LeavesQty fixedpoint.Value `json:"leavesQty"` + LeavesValue fixedpoint.Value `json:"leavesValue"` + StopOrderType string `json:"stopOrderType"` + OrderIv string `json:"orderIv"` + TriggerPrice fixedpoint.Value `json:"triggerPrice"` + TakeProfit fixedpoint.Value `json:"takeProfit"` + StopLoss fixedpoint.Value `json:"stopLoss"` + TpTriggerBy string `json:"tpTriggerBy"` + SlTriggerBy string `json:"slTriggerBy"` + TriggerDirection int `json:"triggerDirection"` + TriggerBy string `json:"triggerBy"` + LastPriceOnCreated string `json:"lastPriceOnCreated"` + ReduceOnly bool `json:"reduceOnly"` + CloseOnTrigger bool `json:"closeOnTrigger"` + SmpType string `json:"smpType"` + SmpGroup int `json:"smpGroup"` + SmpOrderId string `json:"smpOrderId"` + TpslMode string `json:"tpslMode"` + TpLimitPrice string `json:"tpLimitPrice"` + SlLimitPrice string `json:"slLimitPrice"` + PlaceType string `json:"placeType"` } //go:generate GetRequest -url "/v5/order/realtime" -type GetOpenOrdersRequest -responseDataType .OrdersResponse diff --git a/pkg/exchange/bybit/bybitapi/types.go b/pkg/exchange/bybit/bybitapi/types.go index 509b51ded1..2871c7c9b9 100644 --- a/pkg/exchange/bybit/bybitapi/types.go +++ b/pkg/exchange/bybit/bybitapi/types.go @@ -68,9 +68,10 @@ const ( // OrderStatusCreated order has been accepted by the system but not yet put through the matching engine OrderStatusCreated OrderStatus = "Created" // OrderStatusNew is order has been placed successfully. - OrderStatusNew OrderStatus = "New" - OrderStatusRejected OrderStatus = "Rejected" - OrderStatusPartiallyFilled OrderStatus = "PartiallyFilled" + OrderStatusNew OrderStatus = "New" + OrderStatusRejected OrderStatus = "Rejected" + OrderStatusPartiallyFilled OrderStatus = "PartiallyFilled" + // OrderStatusPartiallyFilledCanceled means that the order has been partially filled but not all then cancel. OrderStatusPartiallyFilledCanceled OrderStatus = "PartiallyFilledCanceled" OrderStatusFilled OrderStatus = "Filled" OrderStatusCancelled OrderStatus = "Cancelled" diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index 19043ad23c..8ef6ba13ab 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -2,13 +2,13 @@ package bybit import ( "fmt" - "hash/fnv" "math" "strconv" "time" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi/v3" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -53,22 +53,22 @@ func toGlobalOrder(order bybitapi.Order) (*types.Order, error) { if err != nil { return nil, err } + orderType, err := toGlobalOrderType(order.OrderType) if err != nil { return nil, err } + timeInForce, err := toGlobalTimeInForce(order.TimeInForce) if err != nil { return nil, err } - status, err := toGlobalOrderStatus(order.OrderStatus) - if err != nil { - return nil, err - } - working, err := isWorking(order.OrderStatus) + + status, err := toGlobalOrderStatus(order.OrderStatus, order.Side, order.OrderType) if err != nil { return nil, err } + // linear and inverse : 42f4f364-82e1-49d3-ad1d-cd8cf9aa308d (UUID format) // spot : 1468264727470772736 (only numbers) // Now we only use spot trading. @@ -77,13 +77,18 @@ func toGlobalOrder(order bybitapi.Order) (*types.Order, error) { return nil, fmt.Errorf("unexpected order id: %s, err: %w", order.OrderId, err) } + qty, err := processQuantity(order) + if err != nil { + return nil, err + } + return &types.Order{ SubmitOrder: types.SubmitOrder{ ClientOrderID: order.OrderLinkId, Symbol: order.Symbol, Side: side, Type: orderType, - Quantity: order.Qty, + Quantity: qty, Price: order.Price, TimeInForce: timeInForce, }, @@ -92,7 +97,7 @@ func toGlobalOrder(order bybitapi.Order) (*types.Order, error) { UUID: order.OrderId, Status: status, ExecutedQuantity: order.CumExecQty, - IsWorking: working, + IsWorking: status == types.OrderStatusNew || status == types.OrderStatusPartiallyFilled, CreationTime: types.Time(order.CreatedTime.Time()), UpdateTime: types.Time(order.UpdatedTime.Time()), }, nil @@ -140,7 +145,23 @@ func toGlobalTimeInForce(force bybitapi.TimeInForce) (types.TimeInForce, error) } } -func toGlobalOrderStatus(status bybitapi.OrderStatus) (types.OrderStatus, error) { +func toGlobalOrderStatus(status bybitapi.OrderStatus, side bybitapi.Side, orderType bybitapi.OrderType) (types.OrderStatus, error) { + switch status { + + case bybitapi.OrderStatusPartiallyFilledCanceled: + // market buy order -> PartiallyFilled -> PartiallyFilledCanceled + if orderType == bybitapi.OrderTypeMarket && side == bybitapi.SideBuy { + return types.OrderStatusFilled, nil + } + // limit buy/sell order -> PartiallyFilled -> PartiallyFilledCanceled(Canceled) + return types.OrderStatusCanceled, nil + + default: + return processOtherOrderStatus(status) + } +} + +func processOtherOrderStatus(status bybitapi.OrderStatus) (types.OrderStatus, error) { switch status { case bybitapi.OrderStatusCreated, bybitapi.OrderStatusNew, @@ -154,7 +175,6 @@ func toGlobalOrderStatus(status bybitapi.OrderStatus) (types.OrderStatus, error) return types.OrderStatusPartiallyFilled, nil case bybitapi.OrderStatusCancelled, - bybitapi.OrderStatusPartiallyFilledCanceled, bybitapi.OrderStatusDeactivated: return types.OrderStatusCanceled, nil @@ -169,15 +189,44 @@ func toGlobalOrderStatus(status bybitapi.OrderStatus) (types.OrderStatus, error) } } -func hashStringID(s string) uint64 { - h := fnv.New64a() - h.Write([]byte(s)) - return h.Sum64() -} +// processQuantity converts the quantity unit from quote coin to base coin if the order is a **MARKET BUY**. +// +// If the status is OrderStatusPartiallyFilled, it returns the estimated quantity based on the base coin. +// +// If the order status is OrderStatusPartiallyFilledCanceled, it indicates that the order is not fully filled, +// and the system has automatically canceled it. In this scenario, CumExecQty is considered equal to Qty. +func processQuantity(o bybitapi.Order) (fixedpoint.Value, error) { + if o.Side != bybitapi.SideBuy || o.OrderType != bybitapi.OrderTypeMarket { + return o.Qty, nil + } + + var qty fixedpoint.Value + switch o.OrderStatus { + case bybitapi.OrderStatusPartiallyFilled: + // if CumExecValue is zero, it indicates the caller is from the RESTFUL API. + // we can use AvgPrice to estimate quantity. + if o.CumExecValue.IsZero() { + qty = o.Qty.Div(o.AvgPrice) + } else { + // from web socket event + qty = o.Qty.Div(o.CumExecValue.Div(o.CumExecQty)) + } + + case bybitapi.OrderStatusPartiallyFilledCanceled, + // Considering extreme scenarios, there's a possibility that 'OrderStatusFilled' could occur. + bybitapi.OrderStatusFilled: + qty = o.CumExecQty + + case bybitapi.OrderStatusCreated, + bybitapi.OrderStatusNew, + bybitapi.OrderStatusRejected: + qty = fixedpoint.Zero + + default: + return fixedpoint.Zero, fmt.Errorf("unexpected order status: %s", o.OrderStatus) + } -func isWorking(status bybitapi.OrderStatus) (bool, error) { - s, err := toGlobalOrderStatus(status) - return s == types.OrderStatusNew || s == types.OrderStatusPartiallyFilled, err + return qty, nil } func toLocalOrderType(orderType types.OrderType) (bybitapi.OrderType, error) { diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index d2dab16320..195d133886 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -133,6 +133,260 @@ func TestToGlobalTicker(t *testing.T) { assert.Equal(t, toGlobalTicker(ticker, timeNow), exp) } +func Test_processQuantity(t *testing.T) { + t.Run("websocket event", func(t *testing.T) { + t.Run("Market/Buy/OrderStatusPartiallyFilled", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + CumExecValue: fixedpoint.NewFromFloat(200), + CumExecQty: fixedpoint.NewFromFloat(2), + OrderStatus: bybitapi.OrderStatusPartiallyFilled, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty.Div(o.CumExecValue.Div(o.CumExecQty)), res) + }) + + t.Run("Market/Buy/OrderStatusPartiallyFilledCanceled", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + CumExecValue: fixedpoint.NewFromFloat(200), + CumExecQty: fixedpoint.NewFromFloat(2), + OrderStatus: bybitapi.OrderStatusPartiallyFilled, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty.Div(o.CumExecValue.Div(o.CumExecQty)), res) + }) + + t.Run("Market/Buy/OrderStatusFilled", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + CumExecValue: fixedpoint.NewFromFloat(200), + CumExecQty: fixedpoint.NewFromFloat(2), + OrderStatus: bybitapi.OrderStatusFilled, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.CumExecQty, res) + }) + + t.Run("Market/Buy/OrderStatusCreated", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + CumExecValue: fixedpoint.NewFromFloat(200), + CumExecQty: fixedpoint.NewFromFloat(2), + OrderStatus: bybitapi.OrderStatusCreated, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, fixedpoint.Zero, res) + }) + + t.Run("Market/Buy/OrderStatusNew", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + CumExecValue: fixedpoint.NewFromFloat(200), + CumExecQty: fixedpoint.NewFromFloat(2), + OrderStatus: bybitapi.OrderStatusNew, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, fixedpoint.Zero, res) + }) + + t.Run("Market/Buy/OrderStatusRejected", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + CumExecValue: fixedpoint.NewFromFloat(200), + CumExecQty: fixedpoint.NewFromFloat(2), + OrderStatus: bybitapi.OrderStatusRejected, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, fixedpoint.Zero, res) + }) + + t.Run("Market/Buy/Unexpected status", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + CumExecValue: fixedpoint.NewFromFloat(200), + CumExecQty: fixedpoint.NewFromFloat(2), + OrderStatus: bybitapi.OrderStatus("unexpected"), + } + res, err := processQuantity(o) + assert.Error(t, err) + assert.Equal(t, fmt.Errorf("unexpected order status: %s", o.OrderStatus), err) + assert.Equal(t, fixedpoint.Zero, res) + }) + + t.Run("Market/Sell", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5.55), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideSell, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty, res) + }) + + t.Run("Limit/Buy", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5.55), + OrderType: bybitapi.OrderTypeLimit, + Side: bybitapi.SideBuy, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty, res) + }) + + t.Run("Limit/Sell", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5.55), + OrderType: bybitapi.OrderTypeLimit, + Side: bybitapi.SideSell, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty, res) + }) + }) + + t.Run("Restful API", func(t *testing.T) { + t.Run("Market/Buy/OrderStatusPartiallyFilled", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(200), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + AvgPrice: fixedpoint.NewFromFloat(25000), + OrderStatus: bybitapi.OrderStatusPartiallyFilled, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty.Div(o.AvgPrice), res) + }) + + t.Run("Market/Buy/OrderStatusPartiallyFilledCanceled", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(200), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + AvgPrice: fixedpoint.NewFromFloat(25000), + OrderStatus: bybitapi.OrderStatusPartiallyFilledCanceled, + CumExecQty: fixedpoint.NewFromFloat(0.002), + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.CumExecQty, res) + }) + + t.Run("Market/Buy/OrderStatusFilled", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(200), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + AvgPrice: fixedpoint.NewFromFloat(25000), + OrderStatus: bybitapi.OrderStatusFilled, + CumExecQty: fixedpoint.NewFromFloat(0.002), + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.CumExecQty, res) + }) + + t.Run("Market/Buy/OrderStatusCreated", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(200), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + AvgPrice: fixedpoint.NewFromFloat(25000), + OrderStatus: bybitapi.OrderStatusCreated, + CumExecQty: fixedpoint.NewFromFloat(0.002), + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, fixedpoint.Zero, res) + }) + + t.Run("Market/Buy/OrderStatusNew", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(200), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + AvgPrice: fixedpoint.NewFromFloat(25000), + OrderStatus: bybitapi.OrderStatusNew, + CumExecQty: fixedpoint.NewFromFloat(0.002), + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, fixedpoint.Zero, res) + }) + + t.Run("Market/Buy/OrderStatusRejected", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(200), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + AvgPrice: fixedpoint.NewFromFloat(25000), + OrderStatus: bybitapi.OrderStatusRejected, + CumExecQty: fixedpoint.NewFromFloat(0.002), + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, fixedpoint.Zero, res) + }) + + t.Run("Market/Sell", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5.55), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideSell, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty, res) + }) + + t.Run("Limit/Buy", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5.55), + OrderType: bybitapi.OrderTypeLimit, + Side: bybitapi.SideBuy, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty, res) + }) + + t.Run("Limit/Sell", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5.55), + OrderType: bybitapi.OrderTypeLimit, + Side: bybitapi.SideSell, + } + res, err := processQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty, res) + }) + }) +} + func TestToGlobalOrder(t *testing.T) { // sample: partialFilled //{ @@ -228,9 +482,7 @@ func TestToGlobalOrder(t *testing.T) { assert.NoError(t, err) tif, err := toGlobalTimeInForce(openOrder.TimeInForce) assert.NoError(t, err) - status, err := toGlobalOrderStatus(openOrder.OrderStatus) - assert.NoError(t, err) - working, err := isWorking(openOrder.OrderStatus) + status, err := toGlobalOrderStatus(openOrder.OrderStatus, openOrder.Side, openOrder.OrderType) assert.NoError(t, err) orderIdNum, err := strconv.ParseUint(openOrder.OrderId, 10, 64) assert.NoError(t, err) @@ -250,9 +502,9 @@ func TestToGlobalOrder(t *testing.T) { UUID: openOrder.OrderId, Status: status, ExecutedQuantity: openOrder.CumExecQty, - IsWorking: working, - CreationTime: types.Time(timeNow), - UpdateTime: types.Time(timeNow), + IsWorking: status == types.OrderStatusNew || status == types.OrderStatusPartiallyFilled, + CreationTime: types.Time(openOrder.CreatedTime), + UpdateTime: types.Time(openOrder.UpdatedTime), IsFutures: false, IsMargin: false, IsIsolated: false, @@ -305,60 +557,71 @@ func TestToGlobalTimeInForce(t *testing.T) { assert.Error(t, err) } -func TestToGlobalOrderStatus(t *testing.T) { +func Test_toGlobalOrderStatus(t *testing.T) { + t.Run("market/buy", func(t *testing.T) { + res, err := toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilledCanceled, bybitapi.SideBuy, bybitapi.OrderTypeMarket) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusFilled, res) + }) + + t.Run("limit/buy", func(t *testing.T) { + res, err := toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilledCanceled, bybitapi.SideBuy, bybitapi.OrderTypeLimit) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusCanceled, res) + }) + + t.Run("limit/sell", func(t *testing.T) { + res, err := toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilledCanceled, bybitapi.SideSell, bybitapi.OrderTypeLimit) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusCanceled, res) + }) +} + +func Test_processOtherOrderStatus(t *testing.T) { t.Run("New", func(t *testing.T) { - res, err := toGlobalOrderStatus(bybitapi.OrderStatusNew) + res, err := processOtherOrderStatus(bybitapi.OrderStatusNew) assert.NoError(t, err) assert.Equal(t, types.OrderStatusNew, res) - res, err = toGlobalOrderStatus(bybitapi.OrderStatusActive) + res, err = processOtherOrderStatus(bybitapi.OrderStatusActive) assert.NoError(t, err) assert.Equal(t, types.OrderStatusNew, res) }) t.Run("Filled", func(t *testing.T) { - res, err := toGlobalOrderStatus(bybitapi.OrderStatusFilled) + res, err := processOtherOrderStatus(bybitapi.OrderStatusFilled) assert.NoError(t, err) assert.Equal(t, types.OrderStatusFilled, res) }) t.Run("PartiallyFilled", func(t *testing.T) { - res, err := toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilled) + res, err := processOtherOrderStatus(bybitapi.OrderStatusPartiallyFilled) assert.NoError(t, err) assert.Equal(t, types.OrderStatusPartiallyFilled, res) }) t.Run("OrderStatusCanceled", func(t *testing.T) { - res, err := toGlobalOrderStatus(bybitapi.OrderStatusCancelled) - assert.NoError(t, err) - assert.Equal(t, types.OrderStatusCanceled, res) - - res, err = toGlobalOrderStatus(bybitapi.OrderStatusPartiallyFilledCanceled) + res, err := processOtherOrderStatus(bybitapi.OrderStatusCancelled) assert.NoError(t, err) assert.Equal(t, types.OrderStatusCanceled, res) - res, err = toGlobalOrderStatus(bybitapi.OrderStatusDeactivated) + res, err = processOtherOrderStatus(bybitapi.OrderStatusDeactivated) assert.NoError(t, err) assert.Equal(t, types.OrderStatusCanceled, res) }) t.Run("OrderStatusRejected", func(t *testing.T) { - res, err := toGlobalOrderStatus(bybitapi.OrderStatusRejected) + res, err := processOtherOrderStatus(bybitapi.OrderStatusRejected) assert.NoError(t, err) assert.Equal(t, types.OrderStatusRejected, res) }) -} -func TestIsWorking(t *testing.T) { - for _, s := range bybitapi.AllOrderStatuses { - res, err := isWorking(s) - assert.NoError(t, err) - if res { - gos, err := toGlobalOrderStatus(s) - assert.NoError(t, err) - assert.True(t, gos == types.OrderStatusNew || gos == types.OrderStatusPartiallyFilled) - } - } + t.Run("OrderStatusPartiallyFilledCanceled", func(t *testing.T) { + res, err := processOtherOrderStatus(bybitapi.OrderStatusPartiallyFilledCanceled) + assert.Equal(t, types.OrderStatus(bybitapi.OrderStatusPartiallyFilledCanceled), res) + assert.Error(t, err) + assert.Equal(t, fmt.Errorf("unexpected order status: %s", bybitapi.OrderStatusPartiallyFilledCanceled), err) + }) } func Test_toLocalOrderType(t *testing.T) { From 3dce63710aa739330d0ffd5201b140db75cfb75c Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Mon, 21 Aug 2023 15:31:30 +0800 Subject: [PATCH 1368/1392] Fix to fit all reviews last time --- pkg/exchange/okex/convert.go | 37 +++++++++++++++--------- pkg/exchange/okex/exchange.go | 15 ++++------ pkg/exchange/okex/okexapi/client.go | 2 +- pkg/exchange/okex/okexapi/client_test.go | 2 +- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 7a008a2c23..2721527835 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -165,14 +165,10 @@ func toGlobalTrades(orderDetails []okexapi.OrderDetails) ([]types.Trade, error) func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) { var orders []types.Order for _, orderDetail := range orderDetails { - isMargin := false - if orderDetail.InstrumentType == string(okexapi.InstrumentTypeMARGIN) { - isMargin = true - } - o, err := toGlobalOrder(&orderDetail, isMargin) + o, err := toGlobalOrder(&orderDetail) if err != nil { - log.WithError(err).Error("order convert error") + return nil, err } else { orders = append(orders, *o) } @@ -218,10 +214,10 @@ func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { case okexapi.OrderTypeMarket: return types.OrderTypeMarket, nil - case okexapi.OrderTypeLimit, okexapi.OrderTypeMarketMakerProtection: + case okexapi.OrderTypeLimit, okexapi.OrderTypeFOK, okexapi.OrderTypeIOC, okexapi.OrderTypeMarketMakerProtection: return types.OrderTypeLimit, nil - case okexapi.OrderTypePostOnly, okexapi.OrderTypeIOC, okexapi.OrderTypeFOK, okexapi.OrderTypeMarektMakerProtectionPostOnly: + case okexapi.OrderTypePostOnly, okexapi.OrderTypeMarketMakerProtectionPostOnly: return types.OrderTypeLimitMaker, nil } @@ -236,18 +232,28 @@ func toLocalInterval(src string) string { }) } -func toGlobalOrder(okexOrder *okexapi.OrderDetails, isMargin bool) (*types.Order, error) { +func toGlobalSide(side okexapi.SideType) (s types.SideType) { + switch string(side) { + case "sell": + s = types.SideTypeSell + case "buy": + s = types.SideTypeBuy + } + return s +} + +func toGlobalOrder(okexOrder *okexapi.OrderDetails) (*types.Order, error) { orderID, err := strconv.ParseInt(okexOrder.OrderID, 10, 64) if err != nil { - return &types.Order{}, err + return nil, err } - side := types.SideType(strings.ToUpper(string(okexOrder.Side))) + side := toGlobalSide(okexOrder.Side) orderType, err := toGlobalOrderType(okexOrder.OrderType) if err != nil { - return &types.Order{}, err + return nil, err } timeInForce := types.TimeInForceGTC @@ -260,7 +266,7 @@ func toGlobalOrder(okexOrder *okexapi.OrderDetails, isMargin bool) (*types.Order orderStatus, err := toGlobalOrderStatus(okexOrder.State) if err != nil { - return &types.Order{}, err + return nil, err } isWorking := false @@ -270,6 +276,11 @@ func toGlobalOrder(okexOrder *okexapi.OrderDetails, isMargin bool) (*types.Order } + isMargin := false + if okexOrder.InstrumentType == string(okexapi.InstrumentTypeMARGIN) { + isMargin = true + } + return &types.Order{ SubmitOrder: types.SubmitOrder{ ClientOrderID: okexOrder.ClientOrderID, diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 02b48af646..3affd9cad8 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -26,6 +26,8 @@ var log = logrus.WithFields(logrus.Fields{ "exchange": ID, }) +var ErrSymbolRequired = errors.New("Symbol is required parameter") + type Exchange struct { key, secret, passphrase string @@ -273,7 +275,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) erro var reqs []*okexapi.CancelOrderRequest for _, order := range orders { if len(order.Symbol) == 0 { - return errors.New("symbol is required for canceling an okex order") + return ErrSymbolRequired } req := e.client.TradeService.NewCancelOrderRequest() @@ -342,10 +344,10 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { if len(q.Symbol) == 0 { - return nil, errors.New("okex.QueryOrder: InstrumentID is required parameter") + return nil, ErrSymbolRequired } if len(q.OrderID) == 0 && len(q.ClientOrderID) == 0 { - return nil, errors.New("okex.QueryOrder: ordId or clOrdId is required parameter") + return nil, errors.New("okex.QueryOrder: OrderId or ClientOrderId is required parameter") } req := e.client.TradeService.NewGetOrderDetailsRequest() req.InstrumentID(q.Symbol). @@ -359,10 +361,5 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O return nil, err } - isMargin := false - if order.InstrumentType == string(okexapi.InstrumentTypeMARGIN) { - isMargin = true - } - - return toGlobalOrder(order, isMargin) + return toGlobalOrder(order) } diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index 1138fd41af..3730b3252e 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -39,7 +39,7 @@ const ( OrderTypeFOK OrderType = "fok" OrderTypeIOC OrderType = "ioc" OrderTypeMarketMakerProtection OrderType = "mmp" - OrderTypeMarektMakerProtectionPostOnly OrderType = "mmp_and_post_only" + OrderTypeMarketMakerProtectionPostOnly OrderType = "mmp_and_post_only" ) type InstrumentType string diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index 0f670d3348..ec6841642a 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -18,7 +18,7 @@ func getTestClientOrSkip(t *testing.T) *RestClient { key, secret, passphrase, ok := testutil.IntegrationTestWithPassphraseConfigured(t, "OKEX") if !ok { - t.SkipNow() + t.Skip("Please configure all credentials about OKEX") return nil } From 9ee7377f36cba2117239f0232ae9ff828eccacb0 Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 18 Aug 2023 13:34:00 +0800 Subject: [PATCH 1369/1392] pkg/exchange: fix quantity for buy market order --- pkg/exchange/bybit/convert.go | 17 +++++- pkg/exchange/bybit/convert_test.go | 98 ++++++++++++++++++++++++------ pkg/exchange/bybit/exchange.go | 13 +++- pkg/exchange/bybit/stream.go | 5 ++ 4 files changed, 108 insertions(+), 25 deletions(-) diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index 8ef6ba13ab..b13032cd7b 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -77,7 +77,7 @@ func toGlobalOrder(order bybitapi.Order) (*types.Order, error) { return nil, fmt.Errorf("unexpected order id: %s, err: %w", order.OrderId, err) } - qty, err := processQuantity(order) + qty, err := processMarketBuyQuantity(order) if err != nil { return nil, err } @@ -189,13 +189,13 @@ func processOtherOrderStatus(status bybitapi.OrderStatus) (types.OrderStatus, er } } -// processQuantity converts the quantity unit from quote coin to base coin if the order is a **MARKET BUY**. +// processMarketBuyQuantity converts the quantity unit from quote coin to base coin if the order is a **MARKET BUY**. // // If the status is OrderStatusPartiallyFilled, it returns the estimated quantity based on the base coin. // // If the order status is OrderStatusPartiallyFilledCanceled, it indicates that the order is not fully filled, // and the system has automatically canceled it. In this scenario, CumExecQty is considered equal to Qty. -func processQuantity(o bybitapi.Order) (fixedpoint.Value, error) { +func processMarketBuyQuantity(o bybitapi.Order) (fixedpoint.Value, error) { if o.Side != bybitapi.SideBuy || o.OrderType != bybitapi.OrderTypeMarket { return o.Qty, nil } @@ -206,8 +206,16 @@ func processQuantity(o bybitapi.Order) (fixedpoint.Value, error) { // if CumExecValue is zero, it indicates the caller is from the RESTFUL API. // we can use AvgPrice to estimate quantity. if o.CumExecValue.IsZero() { + if o.AvgPrice.IsZero() { + return fixedpoint.Zero, fmt.Errorf("AvgPrice shouldn't be zero") + } + qty = o.Qty.Div(o.AvgPrice) } else { + if o.CumExecQty.IsZero() { + return fixedpoint.Zero, fmt.Errorf("CumExecQty shouldn't be zero") + } + // from web socket event qty = o.Qty.Div(o.CumExecValue.Div(o.CumExecQty)) } @@ -222,6 +230,9 @@ func processQuantity(o bybitapi.Order) (fixedpoint.Value, error) { bybitapi.OrderStatusRejected: qty = fixedpoint.Zero + case bybitapi.OrderStatusCancelled: + qty = o.Qty + default: return fixedpoint.Zero, fmt.Errorf("unexpected order status: %s", o.OrderStatus) } diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index 195d133886..786234d8de 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -133,7 +133,7 @@ func TestToGlobalTicker(t *testing.T) { assert.Equal(t, toGlobalTicker(ticker, timeNow), exp) } -func Test_processQuantity(t *testing.T) { +func Test_processMarketBuyQuantity(t *testing.T) { t.Run("websocket event", func(t *testing.T) { t.Run("Market/Buy/OrderStatusPartiallyFilled", func(t *testing.T) { o := bybitapi.Order{ @@ -144,7 +144,7 @@ func Test_processQuantity(t *testing.T) { CumExecQty: fixedpoint.NewFromFloat(2), OrderStatus: bybitapi.OrderStatusPartiallyFilled, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.Qty.Div(o.CumExecValue.Div(o.CumExecQty)), res) }) @@ -158,7 +158,7 @@ func Test_processQuantity(t *testing.T) { CumExecQty: fixedpoint.NewFromFloat(2), OrderStatus: bybitapi.OrderStatusPartiallyFilled, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.Qty.Div(o.CumExecValue.Div(o.CumExecQty)), res) }) @@ -172,7 +172,7 @@ func Test_processQuantity(t *testing.T) { CumExecQty: fixedpoint.NewFromFloat(2), OrderStatus: bybitapi.OrderStatusFilled, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.CumExecQty, res) }) @@ -186,7 +186,7 @@ func Test_processQuantity(t *testing.T) { CumExecQty: fixedpoint.NewFromFloat(2), OrderStatus: bybitapi.OrderStatusCreated, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, fixedpoint.Zero, res) }) @@ -200,7 +200,7 @@ func Test_processQuantity(t *testing.T) { CumExecQty: fixedpoint.NewFromFloat(2), OrderStatus: bybitapi.OrderStatusNew, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, fixedpoint.Zero, res) }) @@ -214,11 +214,25 @@ func Test_processQuantity(t *testing.T) { CumExecQty: fixedpoint.NewFromFloat(2), OrderStatus: bybitapi.OrderStatusRejected, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, fixedpoint.Zero, res) }) + t.Run("Market/Buy/OrderStatusCanceled", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + CumExecValue: fixedpoint.NewFromFloat(200), + CumExecQty: fixedpoint.NewFromFloat(2), + OrderStatus: bybitapi.OrderStatusCancelled, + } + res, err := processMarketBuyQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty, res) + }) + t.Run("Market/Buy/Unexpected status", func(t *testing.T) { o := bybitapi.Order{ Qty: fixedpoint.NewFromFloat(5), @@ -228,19 +242,34 @@ func Test_processQuantity(t *testing.T) { CumExecQty: fixedpoint.NewFromFloat(2), OrderStatus: bybitapi.OrderStatus("unexpected"), } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.Error(t, err) assert.Equal(t, fmt.Errorf("unexpected order status: %s", o.OrderStatus), err) assert.Equal(t, fixedpoint.Zero, res) }) + t.Run("Market/Buy/CumExecQty zero", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(5), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + CumExecValue: fixedpoint.NewFromFloat(200), + CumExecQty: fixedpoint.Zero, + OrderStatus: bybitapi.OrderStatusPartiallyFilled, + } + res, err := processMarketBuyQuantity(o) + assert.Error(t, err) + assert.Equal(t, fmt.Errorf("CumExecQty shouldn't be zero"), err) + assert.Equal(t, fixedpoint.Zero, res) + }) + t.Run("Market/Sell", func(t *testing.T) { o := bybitapi.Order{ Qty: fixedpoint.NewFromFloat(5.55), OrderType: bybitapi.OrderTypeMarket, Side: bybitapi.SideSell, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.Qty, res) }) @@ -251,7 +280,7 @@ func Test_processQuantity(t *testing.T) { OrderType: bybitapi.OrderTypeLimit, Side: bybitapi.SideBuy, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.Qty, res) }) @@ -262,7 +291,7 @@ func Test_processQuantity(t *testing.T) { OrderType: bybitapi.OrderTypeLimit, Side: bybitapi.SideSell, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.Qty, res) }) @@ -277,7 +306,7 @@ func Test_processQuantity(t *testing.T) { AvgPrice: fixedpoint.NewFromFloat(25000), OrderStatus: bybitapi.OrderStatusPartiallyFilled, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.Qty.Div(o.AvgPrice), res) }) @@ -291,7 +320,7 @@ func Test_processQuantity(t *testing.T) { OrderStatus: bybitapi.OrderStatusPartiallyFilledCanceled, CumExecQty: fixedpoint.NewFromFloat(0.002), } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.CumExecQty, res) }) @@ -305,7 +334,7 @@ func Test_processQuantity(t *testing.T) { OrderStatus: bybitapi.OrderStatusFilled, CumExecQty: fixedpoint.NewFromFloat(0.002), } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.CumExecQty, res) }) @@ -319,7 +348,7 @@ func Test_processQuantity(t *testing.T) { OrderStatus: bybitapi.OrderStatusCreated, CumExecQty: fixedpoint.NewFromFloat(0.002), } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, fixedpoint.Zero, res) }) @@ -333,7 +362,7 @@ func Test_processQuantity(t *testing.T) { OrderStatus: bybitapi.OrderStatusNew, CumExecQty: fixedpoint.NewFromFloat(0.002), } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, fixedpoint.Zero, res) }) @@ -347,18 +376,47 @@ func Test_processQuantity(t *testing.T) { OrderStatus: bybitapi.OrderStatusRejected, CumExecQty: fixedpoint.NewFromFloat(0.002), } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, fixedpoint.Zero, res) }) + t.Run("Market/Buy/OrderStatusCanceled", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(200), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + AvgPrice: fixedpoint.NewFromFloat(25000), + OrderStatus: bybitapi.OrderStatusCancelled, + CumExecQty: fixedpoint.NewFromFloat(0.002), + } + res, err := processMarketBuyQuantity(o) + assert.NoError(t, err) + assert.Equal(t, o.Qty, res) + }) + + t.Run("Market/Buy/AvgPrice zero", func(t *testing.T) { + o := bybitapi.Order{ + Qty: fixedpoint.NewFromFloat(200), + OrderType: bybitapi.OrderTypeMarket, + Side: bybitapi.SideBuy, + AvgPrice: fixedpoint.Zero, + OrderStatus: bybitapi.OrderStatusPartiallyFilled, + CumExecQty: fixedpoint.NewFromFloat(0.002), + } + res, err := processMarketBuyQuantity(o) + assert.Error(t, err) + assert.Equal(t, fmt.Errorf("AvgPrice shouldn't be zero"), err) + assert.Equal(t, fixedpoint.Zero, res) + }) + t.Run("Market/Sell", func(t *testing.T) { o := bybitapi.Order{ Qty: fixedpoint.NewFromFloat(5.55), OrderType: bybitapi.OrderTypeMarket, Side: bybitapi.SideSell, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.Qty, res) }) @@ -369,7 +427,7 @@ func Test_processQuantity(t *testing.T) { OrderType: bybitapi.OrderTypeLimit, Side: bybitapi.SideBuy, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.Qty, res) }) @@ -380,7 +438,7 @@ func Test_processQuantity(t *testing.T) { OrderType: bybitapi.OrderTypeLimit, Side: bybitapi.SideSell, } - res, err := processQuantity(o) + res, err := processMarketBuyQuantity(o) assert.NoError(t, err) assert.Equal(t, o.Qty, res) }) diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index ba1e123b8b..e087abf118 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -263,7 +263,7 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t } req := e.client.NewPlaceOrderRequest() - req.Symbol(order.Symbol) + req.Symbol(order.Market.Symbol) // set order type orderType, err := toLocalOrderType(order.Type) @@ -280,7 +280,16 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t req.Side(side) // set quantity - req.Qty(order.Market.FormatQuantity(order.Quantity)) + orderQty := order.Quantity + // if the order is market buy, the quantity is quote coin, instead of base coin. so we need to convert it. + if order.Type == types.OrderTypeMarket && order.Side == types.SideTypeBuy { + ticker, err := e.QueryTicker(ctx, order.Market.Symbol) + if err != nil { + return nil, err + } + orderQty = order.Quantity.Mul(ticker.Buy) + } + req.Qty(order.Market.FormatQuantity(orderQty)) // set price switch order.Type { diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 2359030f9b..0a42dd42a4 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -204,6 +204,11 @@ func (s *Stream) ping(ctx context.Context, conn *websocket.Conn, cancelFunc cont func (s *Stream) handlerConnect() { if s.PublicOnly { + if len(s.Subscriptions) == 0 { + log.Debug("no subscriptions") + return + } + var topics []string for _, subscription := range s.Subscriptions { From a4aa9c2edaa273964826d32cbb2bace63dd5db98 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Tue, 22 Aug 2023 15:14:18 +0800 Subject: [PATCH 1370/1392] remove mmp and mmp_post_only --- pkg/exchange/okex/convert.go | 4 ++-- pkg/exchange/okex/okexapi/client.go | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 2721527835..622171e43c 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -214,10 +214,10 @@ func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { case okexapi.OrderTypeMarket: return types.OrderTypeMarket, nil - case okexapi.OrderTypeLimit, okexapi.OrderTypeFOK, okexapi.OrderTypeIOC, okexapi.OrderTypeMarketMakerProtection: + case okexapi.OrderTypeLimit, okexapi.OrderTypeFOK, okexapi.OrderTypeIOC: return types.OrderTypeLimit, nil - case okexapi.OrderTypePostOnly, okexapi.OrderTypeMarketMakerProtectionPostOnly: + case okexapi.OrderTypePostOnly: return types.OrderTypeLimitMaker, nil } diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index 3730b3252e..ccb81f70a5 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -33,13 +33,11 @@ const ( type OrderType string const ( - OrderTypeMarket OrderType = "market" - OrderTypeLimit OrderType = "limit" - OrderTypePostOnly OrderType = "post_only" - OrderTypeFOK OrderType = "fok" - OrderTypeIOC OrderType = "ioc" - OrderTypeMarketMakerProtection OrderType = "mmp" - OrderTypeMarketMakerProtectionPostOnly OrderType = "mmp_and_post_only" + OrderTypeMarket OrderType = "market" + OrderTypeLimit OrderType = "limit" + OrderTypePostOnly OrderType = "post_only" + OrderTypeFOK OrderType = "fok" + OrderTypeIOC OrderType = "ioc" ) type InstrumentType string From a0946fbd4241e15e492eb12e30ec2f899b108597 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Tue, 22 Aug 2023 17:23:16 +0800 Subject: [PATCH 1371/1392] use lower case in error string and add comment for IOC, FOK --- pkg/exchange/okex/convert.go | 7 ++++--- pkg/exchange/okex/exchange.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 622171e43c..4716b750c4 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -168,10 +168,9 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) o, err := toGlobalOrder(&orderDetail) if err != nil { - return nil, err - } else { - orders = append(orders, *o) + log.WithError(err).Error("order convert error") } + orders = append(orders, *o) } return orders, nil @@ -210,6 +209,8 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { } func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { + // Okex IOC and FOK only implement limit order + // reference: https://www.okx.com/cn/help-center/360025135731 switch orderType { case okexapi.OrderTypeMarket: return types.OrderTypeMarket, nil diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 3affd9cad8..08069a9b45 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -26,7 +26,7 @@ var log = logrus.WithFields(logrus.Fields{ "exchange": ID, }) -var ErrSymbolRequired = errors.New("Symbol is required parameter") +var ErrSymbolRequired = errors.New("symbol is a required parameter") type Exchange struct { key, secret, passphrase string From 26cde5d57c7d306c5ced5d293acbaf149549f256 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Wed, 23 Aug 2023 15:44:51 +0800 Subject: [PATCH 1372/1392] use multierr to handle err return from toGlobalOrder --- pkg/exchange/okex/convert.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 4716b750c4..1c903f8a19 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/pkg/errors" + "go.uber.org/multierr" "github.com/c9s/bbgo/pkg/exchange/okex/okexapi" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -164,16 +165,17 @@ func toGlobalTrades(orderDetails []okexapi.OrderDetails) ([]types.Trade, error) func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) { var orders []types.Order + var err error for _, orderDetail := range orderDetails { - o, err := toGlobalOrder(&orderDetail) - if err != nil { - log.WithError(err).Error("order convert error") + o, err2 := toGlobalOrder(&orderDetail) + if err2 != nil { + err = multierr.Append(err, err2) } orders = append(orders, *o) } - return orders, nil + return orders, err } func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) { @@ -209,8 +211,7 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { } func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { - // Okex IOC and FOK only implement limit order - // reference: https://www.okx.com/cn/help-center/360025135731 + // IOC, FOK are only allowed with limit order type, so we assume the order type is always limit order for FOK, IOC orders switch orderType { case okexapi.OrderTypeMarket: return types.OrderTypeMarket, nil From f8ae408fad6625f0eaa4c39b3d09fe0d644ba115 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Wed, 23 Aug 2023 16:16:38 +0800 Subject: [PATCH 1373/1392] add continue in err != nil --- pkg/exchange/okex/convert.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 1c903f8a19..881f66ab9a 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -171,6 +171,7 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) o, err2 := toGlobalOrder(&orderDetail) if err2 != nil { err = multierr.Append(err, err2) + continue } orders = append(orders, *o) } From 9dc7244d8a8805fbd63c36744ed9d333f8b1a381 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 31 Aug 2023 12:40:01 +0800 Subject: [PATCH 1374/1392] FEATURE: round down executed amount to avoid insufficient balance --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e36c942d2c..5a6e11441a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -460,6 +460,8 @@ func (s *Strategy) processFilledOrder(o types.Order) { // will be used for calculating quantity orderExecutedQuoteAmount := executedQuantity.Mul(executedPrice) + // round down order executed quote amount to avoid insufficient balance + orderExecutedQuoteAmount = orderExecutedQuoteAmount.Round(s.Market.PricePrecision, fixedpoint.Down) // collect trades for fee // fee calculation is used to reduce the order quantity From cb0285544ebd5bfa4902a9818ceb0f8e230ca2d5 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 31 Aug 2023 13:48:56 +0800 Subject: [PATCH 1375/1392] add lock to recoverActiveOrders --- pkg/strategy/grid2/strategy.go | 37 ++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e36c942d2c..7f17cdcb9b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -583,7 +583,9 @@ func (s *Strategy) handleOrderFilled(o types.Order) { s.processFilledOrder(o) } -func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { +func (s *Strategy) checkRequiredInvestmentByQuantity( + baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin, +) (requiredBase, requiredQuote fixedpoint.Value, err error) { // check more investment budget details requiredBase = fixedpoint.Zero requiredQuote = fixedpoint.Zero @@ -641,7 +643,9 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, return requiredBase, requiredQuote, nil } -func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { +func (s *Strategy) checkRequiredInvestmentByAmount( + baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin, +) (requiredBase, requiredQuote fixedpoint.Value, err error) { // check more investment budget details requiredBase = fixedpoint.Zero @@ -702,7 +706,9 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am return requiredBase, requiredQuote, nil } -func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { +func (s *Strategy) calculateQuoteInvestmentQuantity( + quoteInvestment, lastPrice fixedpoint.Value, pins []Pin, +) (fixedpoint.Value, error) { // quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + .... // => // quoteInvestment = (p1 + p2 + p3) * q @@ -758,7 +764,9 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f return q, nil } -func (s *Strategy) calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { +func (s *Strategy) calculateBaseQuoteInvestmentQuantity( + quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin, +) (fixedpoint.Value, error) { s.logger.Infof("calculating quantity by base/quote investment: %f / %f", baseInvestment.Float64(), quoteInvestment.Float64()) // q_p1 = q_p2 = q_p3 = q_p4 // baseInvestment = q_p1 + q_p2 + q_p3 + q_p4 + .... @@ -1463,7 +1471,9 @@ func (s *Strategy) checkMinimalQuoteInvestment(grid *Grid) error { return nil } -func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { +func (s *Strategy) recoverGridWithOpenOrders( + ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order, +) error { grid := s.newGrid() s.logger.Infof("GRID RECOVER: %s", grid.String()) @@ -1622,7 +1632,10 @@ func (s *Strategy) getGrid() *Grid { // replayOrderHistory queries the closed order history from the API and rebuild the orderbook from the order history. // startTime, endTime is the time range of the order history. -func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook *bbgo.ActiveOrderBook, historyService types.ExchangeTradeHistoryService, startTime, endTime time.Time, lastOrderID uint64) error { +func (s *Strategy) replayOrderHistory( + ctx context.Context, grid *Grid, orderBook *bbgo.ActiveOrderBook, historyService types.ExchangeTradeHistoryService, + startTime, endTime time.Time, lastOrderID uint64, +) error { // a simple guard, in reality, this startTime is not possible to exceed the endTime // because the queries closed orders might still in the range. orderIdChanged := true @@ -1952,7 +1965,11 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. }) session.UserDataStream.OnConnect(func() { - s.handleConnect(ctx, session) + if !bbgo.IsBackTesting { + go time.AfterFunc(time.Minute, func() { + s.recoverActiveOrders(ctx, session) + }) + } }) // if TriggerPrice is zero, that means we need to open the grid when start up @@ -2117,12 +2134,16 @@ func (s *Strategy) newClientOrderID() string { return "" } -func (s *Strategy) handleConnect(ctx context.Context, session *bbgo.ExchangeSession) { +func (s *Strategy) recoverActiveOrders(ctx context.Context, session *bbgo.ExchangeSession) { grid := s.getGrid() if grid == nil { return } + // this lock avoids recovering the active orders while the openGrid is executing + s.mu.Lock() + defer s.mu.Unlock() + // TODO: move this logics into the active maker orders component, like activeOrders.Sync(ctx) activeOrderBook := s.orderExecutor.ActiveMakerOrders() activeOrders := activeOrderBook.Orders() From 7de6c3d8e4bfe4eff80d857dc63cdf2fb9776a53 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 31 Aug 2023 13:59:44 +0800 Subject: [PATCH 1376/1392] grid2: add more update logs --- pkg/strategy/grid2/strategy.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7f17cdcb9b..b5e62e1315 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -2135,6 +2135,8 @@ func (s *Strategy) newClientOrderID() string { } func (s *Strategy) recoverActiveOrders(ctx context.Context, session *bbgo.ExchangeSession) { + s.logger.Infof("recovering active orders after websocket connect") + grid := s.getGrid() if grid == nil { return @@ -2147,7 +2149,14 @@ func (s *Strategy) recoverActiveOrders(ctx context.Context, session *bbgo.Exchan // TODO: move this logics into the active maker orders component, like activeOrders.Sync(ctx) activeOrderBook := s.orderExecutor.ActiveMakerOrders() activeOrders := activeOrderBook.Orders() + if len(activeOrders) == 0 { + return + } + + s.logger.Infof("found %d active orders to update...", len(activeOrders)) for _, o := range activeOrders { + s.logger.Infof("updating %d order...", o.OrderID) + var updatedOrder *types.Order err := retry.GeneralBackoff(ctx, func() error { var err error From f24bd3532cdfa413d859d5ee9eca09c0ed19e607 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 31 Aug 2023 14:08:19 +0800 Subject: [PATCH 1377/1392] grid2: add 5s delay and <10seconds jitter --- pkg/strategy/grid2/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b5e62e1315..5b34de3b7e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1966,7 +1966,9 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. session.UserDataStream.OnConnect(func() { if !bbgo.IsBackTesting { - go time.AfterFunc(time.Minute, func() { + // callback may block the stream execution, so we spawn the recover function to the background + // add (5 seconds + random <10 seconds jitter) delay + go time.AfterFunc(util.MillisecondsJitter(5*time.Second, 1000*10), func() { s.recoverActiveOrders(ctx, session) }) } From 52412d9eadb77377c97e70236d562165bf541530 Mon Sep 17 00:00:00 2001 From: narumi Date: Thu, 31 Aug 2023 14:30:50 +0800 Subject: [PATCH 1378/1392] refactor: add `IsOver` to check the since time is over given duration --- pkg/types/profit.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/types/profit.go b/pkg/types/profit.go index c7742e0b8f..9feb4edd76 100644 --- a/pkg/types/profit.go +++ b/pkg/types/profit.go @@ -240,9 +240,14 @@ func (s *ProfitStats) AddTrade(trade Trade) { s.AccumulatedVolume = s.AccumulatedVolume.Add(trade.Quantity) } +// IsOver checks if the since time is over given duration +func (s *ProfitStats) IsOver(d time.Duration) bool { + return time.Since(time.Unix(s.TodaySince, 0)) >= d +} + // IsOver24Hours checks if the since time is over 24 hours func (s *ProfitStats) IsOver24Hours() bool { - return time.Since(time.Unix(s.TodaySince, 0)) >= 24*time.Hour + return s.IsOver(24 * time.Hour) } func (s *ProfitStats) ResetToday(t time.Time) { From e74da87e5111e642a913efae32648777c9c6f19a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 31 Aug 2023 17:08:00 +0800 Subject: [PATCH 1379/1392] grid2: delay start process by 5s --- pkg/strategy/grid2/strategy.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 5b34de3b7e..6a51513ba2 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1964,16 +1964,6 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } }) - session.UserDataStream.OnConnect(func() { - if !bbgo.IsBackTesting { - // callback may block the stream execution, so we spawn the recover function to the background - // add (5 seconds + random <10 seconds jitter) delay - go time.AfterFunc(util.MillisecondsJitter(5*time.Second, 1000*10), func() { - s.recoverActiveOrders(ctx, session) - }) - } - }) - // if TriggerPrice is zero, that means we need to open the grid when start up if s.TriggerPrice.IsZero() { // must call the openGrid method inside the OnStart callback because @@ -1985,13 +1975,25 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.logger.Infof("user data stream started, initializing grid...") if !bbgo.IsBackTesting { - go s.startProcess(ctx, session) + go time.AfterFunc(3*time.Second, func() { + s.startProcess(ctx, session) + }) } else { s.startProcess(ctx, session) } }) } + session.UserDataStream.OnConnect(func() { + if !bbgo.IsBackTesting { + // callback may block the stream execution, so we spawn the recover function to the background + // add (5 seconds + random <10 seconds jitter) delay + go time.AfterFunc(util.MillisecondsJitter(5*time.Second, 1000*10), func() { + s.recoverActiveOrders(ctx, session) + }) + } + }) + return nil } From 594ab9cbaf6504a4d59885bba79aeedebe55b778 Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 1 Sep 2023 17:12:32 +0800 Subject: [PATCH 1380/1392] pkg/exchange: fix okex bookticker bug --- pkg/exchange/okex/parse.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/exchange/okex/parse.go b/pkg/exchange/okex/parse.go index 6b9c019ed1..f200093dfe 100644 --- a/pkg/exchange/okex/parse.go +++ b/pkg/exchange/okex/parse.go @@ -65,17 +65,21 @@ type BookEvent struct { } func (data *BookEvent) BookTicker() types.BookTicker { + ticker := types.BookTicker{ + Symbol: data.Symbol, + } - var askBookData BookEntry = data.Asks[0] - var bidBookData BookEntry = data.Bids[0] + if len(data.Bids) > 0 { + ticker.Buy = data.Bids[0].Price + ticker.BuySize = data.Bids[0].Volume + } - return types.BookTicker{ - Symbol: data.Symbol, - Buy: bidBookData.Price, - BuySize: bidBookData.Price, - Sell: askBookData.Price, - SellSize: askBookData.Volume, + if len(data.Asks) > 0 { + ticker.Sell = data.Asks[0].Price + ticker.SellSize = data.Asks[0].Volume } + + return ticker } func (data *BookEvent) Book() types.SliceOrderBook { From 50bfd8ee0e866bf4611ed794271ccfe8abf3035d Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 1 Sep 2023 17:02:08 +0800 Subject: [PATCH 1381/1392] pkg/exchange: add time to SliceOrderBook --- pkg/cmd/orderbook.go | 10 +++--- pkg/exchange/binance/exchange.go | 2 ++ pkg/exchange/binance/parse.go | 1 + pkg/exchange/binance/stream.go | 1 + pkg/exchange/bybit/stream.go | 1 + pkg/exchange/bybit/stream_test.go | 2 ++ pkg/exchange/bybit/types.go | 7 ++++- pkg/exchange/kucoin/exchange.go | 1 + pkg/exchange/kucoin/stream.go | 1 + pkg/exchange/kucoin/websocket.go | 1 + pkg/exchange/max/maxapi/public_parser.go | 39 ++++++++++++------------ pkg/exchange/max/stream.go | 1 + pkg/exchange/okex/parse.go | 1 + pkg/types/sliceorderbook.go | 7 +++++ 14 files changed, 51 insertions(+), 24 deletions(-) diff --git a/pkg/cmd/orderbook.go b/pkg/cmd/orderbook.go index 9eba051d6d..87b8b0087e 100644 --- a/pkg/cmd/orderbook.go +++ b/pkg/cmd/orderbook.go @@ -71,9 +71,10 @@ var orderbookCmd = &cobra.Command{ } if bid, ask, ok := orderBook.BestBidAndAsk(); ok { - log.Infof("ASK | %f x %f / %f x %f | BID", + log.Infof("ASK | %f x %f / %f x %f | BID | %s", ask.Volume.Float64(), ask.Price.Float64(), - bid.Price.Float64(), bid.Volume.Float64()) + bid.Price.Float64(), bid.Volume.Float64(), + book.Time.String()) } }) @@ -84,9 +85,10 @@ var orderbookCmd = &cobra.Command{ orderBook.Update(book) if bid, ask, ok := orderBook.BestBidAndAsk(); ok { - log.Infof("ASK | %f x %f / %f x %f | BID", + log.Infof("ASK | %f x %f / %f x %f | BID | %s", ask.Volume.Float64(), ask.Price.Float64(), - bid.Price.Float64(), bid.Volume.Float64()) + bid.Price.Float64(), bid.Volume.Float64(), + book.Time.String()) } }) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 3e62068c6c..9286be367c 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -1346,6 +1346,8 @@ func (e *Exchange) QueryDepth(ctx context.Context, symbol string) (snapshot type func convertDepth(snapshot types.SliceOrderBook, symbol string, finalUpdateID int64, response *binance.DepthResponse) (types.SliceOrderBook, int64, error) { snapshot.Symbol = symbol + // empty time since the API does not provide time information. + snapshot.Time = time.Time{} finalUpdateID = response.LastUpdateID for _, entry := range response.Bids { // entry.Price, Quantity: entry.Quantity diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index b2990d9778..a1350faf4b 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -461,6 +461,7 @@ func (e *DepthEvent) String() (o string) { func (e *DepthEvent) OrderBook() (book types.SliceOrderBook, err error) { book.Symbol = e.Symbol + book.Time = types.NewMillisecondTimestampFromInt(e.EventBase.Time).Time() // already in descending order book.Bids = e.Bids diff --git a/pkg/exchange/binance/stream.go b/pkg/exchange/binance/stream.go index 9cf5d27b20..cc09d8eb7a 100644 --- a/pkg/exchange/binance/stream.go +++ b/pkg/exchange/binance/stream.go @@ -88,6 +88,7 @@ func NewStream(ex *Exchange, client *binance.Client, futuresClient *futures.Clie if ok { err := f.AddUpdate(types.SliceOrderBook{ Symbol: e.Symbol, + Time: types.NewMillisecondTimestampFromInt(e.EventBase.Time).Time(), Bids: e.Bids, Asks: e.Asks, }, e.FirstUpdateID, e.FinalUpdateID) diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 2359030f9b..fdf284603c 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -131,6 +131,7 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { } book.Type = e.WebSocketTopicEvent.Type + book.ServerTime = e.WebSocketTopicEvent.Ts.Time() return &book, nil case TopicTypeKLine: diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go index 195c6fea2f..04de5d373a 100644 --- a/pkg/exchange/bybit/stream_test.go +++ b/pkg/exchange/bybit/stream_test.go @@ -35,6 +35,7 @@ func getTestClientOrSkip(t *testing.T) *Stream { } func TestStream(t *testing.T) { + t.Skip() s := getTestClientOrSkip(t) t.Run("Auth test", func(t *testing.T) { @@ -182,6 +183,7 @@ func TestStream_parseWebSocketEvent(t *testing.T) { UpdateId: fixedpoint.NewFromFloat(1854104), SequenceId: fixedpoint.NewFromFloat(10559247733), Type: DataTypeDelta, + ServerTime: types.NewMillisecondTimestampFromInt(1691130685111).Time(), }, *book) }) diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index 6b62f6563e..04e63e3ca8 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" "strings" + "time" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -117,14 +118,18 @@ type BookEvent struct { SequenceId fixedpoint.Value `json:"seq"` // internal use - // Type can be one of snapshot or delta. Copied from WebSocketTopicEvent.Type + // Copied from WebSocketTopicEvent.Type, WebSocketTopicEvent.Ts + // Type can be one of snapshot or delta. Type DataType + // ServerTime using the websocket timestamp as server time. Since the event not provide server time information. + ServerTime time.Time } func (e *BookEvent) OrderBook() (snapshot types.SliceOrderBook) { snapshot.Symbol = e.Symbol snapshot.Bids = e.Bids snapshot.Asks = e.Asks + snapshot.Time = e.ServerTime return snapshot } diff --git a/pkg/exchange/kucoin/exchange.go b/pkg/exchange/kucoin/exchange.go index be2d252c65..09ad8066fb 100644 --- a/pkg/exchange/kucoin/exchange.go +++ b/pkg/exchange/kucoin/exchange.go @@ -438,6 +438,7 @@ func (e *Exchange) QueryDepth(ctx context.Context, symbol string) (types.SliceOr return types.SliceOrderBook{ Symbol: toGlobalSymbol(symbol), + Time: orderBook.Time.Time(), Bids: orderBook.Bids, Asks: orderBook.Asks, }, sequence, nil diff --git a/pkg/exchange/kucoin/stream.go b/pkg/exchange/kucoin/stream.go index f6ad67f973..7b2bfe8b97 100644 --- a/pkg/exchange/kucoin/stream.go +++ b/pkg/exchange/kucoin/stream.go @@ -73,6 +73,7 @@ func (s *Stream) handleOrderBookL2Event(e *WebSocketOrderBookL2Event) { if ok { f.AddUpdate(types.SliceOrderBook{ Symbol: toGlobalSymbol(e.Symbol), + Time: e.Time.Time(), Bids: e.Changes.Bids, Asks: e.Changes.Asks, }, e.SequenceStart, e.SequenceEnd) diff --git a/pkg/exchange/kucoin/websocket.go b/pkg/exchange/kucoin/websocket.go index 4ec493aada..b5ddc8d51a 100644 --- a/pkg/exchange/kucoin/websocket.go +++ b/pkg/exchange/kucoin/websocket.go @@ -80,6 +80,7 @@ type WebSocketOrderBookL2Event struct { Asks types.PriceVolumeSlice `json:"asks"` Bids types.PriceVolumeSlice `json:"bids"` } `json:"changes"` + Time types.MillisecondTimestamp `json:"time"` } type WebSocketCandleEvent struct { diff --git a/pkg/exchange/max/maxapi/public_parser.go b/pkg/exchange/max/maxapi/public_parser.go index 66f0cf88a5..d96799f50e 100644 --- a/pkg/exchange/max/maxapi/public_parser.go +++ b/pkg/exchange/max/maxapi/public_parser.go @@ -81,25 +81,25 @@ type KLineEvent struct { } /* -{ - "c": "kline", - "M": "btcusdt", - "e": "update", - "T": 1602999650179, - "k": { - "ST": 1602999900000, - "ET": 1602999900000, - "M": "btcusdt", - "R": "5m", - "O": "11417.21", - "H": "11417.21", - "L": "11417.21", - "C": "11417.21", - "v": "0", - "ti": 0, - "x": false - } -} + { + "c": "kline", + "M": "btcusdt", + "e": "update", + "T": 1602999650179, + "k": { + "ST": 1602999900000, + "ET": 1602999900000, + "M": "btcusdt", + "R": "5m", + "O": "11417.21", + "H": "11417.21", + "L": "11417.21", + "C": "11417.21", + "v": "0", + "ti": 0, + "x": false + } + } */ type KLinePayload struct { StartTime int64 `json:"ST"` @@ -175,6 +175,7 @@ func (e *BookEvent) Time() time.Time { func (e *BookEvent) OrderBook() (snapshot types.SliceOrderBook, err error) { snapshot.Symbol = strings.ToUpper(e.Market) + snapshot.Time = e.Time() for _, bid := range e.Bids { pv, err := bid.PriceVolumePair() diff --git a/pkg/exchange/max/stream.go b/pkg/exchange/max/stream.go index 89f885d1df..27efbb51a9 100644 --- a/pkg/exchange/max/stream.go +++ b/pkg/exchange/max/stream.go @@ -188,6 +188,7 @@ func (s *Stream) handleBookEvent(e max.BookEvent) { } newBook.Symbol = toGlobalSymbol(e.Market) + newBook.Time = e.Time() switch e.Event { case "snapshot": diff --git a/pkg/exchange/okex/parse.go b/pkg/exchange/okex/parse.go index 6b9c019ed1..2505f17d63 100644 --- a/pkg/exchange/okex/parse.go +++ b/pkg/exchange/okex/parse.go @@ -81,6 +81,7 @@ func (data *BookEvent) BookTicker() types.BookTicker { func (data *BookEvent) Book() types.SliceOrderBook { book := types.SliceOrderBook{ Symbol: data.Symbol, + Time: types.NewMillisecondTimestampFromInt(data.MillisecondTimestamp).Time(), } for _, bid := range data.Bids { diff --git a/pkg/types/sliceorderbook.go b/pkg/types/sliceorderbook.go index 777e30333b..d48fb528ae 100644 --- a/pkg/types/sliceorderbook.go +++ b/pkg/types/sliceorderbook.go @@ -12,11 +12,14 @@ import ( // SliceOrderBook is a general order book structure which could be used // for RESTful responses and websocket stream parsing +// //go:generate callbackgen -type SliceOrderBook type SliceOrderBook struct { Symbol string Bids PriceVolumeSlice Asks PriceVolumeSlice + // Time represents the server time. If empty, it indicates that the server does not provide this information. + Time time.Time lastUpdateTime time.Time @@ -162,6 +165,8 @@ func (b *SliceOrderBook) String() string { sb.WriteString("BOOK ") sb.WriteString(b.Symbol) sb.WriteString("\n") + sb.WriteString(b.Time.Format(time.RFC1123)) + sb.WriteString("\n") if len(b.Asks) > 0 { sb.WriteString("ASKS:\n") @@ -187,6 +192,7 @@ func (b *SliceOrderBook) String() string { func (b *SliceOrderBook) CopyDepth(limit int) OrderBook { var book SliceOrderBook book.Symbol = b.Symbol + book.Time = b.Time book.Bids = b.Bids.CopyDepth(limit) book.Asks = b.Asks.CopyDepth(limit) return &book @@ -195,6 +201,7 @@ func (b *SliceOrderBook) CopyDepth(limit int) OrderBook { func (b *SliceOrderBook) Copy() OrderBook { var book SliceOrderBook book.Symbol = b.Symbol + book.Time = b.Time book.Bids = b.Bids.Copy() book.Asks = b.Asks.Copy() return &book From 412d0e0558992d434fd438a2e40b60c642d9ee0a Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 1 Sep 2023 17:51:09 +0800 Subject: [PATCH 1382/1392] *: fix lint --- pkg/cache/cache_test.go | 2 +- pkg/exchange/bybit/types_test.go | 2 +- pkg/migrations/mysql/migration_api_test.go | 4 ++-- pkg/migrations/sqlite3/migration_api_test.go | 4 ++-- pkg/strategy/tri/strategy.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 8e9c6db361..49ee23ab4f 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -100,4 +100,4 @@ func Test_loadMarketsFromMem(t *testing.T) { } globalMarketMemCache = newMarketMemCache() // reset the global cache -} \ No newline at end of file +} diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go index d3ae97643d..3601690fb8 100644 --- a/pkg/exchange/bybit/types_test.go +++ b/pkg/exchange/bybit/types_test.go @@ -524,7 +524,7 @@ func TestTradeEvent_toGlobalTrade(t *testing.T) { OrderId: fmt.Sprintf("%d", expTrade.OrderID), OrderLinkId: "1691419101980", Category: "spot", - Symbol: fmt.Sprintf("%s", expTrade.Symbol), + Symbol: expTrade.Symbol, ExecId: fmt.Sprintf("%d", expTrade.ID), ExecPrice: expTrade.Price, ExecQty: expTrade.Quantity, diff --git a/pkg/migrations/mysql/migration_api_test.go b/pkg/migrations/mysql/migration_api_test.go index 2684076d34..9864095ce0 100644 --- a/pkg/migrations/mysql/migration_api_test.go +++ b/pkg/migrations/mysql/migration_api_test.go @@ -14,7 +14,7 @@ func TestGetMigrationsMap(t *testing.T) { func TestMergeMigrationsMap(t *testing.T) { MergeMigrationsMap(map[int64]*rockhopper.Migration{ - 2: &rockhopper.Migration{}, - 3: &rockhopper.Migration{}, + 2: {}, + 3: {}, }) } diff --git a/pkg/migrations/sqlite3/migration_api_test.go b/pkg/migrations/sqlite3/migration_api_test.go index d1f4fe1ab0..d7f77c875c 100644 --- a/pkg/migrations/sqlite3/migration_api_test.go +++ b/pkg/migrations/sqlite3/migration_api_test.go @@ -14,7 +14,7 @@ func TestGetMigrationsMap(t *testing.T) { func TestMergeMigrationsMap(t *testing.T) { MergeMigrationsMap(map[int64]*rockhopper.Migration{ - 2: &rockhopper.Migration{}, - 3: &rockhopper.Migration{}, + 2: {}, + 3: {}, }) } diff --git a/pkg/strategy/tri/strategy.go b/pkg/strategy/tri/strategy.go index ec8b13e7fe..c96336106a 100644 --- a/pkg/strategy/tri/strategy.go +++ b/pkg/strategy/tri/strategy.go @@ -484,7 +484,7 @@ func (s *Strategy) executePath(ctx context.Context, session *bbgo.ExchangeSessio } func notifyUsdPnL(profit fixedpoint.Value) { - var title = fmt.Sprintf("Triangular Sum PnL ~= ") + var title = "Triangular Sum PnL ~= " title += style.PnLEmojiSimple(profit) + " " title += style.PnLSignString(profit) + " USD" bbgo.Notify(title) From 57198cc6b092619dea38f137bde6310347eb044d Mon Sep 17 00:00:00 2001 From: narumi Date: Thu, 31 Aug 2023 14:50:50 +0800 Subject: [PATCH 1383/1392] fix: reset profit stats when over given duration in circuit break risk control --- doc/topics/riskcontrols.md | 5 +- pkg/risk/riskcontrol/circuit_break.go | 53 ++++++++++++++++------ pkg/risk/riskcontrol/circuit_break_test.go | 11 +++-- pkg/strategy/common/strategy.go | 4 +- pkg/strategy/scmaker/strategy.go | 16 ++++--- pkg/types/profit.go | 7 +-- 6 files changed, 63 insertions(+), 33 deletions(-) diff --git a/doc/topics/riskcontrols.md b/doc/topics/riskcontrols.md index 90dfea94e5..6a03885bd0 100644 --- a/doc/topics/riskcontrols.md +++ b/doc/topics/riskcontrols.md @@ -51,7 +51,8 @@ s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( s.Position, session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), s.CircuitBreakLossThreshold, - s.ProfitStats) + s.ProfitStats, + 24*time.Hour) ``` Should pass in position and profit states. Also need an price EWMA to calculate unrealized profit. @@ -71,7 +72,7 @@ Circuit break condition should be non-greater than zero. Check for circuit break before submitting orders: ``` // Circuit break when accumulated losses are over break condition - if s.circuitBreakRiskControl.IsHalted() { + if s.circuitBreakRiskControl.IsHalted(kline.EndTime) { return } diff --git a/pkg/risk/riskcontrol/circuit_break.go b/pkg/risk/riskcontrol/circuit_break.go index 753d8d2367..921696e21f 100644 --- a/pkg/risk/riskcontrol/circuit_break.go +++ b/pkg/risk/riskcontrol/circuit_break.go @@ -1,41 +1,68 @@ package riskcontrol import ( + "time" + log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/indicator/v2" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/types" ) type CircuitBreakRiskControl struct { // Since price could be fluctuated large, // use an EWMA to smooth it in running time - price *indicatorv2.EWMAStream - position *types.Position - profitStats *types.ProfitStats - lossThreshold fixedpoint.Value + price *indicatorv2.EWMAStream + position *types.Position + profitStats *types.ProfitStats + lossThreshold fixedpoint.Value + haltedDuration time.Duration + + isHalted bool + haltedAt time.Time } func NewCircuitBreakRiskControl( position *types.Position, price *indicatorv2.EWMAStream, lossThreshold fixedpoint.Value, - profitStats *types.ProfitStats) *CircuitBreakRiskControl { - + profitStats *types.ProfitStats, + haltedDuration time.Duration, +) *CircuitBreakRiskControl { return &CircuitBreakRiskControl{ - price: price, - position: position, - profitStats: profitStats, - lossThreshold: lossThreshold, + price: price, + position: position, + profitStats: profitStats, + lossThreshold: lossThreshold, + haltedDuration: haltedDuration, } } +func (c *CircuitBreakRiskControl) IsOverHaltedDuration() bool { + return time.Since(c.haltedAt) >= c.haltedDuration +} + // IsHalted returns whether we reached the circuit break condition set for this day? -func (c *CircuitBreakRiskControl) IsHalted() bool { +func (c *CircuitBreakRiskControl) IsHalted(t time.Time) bool { + if c.profitStats.IsOver24Hours() { + c.profitStats.ResetToday(t) + } + + // if we are not over the halted duration, we don't need to check the condition + if !c.IsOverHaltedDuration() { + return false + } + var unrealized = c.position.UnrealizedProfit(fixedpoint.NewFromFloat(c.price.Last(0))) log.Infof("[CircuitBreakRiskControl] realized PnL = %f, unrealized PnL = %f\n", c.profitStats.TodayPnL.Float64(), unrealized.Float64()) - return unrealized.Add(c.profitStats.TodayPnL).Compare(c.lossThreshold) <= 0 + + c.isHalted = unrealized.Add(c.profitStats.TodayPnL).Compare(c.lossThreshold) <= 0 + if c.isHalted { + c.haltedAt = t + } + + return c.isHalted } diff --git a/pkg/risk/riskcontrol/circuit_break_test.go b/pkg/risk/riskcontrol/circuit_break_test.go index 0e5d00ddad..54f523d4b1 100644 --- a/pkg/risk/riskcontrol/circuit_break_test.go +++ b/pkg/risk/riskcontrol/circuit_break_test.go @@ -2,6 +2,7 @@ package riskcontrol import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -68,11 +69,13 @@ func Test_IsHalted(t *testing.T) { }, priceEWMA, breakCondition, - &types.ProfitStats{ - TodayPnL: realizedPnL, - }, + &types.ProfitStats{}, + 24*time.Hour, ) - assert.Equal(t, tc.isHalted, riskControl.IsHalted()) + now := time.Now() + riskControl.profitStats.ResetToday(now) + riskControl.profitStats.TodayPnL = realizedPnL + assert.Equal(t, tc.isHalted, riskControl.IsHalted(now.Add(time.Hour))) }) } } diff --git a/pkg/strategy/common/strategy.go b/pkg/strategy/common/strategy.go index 5e2cd2c4d5..4f159c7f62 100644 --- a/pkg/strategy/common/strategy.go +++ b/pkg/strategy/common/strategy.go @@ -2,6 +2,7 @@ package common import ( "context" + "time" log "github.com/sirupsen/logrus" @@ -83,6 +84,7 @@ func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, se s.Position, session.Indicators(market.Symbol).EWMA(s.CircuitBreakEMA), s.CircuitBreakLossThreshold, - s.ProfitStats) + s.ProfitStats, + 24*time.Hour) } } diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 3abfad0816..ecccefc8b0 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "sync" + "time" log "github.com/sirupsen/logrus" @@ -123,7 +124,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position, session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), s.CircuitBreakLossThreshold, - s.ProfitStats) + s.ProfitStats, + 24*time.Hour) } scale, err := s.LiquiditySlideRule.Scale() @@ -275,18 +277,18 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { } func (s *Strategy) placeLiquidityOrders(ctx context.Context) { - if s.circuitBreakRiskControl != nil && s.circuitBreakRiskControl.IsHalted() { - log.Warn("circuitBreakRiskControl: trading halted") + ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol) + if logErr(err, "unable to query ticker") { return } - err := s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange) - if logErr(err, "unable to cancel orders") { + if s.circuitBreakRiskControl != nil && s.circuitBreakRiskControl.IsHalted(ticker.Time) { + log.Warn("circuitBreakRiskControl: trading halted") return } - ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol) - if logErr(err, "unable to query ticker") { + err = s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange) + if logErr(err, "unable to cancel orders") { return } diff --git a/pkg/types/profit.go b/pkg/types/profit.go index 9feb4edd76..c7742e0b8f 100644 --- a/pkg/types/profit.go +++ b/pkg/types/profit.go @@ -240,14 +240,9 @@ func (s *ProfitStats) AddTrade(trade Trade) { s.AccumulatedVolume = s.AccumulatedVolume.Add(trade.Quantity) } -// IsOver checks if the since time is over given duration -func (s *ProfitStats) IsOver(d time.Duration) bool { - return time.Since(time.Unix(s.TodaySince, 0)) >= d -} - // IsOver24Hours checks if the since time is over 24 hours func (s *ProfitStats) IsOver24Hours() bool { - return s.IsOver(24 * time.Hour) + return time.Since(time.Unix(s.TodaySince, 0)) >= 24*time.Hour } func (s *ProfitStats) ResetToday(t time.Time) { From f5a66baad3a09944eee9a91c4c9c5acf838cc43e Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 5 Sep 2023 22:04:16 +0800 Subject: [PATCH 1384/1392] pkg/exchage: set default 30d for closed order batch query --- pkg/exchange/batch/closedorders.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/exchange/batch/closedorders.go b/pkg/exchange/batch/closedorders.go index 3af58e2d5f..51d12f5ff1 100644 --- a/pkg/exchange/batch/closedorders.go +++ b/pkg/exchange/batch/closedorders.go @@ -29,6 +29,7 @@ func (q *ClosedOrderBatchQuery) Query(ctx context.Context, symbol string, startT } return strconv.FormatUint(order.OrderID, 10) }, + JumpIfEmpty: 30 * 24 * time.Hour, } c = make(chan types.Order, 100) From 83cdd4e1a47e0224acf53a155c8c57ddfcd8a6c2 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 6 Sep 2023 12:18:17 +0800 Subject: [PATCH 1385/1392] pkg/exchange: update add reconnect and resubscribe func for stream --- pkg/types/stream.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pkg/types/stream.go b/pkg/types/stream.go index 668a12856e..5b481d0d94 100644 --- a/pkg/types/stream.go +++ b/pkg/types/stream.go @@ -27,14 +27,26 @@ var defaultDialer = &websocket.Dialer{ type Stream interface { StandardStreamEventHub + // Subscribe subscribes the specific channel, but not connect to the server. Subscribe(channel Channel, symbol string, options SubscribeOptions) GetSubscriptions() []Subscription + // Resubscribe used to update or renew existing subscriptions. It will reconnect to the server. + Resubscribe(func(oldSubs []Subscription) (newSubs []Subscription, err error)) error + // SetPublicOnly connects to public or private SetPublicOnly() GetPublicOnly() bool + + // Connect connects to websocket server Connect(ctx context.Context) error + Reconnect() Close() error } +type Unsubscriber interface { + // Unsubscribe unsubscribes the all subscriptions. + Unsubscribe() +} + type EndpointCreator func(ctx context.Context) (string, error) type Parser func(message []byte) (interface{}, error) @@ -76,6 +88,10 @@ type StandardStream struct { Subscriptions []Subscription + // subLock is used for locking Subscriptions fields. + // When changing these field values, be sure to call subLock + subLock sync.Mutex + startCallbacks []func() connectCallbacks []func() @@ -290,10 +306,34 @@ func (s *StandardStream) ping(ctx context.Context, conn *websocket.Conn, cancel } func (s *StandardStream) GetSubscriptions() []Subscription { + s.subLock.Lock() + defer s.subLock.Unlock() + return s.Subscriptions } +// Resubscribe synchronizes the new subscriptions based on the provided function. +// The fn function takes the old subscriptions as input and returns the new subscriptions that will replace the old ones +// in the struct then Reconnect. +// This method is thread-safe. +func (s *StandardStream) Resubscribe(fn func(old []Subscription) (new []Subscription, err error)) error { + s.subLock.Lock() + defer s.subLock.Unlock() + + var err error + subs, err := fn(s.Subscriptions) + if err != nil { + return err + } + s.Subscriptions = subs + s.Reconnect() + return nil +} + func (s *StandardStream) Subscribe(channel Channel, symbol string, options SubscribeOptions) { + s.subLock.Lock() + defer s.subLock.Unlock() + s.Subscriptions = append(s.Subscriptions, Subscription{ Channel: channel, Symbol: symbol, From da13bb680e4d61caabee930509148156d67ea913 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 6 Sep 2023 12:18:47 +0800 Subject: [PATCH 1386/1392] pkg/exchange: support unsubscribe for bybit --- pkg/exchange/bybit/stream.go | 76 +++++++++++++++++++------------ pkg/exchange/bybit/stream_test.go | 50 ++++++++++++++++++++ pkg/exchange/bybit/types.go | 18 ++++++-- pkg/exchange/bybit/types_test.go | 26 +++++++++++ 4 files changed, 138 insertions(+), 32 deletions(-) diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 0a42dd42a4..698fd239d6 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -73,6 +73,52 @@ func NewStream(key, secret string, marketProvider MarketInfoProvider) *Stream { return stream } +func (s *Stream) syncSubscriptions(opType WsOpType) error { + if opType != WsOpTypeUnsubscribe && opType != WsOpTypeSubscribe { + return fmt.Errorf("unexpected subscription type: %v", opType) + } + + logger := log.WithField("opType", opType) + lens := len(s.Subscriptions) + for begin := 0; begin < lens; begin += spotArgsLimit { + end := begin + spotArgsLimit + if end > lens { + end = lens + } + + topics := []string{} + for _, subscription := range s.Subscriptions[begin:end] { + topic, err := s.convertSubscription(subscription) + if err != nil { + logger.WithError(err).Errorf("convert error, subscription: %+v", subscription) + return err + } + + topics = append(topics, topic) + } + + logger.Infof("%s channels: %+v", opType, topics) + if err := s.Conn.WriteJSON(WebsocketOp{ + Op: opType, + Args: topics, + }); err != nil { + logger.WithError(err).Error("failed to send request") + return err + } + } + + return nil +} + +func (s *Stream) Unsubscribe() { + // errors are handled in the syncSubscriptions, so they are skipped here. + _ = s.syncSubscriptions(WsOpTypeUnsubscribe) + s.Resubscribe(func(old []types.Subscription) (new []types.Subscription, err error) { + // clear the subscriptions + return []types.Subscription{}, nil + }) +} + func (s *Stream) createEndpoint(_ context.Context) (string, error) { var url string if s.PublicOnly { @@ -204,34 +250,8 @@ func (s *Stream) ping(ctx context.Context, conn *websocket.Conn, cancelFunc cont func (s *Stream) handlerConnect() { if s.PublicOnly { - if len(s.Subscriptions) == 0 { - log.Debug("no subscriptions") - return - } - - var topics []string - - for _, subscription := range s.Subscriptions { - topic, err := s.convertSubscription(subscription) - if err != nil { - log.WithError(err).Errorf("subscription convert error") - continue - } - - topics = append(topics, topic) - } - if len(topics) > spotArgsLimit { - log.Debugf("topics exceeds limit: %d, drop of: %v", spotArgsLimit, topics[spotArgsLimit:]) - topics = topics[:spotArgsLimit] - } - log.Infof("subscribing channels: %+v", topics) - if err := s.Conn.WriteJSON(WebsocketOp{ - Op: WsOpTypeSubscribe, - Args: topics, - }); err != nil { - log.WithError(err).Error("failed to send subscription request") - return - } + // errors are handled in the syncSubscriptions, so they are skipped here. + _ = s.syncSubscriptions(WsOpTypeSubscribe) } else { expires := strconv.FormatInt(time.Now().Add(wsAuthRequest).In(time.UTC).UnixMilli(), 10) diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go index 195c6fea2f..b0ebc9d0db 100644 --- a/pkg/exchange/bybit/stream_test.go +++ b/pkg/exchange/bybit/stream_test.go @@ -7,6 +7,7 @@ import ( "os" "strconv" "testing" + "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -37,6 +38,20 @@ func getTestClientOrSkip(t *testing.T) *Stream { func TestStream(t *testing.T) { s := getTestClientOrSkip(t) + symbols := []string{ + "BTCUSDT", + "ETHUSDT", + "DOTUSDT", + "ADAUSDT", + "AAVEUSDT", + "APTUSDT", + "ATOMUSDT", + "AXSUSDT", + "BNBUSDT", + "SOLUSDT", + "DOGEUSDT", + } + t.Run("Auth test", func(t *testing.T) { s.Connect(context.Background()) c := make(chan struct{}) @@ -61,6 +76,41 @@ func TestStream(t *testing.T) { <-c }) + t.Run("book test on unsubscribe and reconnect", func(t *testing.T) { + for _, symbol := range symbols { + s.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{ + Depth: types.DepthLevel50, + }) + } + + s.SetPublicOnly() + err := s.Connect(context.Background()) + assert.NoError(t, err) + + s.OnBookSnapshot(func(book types.SliceOrderBook) { + t.Log("got snapshot", book) + }) + s.OnBookUpdate(func(book types.SliceOrderBook) { + t.Log("got update", book) + }) + + <-time.After(2 * time.Second) + + s.Unsubscribe() + for _, symbol := range symbols { + s.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{ + Depth: types.DepthLevel50, + }) + } + + <-time.After(2 * time.Second) + + s.Reconnect() + + c := make(chan struct{}) + <-c + }) + t.Run("wallet test", func(t *testing.T) { err := s.Connect(context.Background()) assert.NoError(t, err) diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index 6b62f6563e..20a8d39113 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -28,10 +28,11 @@ func (w *WsEvent) IsTopic() bool { type WsOpType string const ( - WsOpTypePing WsOpType = "ping" - WsOpTypePong WsOpType = "pong" - WsOpTypeAuth WsOpType = "auth" - WsOpTypeSubscribe WsOpType = "subscribe" + WsOpTypePing WsOpType = "ping" + WsOpTypePong WsOpType = "pong" + WsOpTypeAuth WsOpType = "auth" + WsOpTypeSubscribe WsOpType = "subscribe" + WsOpTypeUnsubscribe WsOpType = "unsubscribe" ) type WebsocketOp struct { @@ -72,6 +73,15 @@ func (w *WebSocketOpEvent) IsValid() error { return fmt.Errorf("unexpected response result: %+v", w) } return nil + + case WsOpTypeUnsubscribe: + // in the public channel, you can get RetMsg = 'subscribe', but in the private channel, you cannot. + // so, we only verify that success is true. + if !w.Success { + return fmt.Errorf("unexpected response result: %+v", w) + } + return nil + default: return fmt.Errorf("unexpected op type: %+v", w) } diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go index d3ae97643d..443b6c0aa6 100644 --- a/pkg/exchange/bybit/types_test.go +++ b/pkg/exchange/bybit/types_test.go @@ -189,6 +189,19 @@ func Test_WebSocketEventIsValid(t *testing.T) { assert.NoError(t, w.IsValid()) }) + t.Run("[unsubscribe] valid with public channel", func(t *testing.T) { + expRetMsg := "subscribe" + w := &WebSocketOpEvent{ + Success: true, + RetMsg: expRetMsg, + ReqId: "", + ConnId: "test-conndid", + Op: WsOpTypeUnsubscribe, + Args: nil, + } + assert.NoError(t, w.IsValid()) + }) + t.Run("[subscribe] valid with private channel", func(t *testing.T) { w := &WebSocketOpEvent{ Success: true, @@ -214,6 +227,19 @@ func Test_WebSocketEventIsValid(t *testing.T) { assert.Equal(t, fmt.Errorf("unexpected response result: %+v", w), w.IsValid()) }) + t.Run("[unsubscribe] un-succeeds", func(t *testing.T) { + expRetMsg := "" + w := &WebSocketOpEvent{ + Success: false, + RetMsg: expRetMsg, + ReqId: "", + ConnId: "test-conndid", + Op: WsOpTypeUnsubscribe, + Args: nil, + } + assert.Equal(t, fmt.Errorf("unexpected response result: %+v", w), w.IsValid()) + }) + t.Run("[auth] valid", func(t *testing.T) { w := &WebSocketOpEvent{ Success: true, From 7550ea2be1757fea78d31771b6911ebcd235a453 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Wed, 6 Sep 2023 19:14:21 +0800 Subject: [PATCH 1387/1392] refactor okex to future use --- examples/okex-book/main.go | 37 +++--- pkg/exchange/factory.go | 2 +- pkg/exchange/okex/exchange.go | 31 ++--- pkg/exchange/okex/okexapi/client.go | 105 ++++++---------- pkg/exchange/okex/okexapi/client_test.go | 25 ++-- pkg/exchange/okex/okexapi/market.go | 70 +++++------ pkg/exchange/okex/okexapi/public.go | 47 ++++--- pkg/exchange/okex/okexapi/trade.go | 149 +++++++++++------------ pkg/exchange/okex/query_order_test.go | 3 +- 9 files changed, 213 insertions(+), 256 deletions(-) diff --git a/examples/okex-book/main.go b/examples/okex-book/main.go index 77218d81c4..e6e7341cc3 100644 --- a/examples/okex-book/main.go +++ b/examples/okex-book/main.go @@ -44,10 +44,13 @@ var rootCmd = &cobra.Command{ return errors.New("empty key, secret or passphrase") } - client := okexapi.NewClient() + client, err := okexapi.NewClient() + if err != nil { + return errors.New("init client error: please check url") + } client.Auth(key, secret, passphrase) - instruments, err := client.PublicDataService.NewGetInstrumentsRequest(). + instruments, err := client.NewGetInstrumentsRequest(). InstrumentType("SPOT").Do(ctx) if err != nil { return err @@ -55,14 +58,14 @@ var rootCmd = &cobra.Command{ log.Infof("instruments: %+v", instruments) - fundingRate, err := client.PublicDataService.NewGetFundingRate().InstrumentID("BTC-USDT-SWAP").Do(ctx) + fundingRate, err := client.NewGetFundingRate().InstrumentID("BTC-USDT-SWAP").Do(ctx) if err != nil { return err } log.Infof("funding rate: %+v", fundingRate) log.Infof("ACCOUNT BALANCES:") - account, err := client.AccountBalances() + account, err := client.AccountBalances(ctx) if err != nil { return err } @@ -70,7 +73,7 @@ var rootCmd = &cobra.Command{ log.Infof("%+v", account) log.Infof("ASSET BALANCES:") - assetBalances, err := client.AssetBalances() + assetBalances, err := client.AssetBalances(ctx) if err != nil { return err } @@ -80,7 +83,7 @@ var rootCmd = &cobra.Command{ } log.Infof("ASSET CURRENCIES:") - currencies, err := client.AssetCurrencies() + currencies, err := client.AssetCurrencies(ctx) if err != nil { return err } @@ -90,7 +93,7 @@ var rootCmd = &cobra.Command{ } log.Infof("MARKET TICKERS:") - tickers, err := client.MarketTickers(okexapi.InstrumentTypeSpot) + tickers, err := client.MarketTickers(ctx, okexapi.InstrumentTypeSpot) if err != nil { return err } @@ -99,7 +102,7 @@ var rootCmd = &cobra.Command{ log.Infof("%T%+v", ticker, ticker) } - ticker, err := client.MarketTicker("ETH-USDT") + ticker, err := client.MarketTicker(ctx, "ETH-USDT") if err != nil { return err } @@ -107,7 +110,7 @@ var rootCmd = &cobra.Command{ log.Infof("%T%+v", ticker, ticker) log.Infof("PLACING ORDER:") - placeResponse, err := client.TradeService.NewPlaceOrderRequest(). + placeResponse, err := client.NewPlaceOrderRequest(). InstrumentID("LTC-USDT"). OrderType(okexapi.OrderTypeLimit). Side(okexapi.SideTypeBuy). @@ -122,7 +125,7 @@ var rootCmd = &cobra.Command{ time.Sleep(time.Second) log.Infof("getting order detail...") - orderDetail, err := client.TradeService.NewGetOrderDetailsRequest(). + orderDetail, err := client.NewGetOrderDetailsRequest(). InstrumentID("LTC-USDT"). OrderID(placeResponse.OrderID). Do(ctx) @@ -132,7 +135,7 @@ var rootCmd = &cobra.Command{ log.Infof("order detail: %+v", orderDetail) - cancelResponse, err := client.TradeService.NewCancelOrderRequest(). + cancelResponse, err := client.NewCancelOrderRequest(). InstrumentID("LTC-USDT"). OrderID(placeResponse.OrderID). Do(ctx) @@ -144,15 +147,15 @@ var rootCmd = &cobra.Command{ time.Sleep(time.Second) log.Infof("BATCH PLACE ORDER:") - batchPlaceReq := client.TradeService.NewBatchPlaceOrderRequest() - batchPlaceReq.Add(client.TradeService.NewPlaceOrderRequest(). + batchPlaceReq := client.NewBatchPlaceOrderRequest() + batchPlaceReq.Add(client.NewPlaceOrderRequest(). InstrumentID("LTC-USDT"). OrderType(okexapi.OrderTypeLimit). Side(okexapi.SideTypeBuy). Price("50.0"). Quantity("0.5")) - batchPlaceReq.Add(client.TradeService.NewPlaceOrderRequest(). + batchPlaceReq.Add(client.NewPlaceOrderRequest(). InstrumentID("LTC-USDT"). OrderType(okexapi.OrderTypeLimit). Side(okexapi.SideTypeBuy). @@ -168,7 +171,7 @@ var rootCmd = &cobra.Command{ time.Sleep(time.Second) log.Infof("getting pending orders...") - pendingOrders, err := client.TradeService.NewGetPendingOrderRequest().Do(ctx) + pendingOrders, err := client.NewGetPendingOrderRequest().Do(ctx) if err != nil { return err } @@ -176,9 +179,9 @@ var rootCmd = &cobra.Command{ log.Infof("pending order: %+v", pendingOrder) } - cancelReq := client.TradeService.NewBatchCancelOrderRequest() + cancelReq := client.NewBatchCancelOrderRequest() for _, resp := range batchPlaceResponse { - cancelReq.Add(client.TradeService.NewCancelOrderRequest(). + cancelReq.Add(client.NewCancelOrderRequest(). InstrumentID("LTC-USDT"). OrderID(resp.OrderID)) } diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index 00d6db7c9f..c7f3371cab 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -37,7 +37,7 @@ func New(n types.ExchangeName, key, secret, passphrase string) (types.ExchangeMi return max.New(key, secret), nil case types.ExchangeOKEx: - return okex.New(key, secret, passphrase), nil + return okex.New(key, secret, passphrase) case types.ExchangeKucoin: return kucoin.New(key, secret, passphrase), nil diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 700b958378..92e3221160 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -34,8 +34,11 @@ type Exchange struct { client *okexapi.RestClient } -func New(key, secret, passphrase string) *Exchange { - client := okexapi.NewClient() +func New(key, secret, passphrase string) (*Exchange, error) { + client, err := okexapi.NewClient() + if err != nil { + return nil, err + } if len(key) > 0 && len(secret) > 0 { client.Auth(key, secret, passphrase) @@ -46,7 +49,7 @@ func New(key, secret, passphrase string) *Exchange { secret: secret, passphrase: passphrase, client: client, - } + }, nil } func (e *Exchange) Name() types.ExchangeName { @@ -54,7 +57,7 @@ func (e *Exchange) Name() types.ExchangeName { } func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { - instruments, err := e.client.PublicDataService.NewGetInstrumentsRequest(). + instruments, err := e.client.NewGetInstrumentsRequest(). InstrumentType(okexapi.InstrumentTypeSpot). Do(ctx) @@ -98,7 +101,7 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { symbol = toLocalSymbol(symbol) - marketTicker, err := e.client.MarketTicker(symbol) + marketTicker, err := e.client.MarketTicker(ctx, symbol) if err != nil { return nil, err } @@ -107,7 +110,7 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke } func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[string]types.Ticker, error) { - marketTickers, err := e.client.MarketTickers(okexapi.InstrumentTypeSpot) + marketTickers, err := e.client.MarketTickers(ctx, okexapi.InstrumentTypeSpot) if err != nil { return nil, err } @@ -138,7 +141,7 @@ func (e *Exchange) PlatformFeeCurrency() string { } func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { - accountBalance, err := e.client.AccountBalances() + accountBalance, err := e.client.AccountBalances(ctx) if err != nil { return nil, err } @@ -153,7 +156,7 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { } func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { - accountBalances, err := e.client.AccountBalances() + accountBalances, err := e.client.AccountBalances(ctx) if err != nil { return nil, err } @@ -163,7 +166,7 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, } func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { - orderReq := e.client.TradeService.NewPlaceOrderRequest() + orderReq := e.client.NewPlaceOrderRequest() orderType, err := toLocalOrderType(order.Type) if err != nil { @@ -257,7 +260,7 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { instrumentID := toLocalSymbol(symbol) - req := e.client.TradeService.NewGetPendingOrderRequest().InstrumentType(okexapi.InstrumentTypeSpot).InstrumentID(instrumentID) + req := e.client.NewGetPendingOrderRequest().InstrumentType(okexapi.InstrumentTypeSpot).InstrumentID(instrumentID) orderDetails, err := req.Do(ctx) if err != nil { return orders, err @@ -278,7 +281,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) erro return ErrSymbolRequired } - req := e.client.TradeService.NewCancelOrderRequest() + req := e.client.NewCancelOrderRequest() req.InstrumentID(toLocalSymbol(order.Symbol)) req.OrderID(strconv.FormatUint(order.OrderID, 10)) if len(order.ClientOrderID) > 0 { @@ -287,7 +290,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) erro reqs = append(reqs, req) } - batchReq := e.client.TradeService.NewBatchCancelOrderRequest() + batchReq := e.client.NewBatchCancelOrderRequest() batchReq.Add(reqs...) _, err := batchReq.Do(ctx) return err @@ -304,7 +307,7 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type intervalParam := toLocalInterval(interval.String()) - req := e.client.MarketDataService.NewCandlesticksRequest(toLocalSymbol(symbol)) + req := e.client.NewCandlesticksRequest(toLocalSymbol(symbol)) req.Bar(intervalParam) if options.StartTime != nil { @@ -349,7 +352,7 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O if len(q.OrderID) == 0 && len(q.ClientOrderID) == 0 { return nil, errors.New("okex.QueryOrder: OrderId or ClientOrderId is required parameter") } - req := e.client.TradeService.NewGetOrderDetailsRequest() + req := e.client.NewGetOrderDetailsRequest() req.InstrumentID(q.Symbol). OrderID(q.OrderID). ClientOrderID(q.ClientOrderID) diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index ccb81f70a5..5d05330511 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -2,6 +2,7 @@ package okexapi import ( "bytes" + "context" "crypto/hmac" "crypto/sha256" "encoding/base64" @@ -14,7 +15,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/requestgen" "github.com/pkg/errors" ) @@ -60,34 +61,26 @@ const ( ) type RestClient struct { - BaseURL *url.URL - - client *http.Client + requestgen.BaseAPIClient Key, Secret, Passphrase string - - TradeService *TradeService - PublicDataService *PublicDataService - MarketDataService *MarketDataService } -func NewClient() *RestClient { +func NewClient() (*RestClient, error) { u, err := url.Parse(RestBaseURL) if err != nil { - panic(err) + return nil, err } client := &RestClient{ - BaseURL: u, - client: &http.Client{ - Timeout: defaultHTTPTimeout, + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: &http.Client{ + Timeout: defaultHTTPTimeout, + }, }, } - - client.TradeService = &TradeService{client: client} - client.PublicDataService = &PublicDataService{client: client} - client.MarketDataService = &MarketDataService{client: client} - return client + return client, nil } func (c *RestClient) Auth(key, secret, passphrase string) { @@ -97,44 +90,8 @@ func (c *RestClient) Auth(key, secret, passphrase string) { c.Passphrase = passphrase } -// NewRequest create new API request. Relative url can be provided in refURL. -func (c *RestClient) newRequest(method, refURL string, params url.Values, body []byte) (*http.Request, error) { - rel, err := url.Parse(refURL) - if err != nil { - return nil, err - } - - if params != nil { - rel.RawQuery = params.Encode() - } - - pathURL := c.BaseURL.ResolveReference(rel) - return http.NewRequest(method, pathURL.String(), bytes.NewReader(body)) -} - -// sendRequest sends the request to the API server and handle the response -func (c *RestClient) sendRequest(req *http.Request) (*util.Response, error) { - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - // newResponse reads the response body and return a new Response object - response, err := util.NewResponse(resp) - if err != nil { - return response, err - } - - // Check error, if there is an error, return the ErrorResponse struct type - if response.IsError() { - return response, errors.New(string(response.Body)) - } - - return response, nil -} - -// newAuthenticatedRequest creates new http request for authenticated routes. -func (c *RestClient) newAuthenticatedRequest(method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { +// NewAuthenticatedRequest creates new http request for authenticated routes. +func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { if len(c.Key) == 0 { return nil, errors.New("empty api key") } @@ -215,13 +172,13 @@ type Account struct { Details []BalanceDetail `json:"details"` } -func (c *RestClient) AccountBalances() (*Account, error) { - req, err := c.newAuthenticatedRequest("GET", "/api/v5/account/balance", nil, nil) +func (c *RestClient) AccountBalances(ctx context.Context) (*Account, error) { + req, err := c.NewAuthenticatedRequest(ctx, "GET", "/api/v5/account/balance", nil, nil) if err != nil { return nil, err } - response, err := c.sendRequest(req) + response, err := c.SendRequest(req) if err != nil { return nil, err } @@ -252,13 +209,13 @@ type AssetBalance struct { type AssetBalanceList []AssetBalance -func (c *RestClient) AssetBalances() (AssetBalanceList, error) { - req, err := c.newAuthenticatedRequest("GET", "/api/v5/asset/balances", nil, nil) +func (c *RestClient) AssetBalances(ctx context.Context) (AssetBalanceList, error) { + req, err := c.NewAuthenticatedRequest(ctx, "GET", "/api/v5/asset/balances", nil, nil) if err != nil { return nil, err } - response, err := c.sendRequest(req) + response, err := c.SendRequest(req) if err != nil { return nil, err } @@ -287,13 +244,13 @@ type AssetCurrency struct { MinWithdrawalThreshold fixedpoint.Value `json:"minWd"` } -func (c *RestClient) AssetCurrencies() ([]AssetCurrency, error) { - req, err := c.newAuthenticatedRequest("GET", "/api/v5/asset/currencies", nil, nil) +func (c *RestClient) AssetCurrencies(ctx context.Context) ([]AssetCurrency, error) { + req, err := c.NewAuthenticatedRequest(ctx, "GET", "/api/v5/asset/currencies", nil, nil) if err != nil { return nil, err } - response, err := c.sendRequest(req) + response, err := c.SendRequest(req) if err != nil { return nil, err } @@ -337,17 +294,17 @@ type MarketTicker struct { Timestamp types.MillisecondTimestamp `json:"ts"` } -func (c *RestClient) MarketTicker(instId string) (*MarketTicker, error) { +func (c *RestClient) MarketTicker(ctx context.Context, instId string) (*MarketTicker, error) { // SPOT, SWAP, FUTURES, OPTION var params = url.Values{} params.Add("instId", instId) - req, err := c.newRequest("GET", "/api/v5/market/ticker", params, nil) + req, err := c.NewRequest(ctx, "GET", "/api/v5/market/ticker", params, nil) if err != nil { return nil, err } - response, err := c.sendRequest(req) + response, err := c.SendRequest(req) if err != nil { return nil, err } @@ -368,17 +325,17 @@ func (c *RestClient) MarketTicker(instId string) (*MarketTicker, error) { return &tickerResponse.Data[0], nil } -func (c *RestClient) MarketTickers(instType InstrumentType) ([]MarketTicker, error) { +func (c *RestClient) MarketTickers(ctx context.Context, instType InstrumentType) ([]MarketTicker, error) { // SPOT, SWAP, FUTURES, OPTION var params = url.Values{} params.Add("instType", string(instType)) - req, err := c.newRequest("GET", "/api/v5/market/tickers", params, nil) + req, err := c.NewRequest(ctx, "GET", "/api/v5/market/tickers", params, nil) if err != nil { return nil, err } - response, err := c.sendRequest(req) + response, err := c.SendRequest(req) if err != nil { return nil, err } @@ -405,3 +362,9 @@ func Sign(payload string, secret string) string { return base64.StdEncoding.EncodeToString(sig.Sum(nil)) // return hex.EncodeToString(sig.Sum(nil)) } + +type APIResponse struct { + Code string `json:"code"` + Message string `json:"msg"` + Data json.RawMessage `json:"data"` +} diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index ec6841642a..8169450a93 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -22,17 +22,17 @@ func getTestClientOrSkip(t *testing.T) *RestClient { return nil } - client := NewClient() + client, err := NewClient() + assert.NoError(t, err) client.Auth(key, secret, passphrase) return client } func TestClient_GetInstrumentsRequest(t *testing.T) { - client := NewClient() + client, err := NewClient() + assert.NoError(t, err) ctx := context.Background() - - srv := &PublicDataService{client: client} - req := srv.NewGetInstrumentsRequest() + req := client.NewGetInstrumentsRequest() instruments, err := req. InstrumentType(InstrumentTypeSpot). @@ -43,10 +43,10 @@ func TestClient_GetInstrumentsRequest(t *testing.T) { } func TestClient_GetFundingRateRequest(t *testing.T) { - client := NewClient() + client, err := NewClient() + assert.NoError(t, err) ctx := context.Background() - srv := &PublicDataService{client: client} - req := srv.NewGetFundingRate() + req := client.NewGetFundingRate() instrument, err := req. InstrumentID("BTC-USDT-SWAP"). @@ -59,8 +59,7 @@ func TestClient_GetFundingRateRequest(t *testing.T) { func TestClient_PlaceOrderRequest(t *testing.T) { client := getTestClientOrSkip(t) ctx := context.Background() - srv := &TradeService{client: client} - req := srv.NewPlaceOrderRequest() + req := client.NewPlaceOrderRequest() order, err := req. InstrumentID("BTC-USDT"). @@ -78,8 +77,7 @@ func TestClient_PlaceOrderRequest(t *testing.T) { func TestClient_GetPendingOrderRequest(t *testing.T) { client := getTestClientOrSkip(t) ctx := context.Background() - srv := &TradeService{client: client} - req := srv.NewGetPendingOrderRequest() + req := client.NewGetPendingOrderRequest() odr_type := []string{string(OrderTypeLimit), string(OrderTypeIOC)} pending_order, err := req. @@ -94,8 +92,7 @@ func TestClient_GetPendingOrderRequest(t *testing.T) { func TestClient_GetOrderDetailsRequest(t *testing.T) { client := getTestClientOrSkip(t) ctx := context.Background() - srv := &TradeService{client: client} - req := srv.NewGetOrderDetailsRequest() + req := client.NewGetOrderDetailsRequest() orderDetail, err := req. InstrumentID("BTC-USDT"). diff --git a/pkg/exchange/okex/okexapi/market.go b/pkg/exchange/okex/okexapi/market.go index b9b46c43f2..9c5b7d0db5 100644 --- a/pkg/exchange/okex/okexapi/market.go +++ b/pkg/exchange/okex/okexapi/market.go @@ -2,6 +2,7 @@ package okexapi import ( "context" + "encoding/json" "fmt" "net/url" "strconv" @@ -82,29 +83,29 @@ func (r *CandlesticksRequest) Do(ctx context.Context) ([]Candle, error) { params.Add("limit", strconv.Itoa(*r.limit)) } - req, err := r.client.newRequest("GET", "/api/v5/market/candles", params, nil) + req, err := r.client.NewRequest(ctx, "GET", "/api/v5/market/candles", params, nil) if err != nil { return nil, err } - resp, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } type candleEntry [7]string - var candlesResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []candleEntry `json:"data"` - } - if err := resp.DecodeJSON(&candlesResponse); err != nil { + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []candleEntry + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } var candles []Candle - for _, entry := range candlesResponse.Data { + for _, entry := range data { timestamp, err := strconv.ParseInt(entry[0], 10, 64) if err != nil { return candles, err @@ -177,27 +178,26 @@ func (r *MarketTickersRequest) Do(ctx context.Context) ([]MarketTicker, error) { var params = url.Values{} params.Add("instType", string(r.instType)) - req, err := r.client.newRequest("GET", "/api/v5/market/tickers", params, nil) + req, err := r.client.NewRequest(ctx, "GET", "/api/v5/market/tickers", params, nil) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var tickerResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []MarketTicker `json:"data"` + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err } - - if err := response.DecodeJSON(&tickerResponse); err != nil { + var data []MarketTicker + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } - return tickerResponse.Data, nil + return data, nil } type MarketTickerRequest struct { @@ -216,53 +216,49 @@ func (r *MarketTickerRequest) Do(ctx context.Context) (*MarketTicker, error) { var params = url.Values{} params.Add("instId", r.instId) - req, err := r.client.newRequest("GET", "/api/v5/market/ticker", params, nil) + req, err := r.client.NewRequest(ctx, "GET", "/api/v5/market/ticker", params, nil) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var tickerResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []MarketTicker `json:"data"` + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err } - if err := response.DecodeJSON(&tickerResponse); err != nil { + var data []MarketTicker + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } - if len(tickerResponse.Data) == 0 { + if len(data) == 0 { return nil, fmt.Errorf("ticker of %s not found", r.instId) } - return &tickerResponse.Data[0], nil -} - -type MarketDataService struct { - client *RestClient + return &data[0], nil } -func (c *MarketDataService) NewMarketTickerRequest(instId string) *MarketTickerRequest { +func (c *RestClient) NewMarketTickerRequest(instId string) *MarketTickerRequest { return &MarketTickerRequest{ - client: c.client, + client: c, instId: instId, } } -func (c *MarketDataService) NewMarketTickersRequest(instType string) *MarketTickersRequest { +func (c *RestClient) NewMarketTickersRequest(instType string) *MarketTickersRequest { return &MarketTickersRequest{ - client: c.client, + client: c, instType: instType, } } -func (c *MarketDataService) NewCandlesticksRequest(instId string) *CandlesticksRequest { +func (c *RestClient) NewCandlesticksRequest(instId string) *CandlesticksRequest { return &CandlesticksRequest{ - client: c.client, + client: c, instId: instId, } } diff --git a/pkg/exchange/okex/okexapi/public.go b/pkg/exchange/okex/okexapi/public.go index b877fe6372..af4b45496c 100644 --- a/pkg/exchange/okex/okexapi/public.go +++ b/pkg/exchange/okex/okexapi/public.go @@ -2,6 +2,7 @@ package okexapi import ( "context" + "encoding/json" "net/url" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -9,19 +10,15 @@ import ( "github.com/pkg/errors" ) -type PublicDataService struct { - client *RestClient -} - -func (s *PublicDataService) NewGetInstrumentsRequest() *GetInstrumentsRequest { +func (s *RestClient) NewGetInstrumentsRequest() *GetInstrumentsRequest { return &GetInstrumentsRequest{ - client: s.client, + client: s, } } -func (s *PublicDataService) NewGetFundingRate() *GetFundingRateRequest { +func (s *RestClient) NewGetFundingRate() *GetFundingRateRequest { return &GetFundingRateRequest{ - client: s.client, + client: s, } } @@ -49,30 +46,30 @@ func (r *GetFundingRateRequest) Do(ctx context.Context) (*FundingRate, error) { var params = url.Values{} params.Add("instId", string(r.instId)) - req, err := r.client.newRequest("GET", "/api/v5/public/funding-rate", params, nil) + req, err := r.client.NewRequest(ctx, "GET", "/api/v5/public/funding-rate", params, nil) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var apiResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []FundingRate `json:"data"` - } + var apiResponse APIResponse if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } + var data []FundingRate + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } - if len(apiResponse.Data) == 0 { + if len(data) == 0 { return nil, errors.New("empty funding rate data") } - return &apiResponse.Data[0], nil + return &data[0], nil } type Instrument struct { @@ -123,24 +120,24 @@ func (r *GetInstrumentsRequest) Do(ctx context.Context) ([]Instrument, error) { params.Add("instId", *r.instId) } - req, err := r.client.newRequest("GET", "/api/v5/public/instruments", params, nil) + req, err := r.client.NewRequest(ctx, "GET", "/api/v5/public/instruments", params, nil) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var apiResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []Instrument `json:"data"` - } + var apiResponse APIResponse if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } + var data []Instrument + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } - return apiResponse.Data, nil + return data, nil } diff --git a/pkg/exchange/okex/okexapi/trade.go b/pkg/exchange/okex/okexapi/trade.go index 8a814057b6..4c0ecf26ab 100644 --- a/pkg/exchange/okex/okexapi/trade.go +++ b/pkg/exchange/okex/okexapi/trade.go @@ -2,6 +2,7 @@ package okexapi import ( "context" + "encoding/json" "net/url" "strings" @@ -10,10 +11,6 @@ import ( "github.com/pkg/errors" ) -type TradeService struct { - client *RestClient -} - type OrderResponse struct { OrderID string `json:"ordId"` ClientOrderID string `json:"clOrdId"` @@ -22,45 +19,45 @@ type OrderResponse struct { Message string `json:"sMsg"` } -func (c *TradeService) NewPlaceOrderRequest() *PlaceOrderRequest { +func (c *RestClient) NewPlaceOrderRequest() *PlaceOrderRequest { return &PlaceOrderRequest{ - client: c.client, + client: c, } } -func (c *TradeService) NewBatchPlaceOrderRequest() *BatchPlaceOrderRequest { +func (c *RestClient) NewBatchPlaceOrderRequest() *BatchPlaceOrderRequest { return &BatchPlaceOrderRequest{ - client: c.client, + client: c, } } -func (c *TradeService) NewCancelOrderRequest() *CancelOrderRequest { +func (c *RestClient) NewCancelOrderRequest() *CancelOrderRequest { return &CancelOrderRequest{ - client: c.client, + client: c, } } -func (c *TradeService) NewBatchCancelOrderRequest() *BatchCancelOrderRequest { +func (c *RestClient) NewBatchCancelOrderRequest() *BatchCancelOrderRequest { return &BatchCancelOrderRequest{ - client: c.client, + client: c, } } -func (c *TradeService) NewGetOrderDetailsRequest() *GetOrderDetailsRequest { +func (c *RestClient) NewGetOrderDetailsRequest() *GetOrderDetailsRequest { return &GetOrderDetailsRequest{ - client: c.client, + client: c, } } -func (c *TradeService) NewGetPendingOrderRequest() *GetPendingOrderRequest { +func (c *RestClient) NewGetPendingOrderRequest() *GetPendingOrderRequest { return &GetPendingOrderRequest{ - client: c.client, + client: c, } } -func (c *TradeService) NewGetTransactionDetailsRequest() *GetTransactionDetailsRequest { +func (c *RestClient) NewGetTransactionDetailsRequest() *GetTransactionDetailsRequest { return &GetTransactionDetailsRequest{ - client: c.client, + client: c, } } @@ -99,30 +96,30 @@ func (r *PlaceOrderRequest) Parameters() map[string]interface{} { func (r *PlaceOrderRequest) Do(ctx context.Context) (*OrderResponse, error) { payload := r.Parameters() - req, err := r.client.newAuthenticatedRequest("POST", "/api/v5/trade/order", nil, payload) + req, err := r.client.NewAuthenticatedRequest(ctx, "POST", "/api/v5/trade/order", nil, payload) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []OrderResponse `json:"data"` + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err } - if err := response.DecodeJSON(&orderResponse); err != nil { + var data []OrderResponse + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } - if len(orderResponse.Data) == 0 { + if len(data) == 0 { return nil, errors.New("order create error") } - return &orderResponse.Data[0], nil + return &data[0], nil } //go:generate requestgen -type CancelOrderRequest @@ -149,26 +146,26 @@ func (r *CancelOrderRequest) Do(ctx context.Context) ([]OrderResponse, error) { return nil, errors.New("either orderID or clientOrderID is required for canceling order") } - req, err := r.client.newAuthenticatedRequest("POST", "/api/v5/trade/cancel-order", nil, payload) + req, err := r.client.NewAuthenticatedRequest(ctx, "POST", "/api/v5/trade/cancel-order", nil, payload) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []OrderResponse `json:"data"` + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err } - if err := response.DecodeJSON(&orderResponse); err != nil { + var data []OrderResponse + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } - return orderResponse.Data, nil + return data, nil } type BatchCancelOrderRequest struct { @@ -190,26 +187,26 @@ func (r *BatchCancelOrderRequest) Do(ctx context.Context) ([]OrderResponse, erro parameterList = append(parameterList, params) } - req, err := r.client.newAuthenticatedRequest("POST", "/api/v5/trade/cancel-batch-orders", nil, parameterList) + req, err := r.client.NewAuthenticatedRequest(ctx, "POST", "/api/v5/trade/cancel-batch-orders", nil, parameterList) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []OrderResponse `json:"data"` + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err } - if err := response.DecodeJSON(&orderResponse); err != nil { + var data []OrderResponse + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } - return orderResponse.Data, nil + return data, nil } type BatchPlaceOrderRequest struct { @@ -231,26 +228,26 @@ func (r *BatchPlaceOrderRequest) Do(ctx context.Context) ([]OrderResponse, error parameterList = append(parameterList, params) } - req, err := r.client.newAuthenticatedRequest("POST", "/api/v5/trade/batch-orders", nil, parameterList) + req, err := r.client.NewAuthenticatedRequest(ctx, "POST", "/api/v5/trade/batch-orders", nil, parameterList) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []OrderResponse `json:"data"` + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err } - if err := response.DecodeJSON(&orderResponse); err != nil { + var data []OrderResponse + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } - return orderResponse.Data, nil + return data, nil } type OrderDetails struct { @@ -343,30 +340,30 @@ func (r *GetOrderDetailsRequest) QueryParameters() url.Values { func (r *GetOrderDetailsRequest) Do(ctx context.Context) (*OrderDetails, error) { params := r.QueryParameters() - req, err := r.client.newAuthenticatedRequest("GET", "/api/v5/trade/order", params, nil) + req, err := r.client.NewAuthenticatedRequest(ctx, "GET", "/api/v5/trade/order", params, nil) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []OrderDetails `json:"data"` + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err } - if err := response.DecodeJSON(&orderResponse); err != nil { + var data []OrderDetails + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } - if len(orderResponse.Data) == 0 { + if len(data) == 0 { return nil, errors.New("get order details error") } - return &orderResponse.Data[0], nil + return &data[0], nil } type GetPendingOrderRequest struct { @@ -430,26 +427,26 @@ func (r *GetPendingOrderRequest) Parameters() map[string]interface{} { func (r *GetPendingOrderRequest) Do(ctx context.Context) ([]OrderDetails, error) { payload := r.Parameters() - req, err := r.client.newAuthenticatedRequest("GET", "/api/v5/trade/orders-pending", nil, payload) + req, err := r.client.NewAuthenticatedRequest(ctx, "GET", "/api/v5/trade/orders-pending", nil, payload) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []OrderDetails `json:"data"` + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err } - if err := response.DecodeJSON(&orderResponse); err != nil { + var data []OrderDetails + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } - return orderResponse.Data, nil + return data, nil } type GetTransactionDetailsRequest struct { @@ -497,24 +494,24 @@ func (r *GetTransactionDetailsRequest) Parameters() map[string]interface{} { func (r *GetTransactionDetailsRequest) Do(ctx context.Context) ([]OrderDetails, error) { payload := r.Parameters() - req, err := r.client.newAuthenticatedRequest("GET", "/api/v5/trade/fills", nil, payload) + req, err := r.client.NewAuthenticatedRequest(ctx, "GET", "/api/v5/trade/fills", nil, payload) if err != nil { return nil, err } - response, err := r.client.sendRequest(req) + response, err := r.client.SendRequest(req) if err != nil { return nil, err } - var orderResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []OrderDetails `json:"data"` + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err } - if err := response.DecodeJSON(&orderResponse); err != nil { + var data []OrderDetails + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } - return orderResponse.Data, nil + return data, nil } diff --git a/pkg/exchange/okex/query_order_test.go b/pkg/exchange/okex/query_order_test.go index 3c32da40cf..025e045ba3 100644 --- a/pkg/exchange/okex/query_order_test.go +++ b/pkg/exchange/okex/query_order_test.go @@ -22,7 +22,8 @@ func Test_QueryOrder(t *testing.T) { return } - e := New(key, secret, passphrase) + e, err := New(key, secret, passphrase) + assert.NoError(t, err) queryOrder := types.OrderQuery{ Symbol: "BTC-USDT", From dbf29f8cd2d7b983d76f65f881970201dab30cdc Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Wed, 6 Sep 2023 21:21:13 +0800 Subject: [PATCH 1388/1392] add constructor to check url error --- examples/okex-book/main.go | 6 ++---- pkg/exchange/okex/exchange.go | 5 +---- pkg/exchange/okex/okexapi/client.go | 15 ++++++++++----- pkg/exchange/okex/okexapi/client_test.go | 9 +++------ 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/examples/okex-book/main.go b/examples/okex-book/main.go index e6e7341cc3..80591eb745 100644 --- a/examples/okex-book/main.go +++ b/examples/okex-book/main.go @@ -44,10 +44,8 @@ var rootCmd = &cobra.Command{ return errors.New("empty key, secret or passphrase") } - client, err := okexapi.NewClient() - if err != nil { - return errors.New("init client error: please check url") - } + client := okexapi.NewClient() + client.Auth(key, secret, passphrase) instruments, err := client.NewGetInstrumentsRequest(). diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 92e3221160..06b5889b43 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -35,10 +35,7 @@ type Exchange struct { } func New(key, secret, passphrase string) (*Exchange, error) { - client, err := okexapi.NewClient() - if err != nil { - return nil, err - } + client := okexapi.NewClient() if len(key) > 0 && len(secret) > 0 { client.Auth(key, secret, passphrase) diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index 5d05330511..5b0cb7c91b 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -66,21 +66,26 @@ type RestClient struct { Key, Secret, Passphrase string } -func NewClient() (*RestClient, error) { - u, err := url.Parse(RestBaseURL) +var parsedBaseURL *url.URL + +func init() { + url, err := url.Parse(RestBaseURL) if err != nil { - return nil, err + panic(err) } + parsedBaseURL = url +} +func NewClient() *RestClient { client := &RestClient{ BaseAPIClient: requestgen.BaseAPIClient{ - BaseURL: u, + BaseURL: parsedBaseURL, HttpClient: &http.Client{ Timeout: defaultHTTPTimeout, }, }, } - return client, nil + return client } func (c *RestClient) Auth(key, secret, passphrase string) { diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index 8169450a93..1c78783304 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -22,15 +22,13 @@ func getTestClientOrSkip(t *testing.T) *RestClient { return nil } - client, err := NewClient() - assert.NoError(t, err) + client := NewClient() client.Auth(key, secret, passphrase) return client } func TestClient_GetInstrumentsRequest(t *testing.T) { - client, err := NewClient() - assert.NoError(t, err) + client := NewClient() ctx := context.Background() req := client.NewGetInstrumentsRequest() @@ -43,8 +41,7 @@ func TestClient_GetInstrumentsRequest(t *testing.T) { } func TestClient_GetFundingRateRequest(t *testing.T) { - client, err := NewClient() - assert.NoError(t, err) + client := NewClient() ctx := context.Background() req := client.NewGetFundingRate() From 9a66f82d8c69a028926520786e62f8ce27767425 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Thu, 7 Sep 2023 11:25:12 +0800 Subject: [PATCH 1389/1392] remove err since handling init for client --- pkg/exchange/factory.go | 2 +- pkg/exchange/okex/exchange.go | 4 ++-- pkg/exchange/okex/query_order_test.go | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index c7f3371cab..00d6db7c9f 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -37,7 +37,7 @@ func New(n types.ExchangeName, key, secret, passphrase string) (types.ExchangeMi return max.New(key, secret), nil case types.ExchangeOKEx: - return okex.New(key, secret, passphrase) + return okex.New(key, secret, passphrase), nil case types.ExchangeKucoin: return kucoin.New(key, secret, passphrase), nil diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 06b5889b43..d09fd76bdc 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -34,7 +34,7 @@ type Exchange struct { client *okexapi.RestClient } -func New(key, secret, passphrase string) (*Exchange, error) { +func New(key, secret, passphrase string) *Exchange { client := okexapi.NewClient() if len(key) > 0 && len(secret) > 0 { @@ -46,7 +46,7 @@ func New(key, secret, passphrase string) (*Exchange, error) { secret: secret, passphrase: passphrase, client: client, - }, nil + } } func (e *Exchange) Name() types.ExchangeName { diff --git a/pkg/exchange/okex/query_order_test.go b/pkg/exchange/okex/query_order_test.go index 025e045ba3..3c32da40cf 100644 --- a/pkg/exchange/okex/query_order_test.go +++ b/pkg/exchange/okex/query_order_test.go @@ -22,8 +22,7 @@ func Test_QueryOrder(t *testing.T) { return } - e, err := New(key, secret, passphrase) - assert.NoError(t, err) + e := New(key, secret, passphrase) queryOrder := types.OrderQuery{ Symbol: "BTC-USDT", From c9d67f8131fb64d3d1b13851243a5fbda374a5f7 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 7 Sep 2023 15:21:47 +0800 Subject: [PATCH 1390/1392] improve/binance: update futures account api to v2 --- .../binanceapi/futures_get_account_request.go | 67 +++++++++ .../futures_get_account_request_requestgen.go | 135 ++++++++++++++++++ pkg/exchange/binance/convert_futures.go | 7 +- pkg/exchange/binance/futures.go | 4 +- 4 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 pkg/exchange/binance/binanceapi/futures_get_account_request.go create mode 100644 pkg/exchange/binance/binanceapi/futures_get_account_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/futures_get_account_request.go b/pkg/exchange/binance/binanceapi/futures_get_account_request.go new file mode 100644 index 0000000000..131793f11b --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_account_request.go @@ -0,0 +1,67 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" +) + +// FuturesAccountAsset define account asset +type FuturesAccountAsset struct { + Asset string `json:"asset"` + InitialMargin string `json:"initialMargin"` + MaintMargin string `json:"maintMargin"` + MarginBalance string `json:"marginBalance"` + MaxWithdrawAmount string `json:"maxWithdrawAmount"` + OpenOrderInitialMargin string `json:"openOrderInitialMargin"` + PositionInitialMargin string `json:"positionInitialMargin"` + UnrealizedProfit string `json:"unrealizedProfit"` + WalletBalance string `json:"walletBalance"` +} + +// FuturesAccountPosition define account position +type FuturesAccountPosition struct { + Isolated bool `json:"isolated"` + Leverage string `json:"leverage"` + InitialMargin string `json:"initialMargin"` + MaintMargin string `json:"maintMargin"` + OpenOrderInitialMargin string `json:"openOrderInitialMargin"` + PositionInitialMargin string `json:"positionInitialMargin"` + Symbol string `json:"symbol"` + UnrealizedProfit string `json:"unrealizedProfit"` + EntryPrice string `json:"entryPrice"` + MaxNotional string `json:"maxNotional"` + PositionSide string `json:"positionSide"` + PositionAmt string `json:"positionAmt"` + Notional string `json:"notional"` + IsolatedWallet string `json:"isolatedWallet"` + UpdateTime int64 `json:"updateTime"` +} + +type FuturesAccount struct { + Assets []*FuturesAccountAsset `json:"assets"` + FeeTier int `json:"feeTier"` + CanTrade bool `json:"canTrade"` + CanDeposit bool `json:"canDeposit"` + CanWithdraw bool `json:"canWithdraw"` + UpdateTime int64 `json:"updateTime"` + TotalInitialMargin string `json:"totalInitialMargin"` + TotalMaintMargin string `json:"totalMaintMargin"` + TotalWalletBalance string `json:"totalWalletBalance"` + TotalUnrealizedProfit string `json:"totalUnrealizedProfit"` + TotalMarginBalance string `json:"totalMarginBalance"` + TotalPositionInitialMargin string `json:"totalPositionInitialMargin"` + TotalOpenOrderInitialMargin string `json:"totalOpenOrderInitialMargin"` + TotalCrossWalletBalance string `json:"totalCrossWalletBalance"` + TotalCrossUnPnl string `json:"totalCrossUnPnl"` + AvailableBalance string `json:"availableBalance"` + MaxWithdrawAmount string `json:"maxWithdrawAmount"` + Positions []*FuturesAccountPosition `json:"positions"` +} + +//go:generate requestgen -method GET -url "/fapi/v2/account" -type FuturesGetAccountRequest -responseType FuturesAccount +type FuturesGetAccountRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *FuturesRestClient) NewFuturesGetAccountRequest() *FuturesGetAccountRequest { + return &FuturesGetAccountRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_get_account_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_get_account_request_requestgen.go new file mode 100644 index 0000000000..a9d3f04f8c --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_get_account_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method GET -url /fapi/v2/account -type FuturesGetAccountRequest -responseType FuturesAccount"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGetAccountRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGetAccountRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGetAccountRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGetAccountRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGetAccountRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGetAccountRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGetAccountRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGetAccountRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGetAccountRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesGetAccountRequest) Do(ctx context.Context) (*FuturesAccount, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/fapi/v2/account" + + req, err := f.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse FuturesAccount + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/binance/convert_futures.go b/pkg/exchange/binance/convert_futures.go index 30187c422a..9811e62d2f 100644 --- a/pkg/exchange/binance/convert_futures.go +++ b/pkg/exchange/binance/convert_futures.go @@ -2,6 +2,7 @@ package binance import ( "fmt" + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" "time" "github.com/adshao/go-binance/v2/futures" @@ -11,7 +12,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -func toGlobalFuturesAccountInfo(account *futures.Account) *types.FuturesAccountInfo { +func toGlobalFuturesAccountInfo(account *binanceapi.FuturesAccount) *types.FuturesAccountInfo { return &types.FuturesAccountInfo{ Assets: toGlobalFuturesUserAssets(account.Assets), Positions: toGlobalFuturesPositions(account.Positions), @@ -37,7 +38,7 @@ func toGlobalFuturesBalance(balances []*futures.Balance) types.BalanceMap { return retBalances } -func toGlobalFuturesPositions(futuresPositions []*futures.AccountPosition) types.FuturesPositionMap { +func toGlobalFuturesPositions(futuresPositions []*binanceapi.FuturesAccountPosition) types.FuturesPositionMap { retFuturesPositions := make(types.FuturesPositionMap) for _, futuresPosition := range futuresPositions { retFuturesPositions[futuresPosition.Symbol] = types.FuturesPosition{ // TODO: types.FuturesPosition @@ -58,7 +59,7 @@ func toGlobalFuturesPositions(futuresPositions []*futures.AccountPosition) types return retFuturesPositions } -func toGlobalFuturesUserAssets(assets []*futures.AccountAsset) (retAssets types.FuturesAssetMap) { +func toGlobalFuturesUserAssets(assets []*binanceapi.FuturesAccountAsset) (retAssets types.FuturesAssetMap) { retFuturesAssets := make(types.FuturesAssetMap) for _, futuresAsset := range assets { retFuturesAssets[futuresAsset.Asset] = types.FuturesUserAsset{ diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index f6266e7a24..ac59edc8a1 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -63,7 +63,9 @@ func (e *Exchange) TransferFuturesAccountAsset(ctx context.Context, asset string // Balance.Available = Wallet Balance(in Binance UI) - Used Margin // Balance.Locked = Used Margin func (e *Exchange) QueryFuturesAccount(ctx context.Context) (*types.Account, error) { - account, err := e.futuresClient.NewGetAccountService().Do(ctx) + //account, err := e.futuresClient.NewGetAccountService().Do(ctx) + req_account := e.futuresClient2.NewFuturesGetAccountRequest() + account, err := req_account.Do(ctx) if err != nil { return nil, err } From 40dbfd5b42db90fef4d00c908ba183e6e4c269d9 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 8 Sep 2023 18:14:51 +0800 Subject: [PATCH 1391/1392] improve/binance: rename variable to comply with golang naming convention --- pkg/exchange/binance/futures.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/binance/futures.go b/pkg/exchange/binance/futures.go index ac59edc8a1..32579f224b 100644 --- a/pkg/exchange/binance/futures.go +++ b/pkg/exchange/binance/futures.go @@ -64,8 +64,8 @@ func (e *Exchange) TransferFuturesAccountAsset(ctx context.Context, asset string // Balance.Locked = Used Margin func (e *Exchange) QueryFuturesAccount(ctx context.Context) (*types.Account, error) { //account, err := e.futuresClient.NewGetAccountService().Do(ctx) - req_account := e.futuresClient2.NewFuturesGetAccountRequest() - account, err := req_account.Do(ctx) + reqAccount := e.futuresClient2.NewFuturesGetAccountRequest() + account, err := reqAccount.Do(ctx) if err != nil { return nil, err } From 1c5ad1d1f01f9244959b7c8b84b3c9506cef9ee3 Mon Sep 17 00:00:00 2001 From: Edwin Date: Sun, 10 Sep 2023 20:30:03 +0800 Subject: [PATCH 1392/1392] pkg/exchange: support market trade for bybit --- pkg/exchange/bybit/stream.go | 44 ++++++++++++++++--- pkg/exchange/bybit/stream_callbacks.go | 10 +++++ pkg/exchange/bybit/stream_test.go | 59 ++++++++++++++++++++++++++ pkg/exchange/bybit/types.go | 57 ++++++++++++++++++++++--- pkg/exchange/bybit/types_test.go | 49 +++++++++++++++++++++ 5 files changed, 207 insertions(+), 12 deletions(-) diff --git a/pkg/exchange/bybit/stream.go b/pkg/exchange/bybit/stream.go index 5b9ced6eb8..db7f257709 100644 --- a/pkg/exchange/bybit/stream.go +++ b/pkg/exchange/bybit/stream.go @@ -42,11 +42,12 @@ type Stream struct { // TODO: update the fee rate at 7:00 am UTC; rotation required. symbolFeeDetails map[string]*symbolFeeDetail - bookEventCallbacks []func(e BookEvent) - walletEventCallbacks []func(e []bybitapi.WalletBalances) - kLineEventCallbacks []func(e KLineEvent) - orderEventCallbacks []func(e []OrderEvent) - tradeEventCallbacks []func(e []TradeEvent) + bookEventCallbacks []func(e BookEvent) + marketTradeEventCallbacks []func(e []MarketTradeEvent) + walletEventCallbacks []func(e []bybitapi.WalletBalances) + kLineEventCallbacks []func(e KLineEvent) + orderEventCallbacks []func(e []OrderEvent) + tradeEventCallbacks []func(e []TradeEvent) } func NewStream(key, secret string, marketProvider MarketInfoProvider) *Stream { @@ -66,6 +67,7 @@ func NewStream(key, secret string, marketProvider MarketInfoProvider) *Stream { stream.OnConnect(stream.handlerConnect) stream.OnBookEvent(stream.handleBookEvent) + stream.OnMarketTradeEvent(stream.handleMarketTradeEvent) stream.OnKLineEvent(stream.handleKLineEvent) stream.OnWalletEvent(stream.handleWalletEvent) stream.OnOrderEvent(stream.handleOrderEvent) @@ -139,6 +141,9 @@ func (s *Stream) dispatchEvent(event interface{}) { case *BookEvent: s.EmitBookEvent(*e) + case []MarketTradeEvent: + s.EmitMarketTradeEvent(e) + case []bybitapi.WalletBalances: s.EmitWalletEvent(e) @@ -173,18 +178,28 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) { var book BookEvent err = json.Unmarshal(e.WebSocketTopicEvent.Data, &book) if err != nil { - return nil, fmt.Errorf("failed to unmarshal data into BookEvent: %+v, : %w", string(e.WebSocketTopicEvent.Data), err) + return nil, fmt.Errorf("failed to unmarshal data into BookEvent: %+v, err: %w", string(e.WebSocketTopicEvent.Data), err) } book.Type = e.WebSocketTopicEvent.Type book.ServerTime = e.WebSocketTopicEvent.Ts.Time() return &book, nil + case TopicTypeMarketTrade: + // snapshot only + var trade []MarketTradeEvent + err = json.Unmarshal(e.WebSocketTopicEvent.Data, &trade) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal data into MarketTradeEvent: %+v, err: %w", string(e.WebSocketTopicEvent.Data), err) + } + + return trade, nil + case TopicTypeKLine: var kLines []KLine err = json.Unmarshal(e.WebSocketTopicEvent.Data, &kLines) if err != nil { - return nil, fmt.Errorf("failed to unmarshal data into KLine: %+v, : %w", string(e.WebSocketTopicEvent.Data), err) + return nil, fmt.Errorf("failed to unmarshal data into KLine: %+v, err: %w", string(e.WebSocketTopicEvent.Data), err) } symbol, err := getSymbolFromTopic(e.Topic) @@ -292,6 +307,9 @@ func (s *Stream) convertSubscription(sub types.Subscription) (string, error) { } return genTopic(TopicTypeOrderBook, depth, sub.Symbol), nil + case types.MarketTradeChannel: + return genTopic(TopicTypeMarketTrade, sub.Symbol), nil + case types.KLineChannel: interval, err := toLocalInterval(sub.Options.Interval) if err != nil { @@ -318,6 +336,18 @@ func (s *Stream) handleBookEvent(e BookEvent) { } } +func (s *Stream) handleMarketTradeEvent(events []MarketTradeEvent) { + for _, event := range events { + trade, err := event.toGlobalTrade() + if err != nil { + log.WithError(err).Error("failed to convert to market trade") + continue + } + + s.StandardStream.EmitMarketTrade(trade) + } +} + func (s *Stream) handleWalletEvent(events []bybitapi.WalletBalances) { s.StandardStream.EmitBalanceSnapshot(toGlobalBalanceMap(events)) } diff --git a/pkg/exchange/bybit/stream_callbacks.go b/pkg/exchange/bybit/stream_callbacks.go index fcd1f68595..463e7dbd7b 100644 --- a/pkg/exchange/bybit/stream_callbacks.go +++ b/pkg/exchange/bybit/stream_callbacks.go @@ -16,6 +16,16 @@ func (s *Stream) EmitBookEvent(e BookEvent) { } } +func (s *Stream) OnMarketTradeEvent(cb func(e []MarketTradeEvent)) { + s.marketTradeEventCallbacks = append(s.marketTradeEventCallbacks, cb) +} + +func (s *Stream) EmitMarketTradeEvent(e []MarketTradeEvent) { + for _, cb := range s.marketTradeEventCallbacks { + cb(e) + } +} + func (s *Stream) OnWalletEvent(cb func(e []bybitapi.WalletBalances)) { s.walletEventCallbacks = append(s.walletEventCallbacks, cb) } diff --git a/pkg/exchange/bybit/stream_test.go b/pkg/exchange/bybit/stream_test.go index 12e4837654..daf839872d 100644 --- a/pkg/exchange/bybit/stream_test.go +++ b/pkg/exchange/bybit/stream_test.go @@ -112,6 +112,19 @@ func TestStream(t *testing.T) { <-c }) + t.Run("market trade test", func(t *testing.T) { + s.Subscribe(types.MarketTradeChannel, "BTCUSDT", types.SubscribeOptions{}) + s.SetPublicOnly() + err := s.Connect(context.Background()) + assert.NoError(t, err) + + s.OnMarketTrade(func(trade types.Trade) { + t.Log("got update", trade) + }) + c := make(chan struct{}) + <-c + }) + t.Run("wallet test", func(t *testing.T) { err := s.Connect(context.Background()) assert.NoError(t, err) @@ -237,6 +250,42 @@ func TestStream_parseWebSocketEvent(t *testing.T) { }, *book) }) + t.Run("TopicTypeMarketTrade with snapshot", func(t *testing.T) { + input := `{ + "topic":"publicTrade.BTCUSDT", + "ts":1694348711526, + "type":"snapshot", + "data":[ + { + "i":"2290000000068683805", + "T":1694348711524, + "p":"25816.27", + "v":"0.000083", + "S":"Sell", + "s":"BTCUSDT", + "BT":false + } + ] +}` + + res, err := s.parseWebSocketEvent([]byte(input)) + assert.NoError(t, err) + book, ok := res.([]MarketTradeEvent) + assert.True(t, ok) + assert.Equal(t, []MarketTradeEvent{ + { + Timestamp: types.NewMillisecondTimestampFromInt(1694348711524), + Symbol: "BTCUSDT", + Side: bybitapi.SideSell, + Quantity: fixedpoint.NewFromFloat(0.000083), + Price: fixedpoint.NewFromFloat(25816.27), + Direction: "", + TradeId: "2290000000068683805", + BlockTrade: false, + }, + }, book) + }) + t.Run("TopicTypeKLine with snapshot", func(t *testing.T) { input := `{ "topic": "kline.5.BTCUSDT", @@ -379,6 +428,16 @@ func Test_convertSubscription(t *testing.T) { assert.Equal(t, fmt.Errorf("unsupported stream channel: %s", "unsupported"), err) assert.Equal(t, "", res) }) + + t.Run("MarketTradeChannel", func(t *testing.T) { + res, err := s.convertSubscription(types.Subscription{ + Symbol: "BTCUSDT", + Channel: types.MarketTradeChannel, + Options: types.SubscribeOptions{}, + }) + assert.NoError(t, err) + assert.Equal(t, genTopic(TopicTypeMarketTrade, "BTCUSDT"), res) + }) } func TestStream_getFeeRate(t *testing.T) { diff --git a/pkg/exchange/bybit/types.go b/pkg/exchange/bybit/types.go index c5acd4916f..f5caedf8dc 100644 --- a/pkg/exchange/bybit/types.go +++ b/pkg/exchange/bybit/types.go @@ -91,11 +91,12 @@ func (w *WebSocketOpEvent) IsValid() error { type TopicType string const ( - TopicTypeOrderBook TopicType = "orderbook" - TopicTypeWallet TopicType = "wallet" - TopicTypeOrder TopicType = "order" - TopicTypeKLine TopicType = "kline" - TopicTypeTrade TopicType = "execution" + TopicTypeOrderBook TopicType = "orderbook" + TopicTypeMarketTrade TopicType = "publicTrade" + TopicTypeWallet TopicType = "wallet" + TopicTypeOrder TopicType = "order" + TopicTypeKLine TopicType = "kline" + TopicTypeTrade TopicType = "execution" ) type DataType string @@ -143,6 +144,52 @@ func (e *BookEvent) OrderBook() (snapshot types.SliceOrderBook) { return snapshot } +type MarketTradeEvent struct { + // Timestamp is the timestamp (ms) that the order is filled + Timestamp types.MillisecondTimestamp `json:"T"` + Symbol string `json:"s"` + // Side of taker. Buy,Sell + Side bybitapi.Side `json:"S"` + // Quantity is the trade size + Quantity fixedpoint.Value `json:"v"` + // Price is the trade price + Price fixedpoint.Value `json:"p"` + // L is the direction of price change. Unique field for future + Direction string `json:"L"` + // trade ID + TradeId string `json:"i"` + // Whether it is a block trade order or not + BlockTrade bool `json:"BT"` +} + +func (m *MarketTradeEvent) toGlobalTrade() (types.Trade, error) { + tradeId, err := strconv.ParseUint(m.TradeId, 10, 64) + if err != nil { + return types.Trade{}, fmt.Errorf("unexpected trade id: %s, err: %w", m.TradeId, err) + } + + side, err := toGlobalSideType(m.Side) + if err != nil { + return types.Trade{}, err + } + + return types.Trade{ + ID: tradeId, + OrderID: 0, // not supported + Exchange: types.ExchangeBybit, + Price: m.Price, + Quantity: m.Quantity, + QuoteQuantity: m.Price.Mul(m.Quantity), + Symbol: m.Symbol, + Side: side, + IsBuyer: side == types.SideTypeBuy, + IsMaker: false, // not supported + Time: types.Time(m.Timestamp.Time()), + Fee: fixedpoint.Zero, // not supported + FeeCurrency: "", // not supported + }, nil +} + const topicSeparator = "." func genTopic(in ...interface{}) string { diff --git a/pkg/exchange/bybit/types_test.go b/pkg/exchange/bybit/types_test.go index 21fc04f4f1..5f32abd776 100644 --- a/pkg/exchange/bybit/types_test.go +++ b/pkg/exchange/bybit/types_test.go @@ -406,6 +406,55 @@ func TestBookEvent_OrderBook(t *testing.T) { } +func TestMarketTradeEvent_Trade(t *testing.T) { + qty := fixedpoint.NewFromFloat(0.002289) + price := fixedpoint.NewFromFloat(28829.7600) + tradeId := uint64(2290000000068683542) + tradeTime := types.NewMillisecondTimestampFromInt(1691486100000) + event := MarketTradeEvent{ + Timestamp: tradeTime, + Symbol: "BTCUSDT", + Side: bybitapi.SideSell, + Quantity: qty, + Price: price, + Direction: "", + TradeId: strconv.FormatUint(tradeId, 10), + BlockTrade: false, + } + t.Run("succeeds", func(t *testing.T) { + expEvent := types.Trade{ + ID: tradeId, + Exchange: types.ExchangeBybit, + Price: price, + Quantity: qty, + QuoteQuantity: price.Mul(qty), + Symbol: event.Symbol, + Side: types.SideTypeSell, + IsBuyer: false, + IsMaker: false, + Time: types.Time(tradeTime.Time()), + } + + trade, err := event.toGlobalTrade() + assert.NoError(t, err) + assert.Equal(t, expEvent, trade) + }) + + t.Run("invalid side", func(t *testing.T) { + newEvent := event + newEvent.Side = "invalid" + _, err := newEvent.toGlobalTrade() + assert.ErrorContains(t, err, "unexpected side") + }) + + t.Run("invalid trade id", func(t *testing.T) { + newEvent := event + newEvent.TradeId = "invalid" + _, err := newEvent.toGlobalTrade() + assert.ErrorContains(t, err, "unexpected trade id") + }) +} + func Test_genTopicName(t *testing.T) { exp := "orderbook.50.BTCUSDT" assert.Equal(t, exp, genTopic(TopicTypeOrderBook, types.DepthLevel50, "BTCUSDT"))